diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8a94efd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,60 @@
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs
+hs_err_pid*
+replay_pid*
+
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# Gradle
+.gradle/
+build/
+
+# IDE
+.idea/
+*.iml
+*.ipr
+*.iws
+.project
+.classpath
+.settings/
+.vscode/
+*.swp
+*.swo
+.factorypath
+
+# OS
+.DS_Store
+Thumbs.db
+
+# IronLicensing cache
+.ironlicensing/
diff --git a/README.md b/README.md
index 76b40c8..faa40bb 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,358 @@
-# ironlicensing-java
-IronLicensing SDK for Java - Software licensing and activation
+# IronLicensing Java SDK
+
+Official Java SDK for [IronLicensing](https://ironlicensing.com) - Software licensing and activation for your applications.
+
+## Installation
+
+### Maven
+
+```xml
+
+ com.ironservices
+ licensing
+ 1.0.0
+
+```
+
+### Gradle
+
+```groovy
+implementation 'com.ironservices:licensing:1.0.0'
+```
+
+## Quick Start
+
+### Using Static API
+
+```java
+import com.ironservices.licensing.*;
+
+public class Main {
+ public static void main(String[] args) {
+ // Initialize the SDK
+ IronLicensing.init("pk_live_your_public_key", "your-product-slug");
+
+ // Validate a license
+ LicenseResult result = IronLicensing.validate("IRON-XXXX-XXXX-XXXX-XXXX");
+ if (result.isValid()) {
+ System.out.println("License is valid!");
+ System.out.println("Status: " + result.getLicense().getStatus());
+ } else {
+ System.out.println("Validation failed: " + result.getError());
+ }
+
+ // Check for features
+ if (IronLicensing.hasFeature("premium")) {
+ System.out.println("Premium features enabled!");
+ }
+ }
+}
+```
+
+### Using Client Instance
+
+```java
+import com.ironservices.licensing.*;
+
+public class Main {
+ public static void main(String[] args) {
+ LicenseOptions options = LicenseOptions.builder("pk_live_your_public_key", "your-product-slug")
+ .debug(true)
+ .build();
+
+ LicenseClient client = new LicenseClient(options);
+
+ // Activate license
+ LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX", "My Machine");
+
+ if (result.isValid()) {
+ System.out.println("Activated! License type: " + result.getLicense().getType());
+ }
+ }
+}
+```
+
+## Configuration
+
+### Builder Pattern
+
+```java
+LicenseOptions options = LicenseOptions.builder("pk_live_xxx", "your-product")
+ .apiBaseUrl("https://api.ironlicensing.com") // Custom API URL
+ .debug(true) // Enable debug logging
+ .enableOfflineCache(true) // Cache for offline use
+ .cacheValidationMinutes(60) // Cache duration
+ .offlineGraceDays(7) // Offline grace period
+ .httpTimeout(Duration.ofSeconds(30)) // Request timeout
+ .build();
+```
+
+### Fluent Setters
+
+```java
+LicenseOptions options = new LicenseOptions("pk_live_xxx", "your-product")
+ .setDebug(true)
+ .setEnableOfflineCache(true);
+```
+
+## License Validation
+
+```java
+// Synchronous validation
+LicenseResult result = client.validate("IRON-XXXX-XXXX-XXXX-XXXX");
+
+// Asynchronous validation
+client.validateAsync("IRON-XXXX-XXXX-XXXX-XXXX")
+ .thenAccept(r -> {
+ if (r.isValid()) {
+ License license = r.getLicense();
+ System.out.println("License: " + license.getKey());
+ System.out.println("Status: " + license.getStatus());
+ System.out.println("Type: " + license.getType());
+ System.out.printf("Activations: %d/%d%n",
+ license.getCurrentActivations(),
+ license.getMaxActivations());
+ }
+ });
+```
+
+## License Activation
+
+```java
+// Simple activation (uses hostname as machine name)
+LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX");
+
+// With custom machine name
+LicenseResult result = client.activate("IRON-XXXX-XXXX-XXXX-XXXX", "Production Server");
+
+if (result.isValid()) {
+ System.out.println("License activated successfully!");
+
+ // View activations
+ for (Activation activation : result.getActivations()) {
+ System.out.printf("- %s (%s)%n", activation.getMachineName(), activation.getPlatform());
+ }
+}
+
+// Async activation
+client.activateAsync(licenseKey, "My Machine")
+ .thenAccept(r -> System.out.println("Activated: " + r.isValid()));
+```
+
+## License Deactivation
+
+```java
+// Synchronous
+if (client.deactivate()) {
+ System.out.println("License deactivated from this machine");
+}
+
+// Asynchronous
+client.deactivateAsync()
+ .thenAccept(success -> {
+ if (success) {
+ System.out.println("Deactivated successfully");
+ }
+ });
+```
+
+## Feature Checking
+
+```java
+// Check if feature is available
+if (client.hasFeature("advanced-analytics")) {
+ // Enable advanced analytics
+}
+
+// Require feature (throws LicenseRequiredException if not available)
+try {
+ client.requireFeature("export-pdf");
+ // Feature is available, continue with export
+} catch (LicenseRequiredException e) {
+ System.out.println("Feature not available: " + e.getFeature());
+}
+
+// Get feature details
+Feature feature = client.getFeature("max-users");
+if (feature != null) {
+ System.out.printf("Feature: %s - %s%n", feature.getName(), feature.getDescription());
+}
+```
+
+## Trial Management
+
+```java
+LicenseResult result = client.startTrial("user@example.com");
+
+if (result.isValid()) {
+ System.out.println("Trial started!");
+ System.out.println("Trial key: " + result.getLicense().getKey());
+
+ String expiresAt = result.getLicense().getExpiresAt();
+ if (expiresAt != null) {
+ System.out.println("Expires: " + expiresAt);
+ }
+}
+```
+
+## In-App Purchase
+
+```java
+// Get available tiers
+List tiers = client.getTiers();
+for (ProductTier tier : tiers) {
+ System.out.printf("%s - $%.2f %s%n", tier.getName(), tier.getPrice(), tier.getCurrency());
+}
+
+// Start checkout
+CheckoutResult checkout = client.startPurchase("tier-id", "user@example.com");
+if (checkout.isSuccess()) {
+ System.out.println("Checkout URL: " + checkout.getCheckoutUrl());
+ // Open URL in browser for user to complete purchase
+}
+
+// Async purchase
+client.startPurchaseAsync("tier-id", "user@example.com")
+ .thenAccept(c -> {
+ if (c.isSuccess()) {
+ // Handle success
+ }
+ });
+```
+
+## License Status
+
+```java
+// Get current license
+License license = client.getLicense();
+if (license != null) {
+ System.out.println("Licensed to: " + license.getEmail());
+}
+
+// Check status
+LicenseStatus status = client.getStatus();
+switch (status) {
+ case VALID:
+ System.out.println("License is valid");
+ break;
+ case EXPIRED:
+ System.out.println("License has expired");
+ break;
+ case TRIAL:
+ System.out.println("Running in trial mode");
+ break;
+ case NOT_ACTIVATED:
+ System.out.println("No license activated");
+ break;
+ default:
+ System.out.println("Status: " + status);
+}
+
+// Quick checks
+if (client.isLicensed()) {
+ System.out.println("Application is licensed");
+}
+
+if (client.isTrial()) {
+ System.out.println("Running in trial mode");
+}
+```
+
+## License Change Listener
+
+```java
+client.setOnLicenseChanged(license -> {
+ if (license != null) {
+ System.out.println("License updated: " + license.getStatus());
+ } else {
+ System.out.println("License removed");
+ }
+});
+```
+
+## 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 |
+| `TRIAL_EXPIRED` | Trial period ended |
+| `NOT_ACTIVATED` | No license activated |
+
+## Thread Safety
+
+The client is thread-safe and can be used concurrently:
+
+```java
+ExecutorService executor = Executors.newFixedThreadPool(10);
+for (int i = 0; i < 10; i++) {
+ executor.submit(() -> {
+ if (client.hasFeature("concurrent-feature")) {
+ // Safe to call from multiple threads
+ }
+ });
+}
+executor.shutdown();
+```
+
+## Error Handling
+
+```java
+// Validation errors
+LicenseResult result = client.validate(licenseKey);
+if (!result.isValid()) {
+ String error = result.getError();
+ switch (error) {
+ case "license_not_found":
+ System.out.println("Invalid license key");
+ break;
+ case "license_expired":
+ System.out.println("Your license has expired");
+ break;
+ case "max_activations_reached":
+ System.out.println("No more activations available");
+ break;
+ default:
+ System.out.println("Error: " + error);
+ }
+}
+
+// Feature requirement errors
+try {
+ client.requireFeature("premium");
+} catch (LicenseRequiredException e) {
+ System.out.printf("Feature '%s' requires a valid license%n", e.getFeature());
+}
+```
+
+## 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
+
+```java
+String machineId = client.getMachineId();
+```
+
+## Requirements
+
+- Java 11 or later
+- OkHttp 4.x
+- Gson 2.x
+
+## License
+
+MIT License - see LICENSE file for details.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..518f40e
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ com.ironservices
+ licensing
+ 1.0.0
+ jar
+
+ IronLicensing Java SDK
+ Official Java SDK for IronLicensing - Software licensing and activation
+ https://github.com/IronServices/ironlicensing-java
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ IronServices
+ support@ironservices.com
+ IronServices
+ https://ironservices.com
+
+
+
+
+ scm:git:git://github.com/IronServices/ironlicensing-java.git
+ scm:git:ssh://github.com:IronServices/ironlicensing-java.git
+ https://github.com/IronServices/ironlicensing-java/tree/main
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar-no-fork
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.0
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/src/main/java/com/ironservices/licensing/Activation.java b/src/main/java/com/ironservices/licensing/Activation.java
new file mode 100644
index 0000000..ef36280
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/Activation.java
@@ -0,0 +1,81 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents an activation of a license on a machine.
+ */
+public class Activation {
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("machineId")
+ private String machineId;
+
+ @SerializedName("machineName")
+ private String machineName;
+
+ @SerializedName("platform")
+ private String platform;
+
+ @SerializedName("activatedAt")
+ private String activatedAt;
+
+ @SerializedName("lastSeenAt")
+ private String lastSeenAt;
+
+ public Activation() {}
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getMachineId() {
+ return machineId;
+ }
+
+ public void setMachineId(String machineId) {
+ this.machineId = machineId;
+ }
+
+ public String getMachineName() {
+ return machineName;
+ }
+
+ public void setMachineName(String machineName) {
+ this.machineName = machineName;
+ }
+
+ public String getPlatform() {
+ return platform;
+ }
+
+ public void setPlatform(String platform) {
+ this.platform = platform;
+ }
+
+ public String getActivatedAt() {
+ return activatedAt;
+ }
+
+ public void setActivatedAt(String activatedAt) {
+ this.activatedAt = activatedAt;
+ }
+
+ public String getLastSeenAt() {
+ return lastSeenAt;
+ }
+
+ public void setLastSeenAt(String lastSeenAt) {
+ this.lastSeenAt = lastSeenAt;
+ }
+
+ @Override
+ public String toString() {
+ return "Activation{id='" + id + "', machineName='" + machineName + "', platform='" + platform + "'}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/CheckoutResult.java b/src/main/java/com/ironservices/licensing/CheckoutResult.java
new file mode 100644
index 0000000..39e7cfe
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/CheckoutResult.java
@@ -0,0 +1,76 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents the result of starting a checkout.
+ */
+public class CheckoutResult {
+ @SerializedName("success")
+ private boolean success;
+
+ @SerializedName("checkoutUrl")
+ private String checkoutUrl;
+
+ @SerializedName("sessionId")
+ private String sessionId;
+
+ @SerializedName("error")
+ private String error;
+
+ public CheckoutResult() {}
+
+ public CheckoutResult(boolean success, String error) {
+ this.success = success;
+ this.error = error;
+ }
+
+ public static CheckoutResult success(String checkoutUrl, String sessionId) {
+ CheckoutResult result = new CheckoutResult();
+ result.success = true;
+ result.checkoutUrl = checkoutUrl;
+ result.sessionId = sessionId;
+ return result;
+ }
+
+ public static CheckoutResult failure(String error) {
+ return new CheckoutResult(false, error);
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public String getCheckoutUrl() {
+ return checkoutUrl;
+ }
+
+ public void setCheckoutUrl(String checkoutUrl) {
+ this.checkoutUrl = checkoutUrl;
+ }
+
+ public String getSessionId() {
+ return sessionId;
+ }
+
+ public void setSessionId(String sessionId) {
+ this.sessionId = sessionId;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+
+ @Override
+ public String toString() {
+ return "CheckoutResult{success=" + success + ", checkoutUrl='" + checkoutUrl + "'}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/Feature.java b/src/main/java/com/ironservices/licensing/Feature.java
new file mode 100644
index 0000000..49a8142
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/Feature.java
@@ -0,0 +1,77 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.Map;
+
+/**
+ * Represents a feature in a license.
+ */
+public class Feature {
+ @SerializedName("key")
+ private String key;
+
+ @SerializedName("name")
+ private String name;
+
+ @SerializedName("enabled")
+ private boolean enabled;
+
+ @SerializedName("description")
+ private String description;
+
+ @SerializedName("metadata")
+ private Map metadata;
+
+ public Feature() {}
+
+ public Feature(String key, String name, boolean enabled) {
+ this.key = key;
+ this.name = name;
+ this.enabled = enabled;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public String toString() {
+ return "Feature{key='" + key + "', name='" + name + "', enabled=" + enabled + "}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/IronLicensing.java b/src/main/java/com/ironservices/licensing/IronLicensing.java
new file mode 100644
index 0000000..911029d
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/IronLicensing.java
@@ -0,0 +1,209 @@
+package com.ironservices.licensing;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+/**
+ * Static facade for the IronLicensing SDK.
+ * Provides a simple global API for license operations.
+ */
+public final class IronLicensing {
+ private static volatile LicenseClient client;
+ private static final Object lock = new Object();
+
+ private IronLicensing() {}
+
+ /**
+ * Initializes the global IronLicensing client.
+ *
+ * @param publicKey The public key for your product
+ * @param productSlug The product slug
+ */
+ public static void init(String publicKey, String productSlug) {
+ init(new LicenseOptions(publicKey, productSlug));
+ }
+
+ /**
+ * Initializes the global IronLicensing client with options.
+ *
+ * @param options Configuration options
+ */
+ public static void init(LicenseOptions options) {
+ synchronized (lock) {
+ client = new LicenseClient(options);
+ }
+ }
+
+ /**
+ * Gets the global client instance.
+ *
+ * @return The client, or null if not initialized
+ */
+ public static LicenseClient getClient() {
+ return client;
+ }
+
+ private static LicenseClient requireClient() {
+ LicenseClient c = client;
+ if (c == null) {
+ throw new IllegalStateException("IronLicensing not initialized. Call IronLicensing.init() first.");
+ }
+ return c;
+ }
+
+ /**
+ * Validates a license key.
+ *
+ * @param licenseKey The license key to validate
+ * @return The validation result
+ */
+ public static LicenseResult validate(String licenseKey) {
+ return requireClient().validate(licenseKey);
+ }
+
+ /**
+ * Validates a license key asynchronously.
+ *
+ * @param licenseKey The license key to validate
+ * @return A CompletableFuture with the validation result
+ */
+ public static CompletableFuture validateAsync(String licenseKey) {
+ return requireClient().validateAsync(licenseKey);
+ }
+
+ /**
+ * Activates a license key on this machine.
+ *
+ * @param licenseKey The license key to activate
+ * @return The activation result
+ */
+ public static LicenseResult activate(String licenseKey) {
+ return requireClient().activate(licenseKey);
+ }
+
+ /**
+ * Activates a license key with a custom machine name.
+ *
+ * @param licenseKey The license key to activate
+ * @param machineName The machine name
+ * @return The activation result
+ */
+ public static LicenseResult activate(String licenseKey, String machineName) {
+ return requireClient().activate(licenseKey, machineName);
+ }
+
+ /**
+ * Deactivates the current license from this machine.
+ *
+ * @return true if deactivation was successful
+ */
+ public static boolean deactivate() {
+ return requireClient().deactivate();
+ }
+
+ /**
+ * Starts a trial for the given email.
+ *
+ * @param email The email address for the trial
+ * @return The trial result
+ */
+ public static LicenseResult startTrial(String email) {
+ return requireClient().startTrial(email);
+ }
+
+ /**
+ * Checks if a feature is available.
+ *
+ * @param featureKey The feature key to check
+ * @return true if the feature is enabled
+ */
+ public static boolean hasFeature(String featureKey) {
+ return requireClient().hasFeature(featureKey);
+ }
+
+ /**
+ * Requires a feature to be available.
+ *
+ * @param featureKey The feature key to require
+ * @throws LicenseRequiredException if the feature is not available
+ */
+ public static void requireFeature(String featureKey) throws LicenseRequiredException {
+ requireClient().requireFeature(featureKey);
+ }
+
+ /**
+ * Gets a feature from the current license.
+ *
+ * @param featureKey The feature key
+ * @return The feature, or null if not found
+ */
+ public static Feature getFeature(String featureKey) {
+ return requireClient().getFeature(featureKey);
+ }
+
+ /**
+ * Gets the current license.
+ *
+ * @return The current license, or null if not licensed
+ */
+ public static License getLicense() {
+ return requireClient().getLicense();
+ }
+
+ /**
+ * Gets the current license status.
+ *
+ * @return The license status
+ */
+ public static LicenseStatus getStatus() {
+ return requireClient().getStatus();
+ }
+
+ /**
+ * Checks if the application is licensed.
+ *
+ * @return true if licensed
+ */
+ public static boolean isLicensed() {
+ return requireClient().isLicensed();
+ }
+
+ /**
+ * Checks if running in trial mode.
+ *
+ * @return true if in trial mode
+ */
+ public static boolean isTrial() {
+ return requireClient().isTrial();
+ }
+
+ /**
+ * Gets available product tiers.
+ *
+ * @return List of product tiers
+ */
+ public static List getTiers() {
+ return requireClient().getTiers();
+ }
+
+ /**
+ * Starts a checkout session.
+ *
+ * @param tierId The tier ID to purchase
+ * @param email The customer's email
+ * @return The checkout result
+ */
+ public static CheckoutResult startPurchase(String tierId, String email) {
+ return requireClient().startPurchase(tierId, email);
+ }
+
+ /**
+ * Sets a listener for license changes.
+ *
+ * @param listener The listener to call when license changes
+ */
+ public static void setOnLicenseChanged(Consumer listener) {
+ requireClient().setOnLicenseChanged(listener);
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/License.java b/src/main/java/com/ironservices/licensing/License.java
new file mode 100644
index 0000000..9373505
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/License.java
@@ -0,0 +1,186 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents license information.
+ */
+public class License {
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("key")
+ private String key;
+
+ @SerializedName("status")
+ private LicenseStatus status;
+
+ @SerializedName("type")
+ private LicenseType type;
+
+ @SerializedName("email")
+ private String email;
+
+ @SerializedName("name")
+ private String name;
+
+ @SerializedName("company")
+ private String company;
+
+ @SerializedName("features")
+ private List features;
+
+ @SerializedName("maxActivations")
+ private int maxActivations;
+
+ @SerializedName("currentActivations")
+ private int currentActivations;
+
+ @SerializedName("expiresAt")
+ private String expiresAt;
+
+ @SerializedName("createdAt")
+ private String createdAt;
+
+ @SerializedName("lastValidatedAt")
+ private String lastValidatedAt;
+
+ @SerializedName("metadata")
+ private Map metadata;
+
+ public License() {}
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public LicenseStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(LicenseStatus status) {
+ this.status = status;
+ }
+
+ public LicenseType getType() {
+ return type;
+ }
+
+ public void setType(LicenseType type) {
+ this.type = type;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getCompany() {
+ return company;
+ }
+
+ public void setCompany(String company) {
+ this.company = company;
+ }
+
+ public List getFeatures() {
+ return features;
+ }
+
+ public void setFeatures(List features) {
+ this.features = features;
+ }
+
+ public int getMaxActivations() {
+ return maxActivations;
+ }
+
+ public void setMaxActivations(int maxActivations) {
+ this.maxActivations = maxActivations;
+ }
+
+ public int getCurrentActivations() {
+ return currentActivations;
+ }
+
+ public void setCurrentActivations(int currentActivations) {
+ this.currentActivations = currentActivations;
+ }
+
+ public String getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(String expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+
+ public String getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(String createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public String getLastValidatedAt() {
+ return lastValidatedAt;
+ }
+
+ public void setLastValidatedAt(String lastValidatedAt) {
+ this.lastValidatedAt = lastValidatedAt;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ public boolean hasFeature(String featureKey) {
+ if (features == null) return false;
+ return features.stream()
+ .anyMatch(f -> f.getKey().equals(featureKey) && f.isEnabled());
+ }
+
+ public Feature getFeature(String featureKey) {
+ if (features == null) return null;
+ return features.stream()
+ .filter(f -> f.getKey().equals(featureKey))
+ .findFirst()
+ .orElse(null);
+ }
+
+ @Override
+ public String toString() {
+ return "License{id='" + id + "', key='" + key + "', status=" + status + ", type=" + type + "}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseClient.java b/src/main/java/com/ironservices/licensing/LicenseClient.java
new file mode 100644
index 0000000..28fefbb
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseClient.java
@@ -0,0 +1,362 @@
+package com.ironservices.licensing;
+
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.function.Consumer;
+
+/**
+ * Main client for the IronLicensing SDK.
+ * Thread-safe and can be used concurrently.
+ */
+public class LicenseClient {
+ private final LicenseOptions options;
+ private final Transport transport;
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private License currentLicense;
+ private String licenseKey;
+ private Consumer onLicenseChanged;
+
+ /**
+ * Creates a new LicenseClient with the given options.
+ *
+ * @param options Configuration options
+ */
+ public LicenseClient(LicenseOptions options) {
+ this.options = options;
+ this.transport = new Transport(options);
+ if (options.isDebug()) {
+ log("Client initialized");
+ }
+ }
+
+ /**
+ * Creates a new LicenseClient with public key and product slug.
+ *
+ * @param publicKey The public key for your product
+ * @param productSlug The product slug
+ */
+ public LicenseClient(String publicKey, String productSlug) {
+ this(new LicenseOptions(publicKey, productSlug));
+ }
+
+ private void log(String message) {
+ if (options.isDebug()) {
+ System.out.println("[IronLicensing] " + message);
+ }
+ }
+
+ /**
+ * Sets a listener for license changes.
+ *
+ * @param listener The listener to call when license changes
+ */
+ public void setOnLicenseChanged(Consumer listener) {
+ this.onLicenseChanged = listener;
+ }
+
+ /**
+ * Validates a license key.
+ *
+ * @param licenseKey The license key to validate
+ * @return The validation result
+ */
+ public LicenseResult validate(String licenseKey) {
+ LicenseResult result = transport.validate(licenseKey);
+ if (result.isValid() && result.getLicense() != null) {
+ updateLicense(licenseKey, result.getLicense());
+ }
+ return result;
+ }
+
+ /**
+ * Validates a license key asynchronously.
+ *
+ * @param licenseKey The license key to validate
+ * @return A CompletableFuture with the validation result
+ */
+ public CompletableFuture validateAsync(String licenseKey) {
+ return CompletableFuture.supplyAsync(() -> validate(licenseKey));
+ }
+
+ /**
+ * Activates a license key on this machine.
+ *
+ * @param licenseKey The license key to activate
+ * @return The activation result
+ */
+ public LicenseResult activate(String licenseKey) {
+ return activate(licenseKey, null);
+ }
+
+ /**
+ * Activates a license key on this machine with a custom machine name.
+ *
+ * @param licenseKey The license key to activate
+ * @param machineName Optional machine name
+ * @return The activation result
+ */
+ public LicenseResult activate(String licenseKey, String machineName) {
+ LicenseResult result = transport.activate(licenseKey, machineName);
+ if (result.isValid() && result.getLicense() != null) {
+ updateLicense(licenseKey, result.getLicense());
+ }
+ return result;
+ }
+
+ /**
+ * Activates a license key asynchronously.
+ *
+ * @param licenseKey The license key to activate
+ * @param machineName Optional machine name
+ * @return A CompletableFuture with the activation result
+ */
+ public CompletableFuture activateAsync(String licenseKey, String machineName) {
+ return CompletableFuture.supplyAsync(() -> activate(licenseKey, machineName));
+ }
+
+ /**
+ * Deactivates the current license from this machine.
+ *
+ * @return true if deactivation was successful
+ */
+ public boolean deactivate() {
+ lock.readLock().lock();
+ String key;
+ try {
+ key = this.licenseKey;
+ } finally {
+ lock.readLock().unlock();
+ }
+
+ if (key == null || key.isEmpty()) {
+ return false;
+ }
+
+ if (transport.deactivate(key)) {
+ lock.writeLock().lock();
+ try {
+ this.currentLicense = null;
+ this.licenseKey = null;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ notifyLicenseChanged(null);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Deactivates the current license asynchronously.
+ *
+ * @return A CompletableFuture with the deactivation result
+ */
+ public CompletableFuture deactivateAsync() {
+ return CompletableFuture.supplyAsync(this::deactivate);
+ }
+
+ /**
+ * Starts a trial for the given email.
+ *
+ * @param email The email address for the trial
+ * @return The trial result
+ */
+ public LicenseResult startTrial(String email) {
+ LicenseResult result = transport.startTrial(email);
+ if (result.isValid() && result.getLicense() != null) {
+ updateLicense(result.getLicense().getKey(), result.getLicense());
+ }
+ return result;
+ }
+
+ /**
+ * Starts a trial asynchronously.
+ *
+ * @param email The email address for the trial
+ * @return A CompletableFuture with the trial result
+ */
+ public CompletableFuture startTrialAsync(String email) {
+ return CompletableFuture.supplyAsync(() -> startTrial(email));
+ }
+
+ /**
+ * Checks if a feature is available in the current license.
+ *
+ * @param featureKey The feature key to check
+ * @return true if the feature is enabled
+ */
+ public boolean hasFeature(String featureKey) {
+ lock.readLock().lock();
+ try {
+ return currentLicense != null && currentLicense.hasFeature(featureKey);
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Requires a feature to be available, throws if not.
+ *
+ * @param featureKey The feature key to require
+ * @throws LicenseRequiredException if the feature is not available
+ */
+ public void requireFeature(String featureKey) throws LicenseRequiredException {
+ if (!hasFeature(featureKey)) {
+ throw new LicenseRequiredException(featureKey);
+ }
+ }
+
+ /**
+ * Gets a feature from the current license.
+ *
+ * @param featureKey The feature key
+ * @return The feature, or null if not found
+ */
+ public Feature getFeature(String featureKey) {
+ lock.readLock().lock();
+ try {
+ if (currentLicense != null) {
+ return currentLicense.getFeature(featureKey);
+ }
+ return null;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Gets the current license.
+ *
+ * @return The current license, or null if not licensed
+ */
+ public License getLicense() {
+ lock.readLock().lock();
+ try {
+ return currentLicense;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Gets the current license status.
+ *
+ * @return The license status
+ */
+ public LicenseStatus getStatus() {
+ lock.readLock().lock();
+ try {
+ if (currentLicense != null) {
+ return currentLicense.getStatus();
+ }
+ return LicenseStatus.NOT_ACTIVATED;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Checks if the application is licensed.
+ *
+ * @return true if licensed (valid or trial)
+ */
+ public boolean isLicensed() {
+ lock.readLock().lock();
+ try {
+ if (currentLicense == null) return false;
+ LicenseStatus status = currentLicense.getStatus();
+ return status == LicenseStatus.VALID || status == LicenseStatus.TRIAL;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Checks if running in trial mode.
+ *
+ * @return true if in trial mode
+ */
+ public boolean isTrial() {
+ lock.readLock().lock();
+ try {
+ if (currentLicense == null) return false;
+ return currentLicense.getStatus() == LicenseStatus.TRIAL ||
+ currentLicense.getType() == LicenseType.TRIAL;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Gets available product tiers for purchase.
+ *
+ * @return List of product tiers
+ */
+ public List getTiers() {
+ return transport.getTiers();
+ }
+
+ /**
+ * Gets available product tiers asynchronously.
+ *
+ * @return A CompletableFuture with the list of tiers
+ */
+ public CompletableFuture> getTiersAsync() {
+ return CompletableFuture.supplyAsync(this::getTiers);
+ }
+
+ /**
+ * Starts a checkout session for the specified tier.
+ *
+ * @param tierId The tier ID to purchase
+ * @param email The customer's email
+ * @return The checkout result with URL
+ */
+ public CheckoutResult startPurchase(String tierId, String email) {
+ return transport.startCheckout(tierId, email);
+ }
+
+ /**
+ * Starts a checkout session asynchronously.
+ *
+ * @param tierId The tier ID to purchase
+ * @param email The customer's email
+ * @return A CompletableFuture with the checkout result
+ */
+ public CompletableFuture startPurchaseAsync(String tierId, String email) {
+ return CompletableFuture.supplyAsync(() -> startPurchase(tierId, email));
+ }
+
+ /**
+ * Gets the machine ID used for activations.
+ *
+ * @return The machine ID
+ */
+ public String getMachineId() {
+ return transport.getMachineId();
+ }
+
+ private void updateLicense(String key, License license) {
+ lock.writeLock().lock();
+ try {
+ this.licenseKey = key;
+ this.currentLicense = license;
+ } finally {
+ lock.writeLock().unlock();
+ }
+ notifyLicenseChanged(license);
+ }
+
+ private void notifyLicenseChanged(License license) {
+ if (onLicenseChanged != null) {
+ try {
+ onLicenseChanged.accept(license);
+ } catch (Exception e) {
+ log("License change listener error: " + e.getMessage());
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseOptions.java b/src/main/java/com/ironservices/licensing/LicenseOptions.java
new file mode 100644
index 0000000..38950b6
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseOptions.java
@@ -0,0 +1,153 @@
+package com.ironservices.licensing;
+
+import java.time.Duration;
+
+/**
+ * Configuration options for the LicenseClient.
+ */
+public class LicenseOptions {
+ private static final String DEFAULT_API_BASE_URL = "https://api.ironlicensing.com";
+ private static final Duration DEFAULT_HTTP_TIMEOUT = Duration.ofSeconds(30);
+ private static final int DEFAULT_CACHE_VALIDATION_MINUTES = 60;
+ private static final int DEFAULT_OFFLINE_GRACE_DAYS = 7;
+
+ private String publicKey;
+ private String productSlug;
+ private String apiBaseUrl = DEFAULT_API_BASE_URL;
+ private boolean debug = false;
+ private boolean enableOfflineCache = true;
+ private int cacheValidationMinutes = DEFAULT_CACHE_VALIDATION_MINUTES;
+ private int offlineGraceDays = DEFAULT_OFFLINE_GRACE_DAYS;
+ private Duration httpTimeout = DEFAULT_HTTP_TIMEOUT;
+
+ public LicenseOptions() {}
+
+ public LicenseOptions(String publicKey, String productSlug) {
+ this.publicKey = publicKey;
+ this.productSlug = productSlug;
+ }
+
+ public static Builder builder(String publicKey, String productSlug) {
+ return new Builder(publicKey, productSlug);
+ }
+
+ public String getPublicKey() {
+ return publicKey;
+ }
+
+ public LicenseOptions setPublicKey(String publicKey) {
+ this.publicKey = publicKey;
+ return this;
+ }
+
+ public String getProductSlug() {
+ return productSlug;
+ }
+
+ public LicenseOptions setProductSlug(String productSlug) {
+ this.productSlug = productSlug;
+ return this;
+ }
+
+ public String getApiBaseUrl() {
+ return apiBaseUrl;
+ }
+
+ public LicenseOptions setApiBaseUrl(String apiBaseUrl) {
+ this.apiBaseUrl = apiBaseUrl;
+ return this;
+ }
+
+ public boolean isDebug() {
+ return debug;
+ }
+
+ public LicenseOptions setDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ public boolean isEnableOfflineCache() {
+ return enableOfflineCache;
+ }
+
+ public LicenseOptions setEnableOfflineCache(boolean enableOfflineCache) {
+ this.enableOfflineCache = enableOfflineCache;
+ return this;
+ }
+
+ public int getCacheValidationMinutes() {
+ return cacheValidationMinutes;
+ }
+
+ public LicenseOptions setCacheValidationMinutes(int cacheValidationMinutes) {
+ this.cacheValidationMinutes = cacheValidationMinutes;
+ return this;
+ }
+
+ public int getOfflineGraceDays() {
+ return offlineGraceDays;
+ }
+
+ public LicenseOptions setOfflineGraceDays(int offlineGraceDays) {
+ this.offlineGraceDays = offlineGraceDays;
+ return this;
+ }
+
+ public Duration getHttpTimeout() {
+ return httpTimeout;
+ }
+
+ public LicenseOptions setHttpTimeout(Duration httpTimeout) {
+ this.httpTimeout = httpTimeout;
+ return this;
+ }
+
+ public static class Builder {
+ private final LicenseOptions options;
+
+ public Builder(String publicKey, String productSlug) {
+ this.options = new LicenseOptions(publicKey, productSlug);
+ }
+
+ public Builder apiBaseUrl(String apiBaseUrl) {
+ options.setApiBaseUrl(apiBaseUrl);
+ return this;
+ }
+
+ public Builder debug(boolean debug) {
+ options.setDebug(debug);
+ return this;
+ }
+
+ public Builder enableOfflineCache(boolean enable) {
+ options.setEnableOfflineCache(enable);
+ return this;
+ }
+
+ public Builder cacheValidationMinutes(int minutes) {
+ options.setCacheValidationMinutes(minutes);
+ return this;
+ }
+
+ public Builder offlineGraceDays(int days) {
+ options.setOfflineGraceDays(days);
+ return this;
+ }
+
+ public Builder httpTimeout(Duration timeout) {
+ options.setHttpTimeout(timeout);
+ return this;
+ }
+
+ public LicenseOptions build() {
+ if (options.getPublicKey() == null || options.getPublicKey().isEmpty()) {
+ throw new IllegalArgumentException("Public key is required");
+ }
+ if (options.getProductSlug() == null || options.getProductSlug().isEmpty()) {
+ throw new IllegalArgumentException("Product slug is required");
+ }
+ return options;
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseRequiredException.java b/src/main/java/com/ironservices/licensing/LicenseRequiredException.java
new file mode 100644
index 0000000..aa0d0f0
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseRequiredException.java
@@ -0,0 +1,17 @@
+package com.ironservices.licensing;
+
+/**
+ * Exception thrown when a required feature is not available in the current license.
+ */
+public class LicenseRequiredException extends RuntimeException {
+ private final String feature;
+
+ public LicenseRequiredException(String feature) {
+ super("Feature '" + feature + "' requires a valid license");
+ this.feature = feature;
+ }
+
+ public String getFeature() {
+ return feature;
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseResult.java b/src/main/java/com/ironservices/licensing/LicenseResult.java
new file mode 100644
index 0000000..3e94e3b
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseResult.java
@@ -0,0 +1,87 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+
+/**
+ * Represents the result of a license validation or activation.
+ */
+public class LicenseResult {
+ @SerializedName("valid")
+ private boolean valid;
+
+ @SerializedName("license")
+ private License license;
+
+ @SerializedName("activations")
+ private List activations;
+
+ @SerializedName("error")
+ private String error;
+
+ @SerializedName("cached")
+ private boolean cached;
+
+ public LicenseResult() {}
+
+ public LicenseResult(boolean valid, String error) {
+ this.valid = valid;
+ this.error = error;
+ }
+
+ public static LicenseResult success(License license) {
+ LicenseResult result = new LicenseResult();
+ result.valid = true;
+ result.license = license;
+ return result;
+ }
+
+ public static LicenseResult failure(String error) {
+ return new LicenseResult(false, error);
+ }
+
+ public boolean isValid() {
+ return valid;
+ }
+
+ public void setValid(boolean valid) {
+ this.valid = valid;
+ }
+
+ public License getLicense() {
+ return license;
+ }
+
+ public void setLicense(License license) {
+ this.license = license;
+ }
+
+ public List getActivations() {
+ return activations;
+ }
+
+ public void setActivations(List activations) {
+ this.activations = activations;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
+
+ public boolean isCached() {
+ return cached;
+ }
+
+ public void setCached(boolean cached) {
+ this.cached = cached;
+ }
+
+ @Override
+ public String toString() {
+ return "LicenseResult{valid=" + valid + ", error='" + error + "'}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseStatus.java b/src/main/java/com/ironservices/licensing/LicenseStatus.java
new file mode 100644
index 0000000..3e8584b
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseStatus.java
@@ -0,0 +1,54 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents the status of a license.
+ */
+public enum LicenseStatus {
+ @SerializedName("valid")
+ VALID("valid"),
+
+ @SerializedName("expired")
+ EXPIRED("expired"),
+
+ @SerializedName("suspended")
+ SUSPENDED("suspended"),
+
+ @SerializedName("revoked")
+ REVOKED("revoked"),
+
+ @SerializedName("invalid")
+ INVALID("invalid"),
+
+ @SerializedName("trial")
+ TRIAL("trial"),
+
+ @SerializedName("trial_expired")
+ TRIAL_EXPIRED("trial_expired"),
+
+ @SerializedName("not_activated")
+ NOT_ACTIVATED("not_activated"),
+
+ @SerializedName("unknown")
+ UNKNOWN("unknown");
+
+ private final String value;
+
+ LicenseStatus(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static LicenseStatus fromValue(String value) {
+ for (LicenseStatus status : values()) {
+ if (status.value.equals(value)) {
+ return status;
+ }
+ }
+ return UNKNOWN;
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/LicenseType.java b/src/main/java/com/ironservices/licensing/LicenseType.java
new file mode 100644
index 0000000..5d0a3a4
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/LicenseType.java
@@ -0,0 +1,36 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Represents the type of a license.
+ */
+public enum LicenseType {
+ @SerializedName("perpetual")
+ PERPETUAL("perpetual"),
+
+ @SerializedName("subscription")
+ SUBSCRIPTION("subscription"),
+
+ @SerializedName("trial")
+ TRIAL("trial");
+
+ private final String value;
+
+ LicenseType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public static LicenseType fromValue(String value) {
+ for (LicenseType type : values()) {
+ if (type.value.equals(value)) {
+ return type;
+ }
+ }
+ return PERPETUAL;
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/ProductTier.java b/src/main/java/com/ironservices/licensing/ProductTier.java
new file mode 100644
index 0000000..3ba9520
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/ProductTier.java
@@ -0,0 +1,104 @@
+package com.ironservices.licensing;
+
+import com.google.gson.annotations.SerializedName;
+import java.util.List;
+
+/**
+ * Represents a product tier available for purchase.
+ */
+public class ProductTier {
+ @SerializedName("id")
+ private String id;
+
+ @SerializedName("slug")
+ private String slug;
+
+ @SerializedName("name")
+ private String name;
+
+ @SerializedName("description")
+ private String description;
+
+ @SerializedName("price")
+ private double price;
+
+ @SerializedName("currency")
+ private String currency;
+
+ @SerializedName("billingPeriod")
+ private String billingPeriod;
+
+ @SerializedName("features")
+ private List features;
+
+ public ProductTier() {}
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getSlug() {
+ return slug;
+ }
+
+ public void setSlug(String slug) {
+ this.slug = slug;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public double getPrice() {
+ return price;
+ }
+
+ public void setPrice(double price) {
+ this.price = price;
+ }
+
+ public String getCurrency() {
+ return currency;
+ }
+
+ public void setCurrency(String currency) {
+ this.currency = currency;
+ }
+
+ public String getBillingPeriod() {
+ return billingPeriod;
+ }
+
+ public void setBillingPeriod(String billingPeriod) {
+ this.billingPeriod = billingPeriod;
+ }
+
+ public List getFeatures() {
+ return features;
+ }
+
+ public void setFeatures(List features) {
+ this.features = features;
+ }
+
+ @Override
+ public String toString() {
+ return "ProductTier{id='" + id + "', name='" + name + "', price=" + price + " " + currency + "}";
+ }
+}
diff --git a/src/main/java/com/ironservices/licensing/Transport.java b/src/main/java/com/ironservices/licensing/Transport.java
new file mode 100644
index 0000000..e0d2584
--- /dev/null
+++ b/src/main/java/com/ironservices/licensing/Transport.java
@@ -0,0 +1,220 @@
+package com.ironservices.licensing;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import okhttp3.*;
+
+import java.io.*;
+import java.lang.reflect.Type;
+import java.net.InetAddress;
+import java.nio.file.*;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HTTP transport layer for IronLicensing API.
+ */
+class Transport {
+ private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
+
+ private final String baseUrl;
+ private final String publicKey;
+ private final String productSlug;
+ private final boolean debug;
+ private final OkHttpClient httpClient;
+ private final Gson gson;
+ private final String machineId;
+
+ Transport(LicenseOptions options) {
+ this.baseUrl = options.getApiBaseUrl();
+ this.publicKey = options.getPublicKey();
+ this.productSlug = options.getProductSlug();
+ this.debug = options.isDebug();
+ this.httpClient = new OkHttpClient.Builder()
+ .connectTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS)
+ .readTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS)
+ .writeTimeout(options.getHttpTimeout().toMillis(), TimeUnit.MILLISECONDS)
+ .build();
+ this.gson = new GsonBuilder().create();
+ this.machineId = getOrCreateMachineId();
+ }
+
+ private void log(String message) {
+ if (debug) {
+ System.out.println("[IronLicensing] " + message);
+ }
+ }
+
+ private String getOrCreateMachineId() {
+ try {
+ Path idPath = Paths.get(System.getProperty("user.home"), ".ironlicensing", "machine_id");
+ if (Files.exists(idPath)) {
+ return new String(Files.readAllBytes(idPath)).trim();
+ }
+ String id = UUID.randomUUID().toString();
+ Files.createDirectories(idPath.getParent());
+ Files.write(idPath, id.getBytes());
+ return id;
+ } catch (IOException e) {
+ return UUID.randomUUID().toString();
+ }
+ }
+
+ String getMachineId() {
+ return machineId;
+ }
+
+ private String getHostname() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (Exception e) {
+ return "unknown";
+ }
+ }
+
+ private String getPlatform() {
+ String os = System.getProperty("os.name", "").toLowerCase();
+ if (os.contains("win")) return "windows";
+ if (os.contains("mac")) return "macos";
+ if (os.contains("nix") || os.contains("nux")) return "linux";
+ return os;
+ }
+
+ private Request.Builder createRequest(String path) {
+ return new Request.Builder()
+ .url(baseUrl + path)
+ .addHeader("Content-Type", "application/json")
+ .addHeader("X-Public-Key", publicKey)
+ .addHeader("X-Product-Slug", productSlug);
+ }
+
+ LicenseResult validate(String licenseKey) {
+ log("Validating: " + licenseKey.substring(0, Math.min(10, licenseKey.length())) + "...");
+
+ Map body = new HashMap<>();
+ body.put("licenseKey", licenseKey);
+ body.put("machineId", machineId);
+
+ Request request = createRequest("/api/v1/validate")
+ .post(RequestBody.create(gson.toJson(body), JSON))
+ .build();
+
+ return executeRequest(request);
+ }
+
+ LicenseResult activate(String licenseKey, String machineName) {
+ log("Activating: " + licenseKey.substring(0, Math.min(10, licenseKey.length())) + "...");
+
+ if (machineName == null || machineName.isEmpty()) {
+ machineName = getHostname();
+ }
+
+ Map body = new HashMap<>();
+ body.put("licenseKey", licenseKey);
+ body.put("machineId", machineId);
+ body.put("machineName", machineName);
+ body.put("platform", getPlatform());
+
+ Request request = createRequest("/api/v1/activate")
+ .post(RequestBody.create(gson.toJson(body), JSON))
+ .build();
+
+ return executeRequest(request);
+ }
+
+ boolean deactivate(String licenseKey) {
+ log("Deactivating license");
+
+ Map body = new HashMap<>();
+ body.put("licenseKey", licenseKey);
+ body.put("machineId", machineId);
+
+ Request request = createRequest("/api/v1/deactivate")
+ .post(RequestBody.create(gson.toJson(body), JSON))
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ return response.isSuccessful();
+ } catch (IOException e) {
+ log("Deactivation failed: " + e.getMessage());
+ return false;
+ }
+ }
+
+ LicenseResult startTrial(String email) {
+ log("Starting trial for: " + email);
+
+ Map body = new HashMap<>();
+ body.put("email", email);
+ body.put("machineId", machineId);
+
+ Request request = createRequest("/api/v1/trial")
+ .post(RequestBody.create(gson.toJson(body), JSON))
+ .build();
+
+ return executeRequest(request);
+ }
+
+ List getTiers() {
+ log("Fetching product tiers");
+
+ Request request = createRequest("/api/v1/tiers")
+ .get()
+ .build();
+
+ try (Response response = httpClient.newCall(request).execute()) {
+ if (response.isSuccessful() && response.body() != null) {
+ String json = response.body().string();
+ Type type = new TypeToken