diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..a56e0b3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,49 @@
+# Compiled class files
+*.class
+
+# Log files
+*.log
+
+# Package files
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# 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
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Local storage
+.ironnotify/
diff --git a/README.md b/README.md
index 68d1861..ea0cbb9 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,358 @@
-# ironnotify-java
-IronNotify SDK for Java - Event notifications and alerts
+# IronNotify SDK for Java
+
+Event notifications and alerts SDK for Java applications. Send notifications, receive real-time updates, and manage notification state.
+
+[](https://search.maven.org/artifact/com.ironservices/notify)
+[](https://opensource.org/licenses/MIT)
+
+## Installation
+
+### Maven
+
+```xml
+
+ com.ironservices
+ notify
+ 1.0.0
+
+```
+
+### Gradle
+
+```groovy
+implementation 'com.ironservices:notify:1.0.0'
+```
+
+## Quick Start
+
+### Send a Simple Notification
+
+```java
+import com.ironservices.notify.*;
+import java.util.Map;
+
+public class Main {
+ public static void main(String[] args) {
+ // Initialize
+ IronNotify.init("ak_live_xxxxx");
+
+ // Send a simple notification
+ SendResult result = IronNotify.notify(
+ "order.created",
+ "New Order Received",
+ "Order #1234 has been placed",
+ SeverityLevel.SUCCESS,
+ Map.of("order_id", "1234", "amount", 99.99)
+ );
+
+ if (result.isSuccess()) {
+ System.out.println("Notification sent: " + result.getNotificationId());
+ }
+
+ // Shutdown when done
+ IronNotify.shutdown();
+ }
+}
+```
+
+### Fluent Event Builder
+
+```java
+import com.ironservices.notify.*;
+import java.time.Duration;
+
+public class Main {
+ public static void main(String[] args) {
+ IronNotify.init("ak_live_xxxxx");
+
+ // Build complex notifications with the fluent API
+ SendResult result = IronNotify.event("payment.failed")
+ .withTitle("Payment Failed")
+ .withMessage("Payment could not be processed")
+ .withSeverity(SeverityLevel.ERROR)
+ .withMetadata("order_id", "1234")
+ .withMetadata("reason", "Card declined")
+ .withAction("Retry Payment", "/orders/1234/retry", null, "primary")
+ .withAction(EventBuilder.action("Contact Support")
+ .handler("open_support")
+ .build())
+ .forUser("user-123")
+ .withDeduplicationKey("payment-failed-1234")
+ .expiresIn(Duration.ofHours(24))
+ .send();
+
+ if (result.isQueued()) {
+ System.out.println("Notification queued for later");
+ }
+
+ IronNotify.shutdown();
+ }
+}
+```
+
+### Async Support
+
+```java
+import com.ironservices.notify.*;
+import java.util.concurrent.CompletableFuture;
+
+public class Main {
+ public static void main(String[] args) {
+ IronNotify.init("ak_live_xxxxx");
+
+ // Async notification
+ CompletableFuture future = IronNotify.notifyAsync(
+ "user.signup",
+ "New User Registered"
+ );
+
+ future.thenAccept(result -> {
+ System.out.println("Notification sent: " + result.isSuccess());
+ });
+
+ // Async event builder
+ IronNotify.event("order.shipped")
+ .withTitle("Order Shipped")
+ .forUser("user-123")
+ .sendAsync()
+ .thenAccept(result -> System.out.println("Done!"));
+
+ // Wait for async operations
+ future.join();
+ IronNotify.shutdown();
+ }
+}
+```
+
+### Using the Client Directly
+
+```java
+import com.ironservices.notify.*;
+
+public class Main {
+ public static void main(String[] args) {
+ NotifyClient client = new NotifyClient(
+ NotifyOptions.builder()
+ .apiKey("ak_live_xxxxx")
+ .debug(true)
+ .build()
+ );
+
+ // Send notification
+ SendResult result = client.notify("event.type", "Title");
+
+ // Use event builder
+ result = client.event("event.type")
+ .withTitle("Title")
+ .send();
+
+ // Clean up
+ client.close();
+ }
+}
+```
+
+## Configuration
+
+```java
+import com.ironservices.notify.*;
+import java.time.Duration;
+
+NotifyClient client = new NotifyClient(
+ NotifyOptions.builder()
+ .apiKey("ak_live_xxxxx")
+ .apiBaseUrl("https://api.ironnotify.com")
+ .webSocketUrl("wss://ws.ironnotify.com")
+ .debug(false)
+ .enableOfflineQueue(true)
+ .maxOfflineQueueSize(100)
+ .autoReconnect(true)
+ .maxReconnectAttempts(5)
+ .reconnectDelay(Duration.ofSeconds(1))
+ .httpTimeout(Duration.ofSeconds(30))
+ .build()
+);
+```
+
+### Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `apiKey` | String | required | Your API key (ak_live_xxx or ak_test_xxx) |
+| `apiBaseUrl` | String | https://api.ironnotify.com | API base URL |
+| `webSocketUrl` | String | wss://ws.ironnotify.com | WebSocket URL |
+| `debug` | boolean | false | Enable debug logging |
+| `enableOfflineQueue` | boolean | true | Queue notifications when offline |
+| `maxOfflineQueueSize` | int | 100 | Max offline queue size |
+| `autoReconnect` | boolean | true | Auto-reconnect WebSocket |
+| `maxReconnectAttempts` | int | 5 | Max reconnection attempts |
+| `reconnectDelay` | Duration | 1s | Base reconnection delay |
+| `httpTimeout` | Duration | 30s | HTTP request timeout |
+
+## Severity Levels
+
+```java
+SeverityLevel.INFO // "info"
+SeverityLevel.SUCCESS // "success"
+SeverityLevel.WARNING // "warning"
+SeverityLevel.ERROR // "error"
+SeverityLevel.CRITICAL // "critical"
+```
+
+## Actions
+
+```java
+// Simple action with URL
+IronNotify.event("order.shipped")
+ .withTitle("Order Shipped")
+ .withAction("Track Package", "https://tracking.example.com/123")
+ .send();
+
+// Action with handler
+IronNotify.event("order.shipped")
+ .withTitle("Order Shipped")
+ .withAction("View Order", null, "view_order", "default")
+ .send();
+
+// Using action builder
+IronNotify.event("order.shipped")
+ .withTitle("Order Shipped")
+ .withAction(EventBuilder.action("Track Package")
+ .url("https://tracking.example.com/123")
+ .style("primary")
+ .build())
+ .send();
+```
+
+## Deduplication
+
+Prevent duplicate notifications:
+
+```java
+IronNotify.event("reminder")
+ .withTitle("Daily Reminder")
+ .withDeduplicationKey("daily-reminder-2024-01-15")
+ .send();
+```
+
+## Grouping
+
+Group related notifications:
+
+```java
+IronNotify.event("comment.new")
+ .withTitle("New Comment")
+ .withGroupKey("post-123-comments")
+ .send();
+```
+
+## Expiration
+
+```java
+import java.time.Duration;
+import java.time.Instant;
+
+// Expires in 1 hour
+IronNotify.event("flash_sale")
+ .withTitle("Flash Sale!")
+ .expiresIn(Duration.ofHours(1))
+ .send();
+
+// Expires at specific time
+IronNotify.event("event_reminder")
+ .withTitle("Event Tomorrow")
+ .expiresAt(Instant.now().plus(Duration.ofDays(1)))
+ .send();
+```
+
+## Managing Notifications
+
+### Get Notifications
+
+```java
+import java.util.List;
+import java.io.IOException;
+
+// Get all notifications
+List notifications = client.getNotifications();
+
+// With options
+List unread = client.getNotifications(10, 0, true);
+
+// Async
+client.getNotificationsAsync(10, 0, false)
+ .thenAccept(list -> System.out.println("Got " + list.size() + " notifications"));
+```
+
+### Mark as Read
+
+```java
+// Mark single notification
+client.markAsRead("notification-id");
+
+// Mark all as read
+client.markAllAsRead();
+```
+
+### Get Unread Count
+
+```java
+int count = client.getUnreadCount();
+System.out.printf("You have %d unread notifications%n", count);
+```
+
+## Real-Time Notifications
+
+```java
+NotifyClient client = new NotifyClient(new NotifyOptions("ak_live_xxxxx"));
+
+client.onNotification(notification -> {
+ System.out.println("New notification: " + notification.getTitle());
+});
+
+client.onUnreadCountChange(count -> {
+ System.out.println("Unread count: " + count);
+});
+
+client.onConnectionStateChange(state -> {
+ System.out.println("Connection state: " + state);
+});
+
+client.connect();
+client.subscribeToUser("user-123");
+client.subscribeToApp();
+```
+
+## Offline Support
+
+Notifications are automatically queued when offline:
+
+```java
+// This will be queued if offline
+IronNotify.notify("event", "Title");
+
+// Manually flush the queue
+IronNotify.flush();
+
+// Or async
+IronNotify.flushAsync().join();
+```
+
+## Requirements
+
+- Java 11+
+- OkHttp 4.x
+- Gson 2.x
+
+## Thread Safety
+
+The client is thread-safe and can be used from multiple threads concurrently.
+
+## Links
+
+- [Documentation](https://www.ironnotify.com/docs)
+- [Dashboard](https://www.ironnotify.com)
+
+## License
+
+MIT License - see [LICENSE](LICENSE) for details.
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2cac911
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,101 @@
+
+
+ 4.0.0
+
+ com.ironservices
+ notify
+ 1.0.0
+ jar
+
+ IronNotify Java SDK
+ Event notifications and alerts SDK for Java applications
+ https://github.com/IronServices/ironnotify-java
+
+
+
+ MIT License
+ https://opensource.org/licenses/MIT
+
+
+
+
+
+ IronServices
+ support@ironservices.com
+ IronServices
+ https://www.ironservices.com
+
+
+
+
+ scm:git:git://github.com/IronServices/ironnotify-java.git
+ scm:git:ssh://github.com:IronServices/ironnotify-java.git
+ https://github.com/IronServices/ironnotify-java/tree/main
+
+
+
+ 11
+ 11
+ UTF-8
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.5.4
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 11
+ 11
+
+
+
+ org.apache.maven.plugins
+ maven-source-plugin
+ 3.3.0
+
+
+ attach-sources
+
+ jar
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-javadoc-plugin
+ 3.6.0
+
+
+ attach-javadocs
+
+ jar
+
+
+
+
+
+
+
diff --git a/src/main/java/com/ironservices/notify/ConnectionState.java b/src/main/java/com/ironservices/notify/ConnectionState.java
new file mode 100644
index 0000000..f99c065
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/ConnectionState.java
@@ -0,0 +1,26 @@
+package com.ironservices.notify;
+
+/**
+ * WebSocket connection state.
+ */
+public enum ConnectionState {
+ DISCONNECTED("disconnected"),
+ CONNECTING("connecting"),
+ CONNECTED("connected"),
+ RECONNECTING("reconnecting");
+
+ private final String value;
+
+ ConnectionState(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/EventBuilder.java b/src/main/java/com/ironservices/notify/EventBuilder.java
new file mode 100644
index 0000000..b20150b
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/EventBuilder.java
@@ -0,0 +1,215 @@
+package com.ironservices.notify;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Builder for creating notifications with a fluent API.
+ */
+public class EventBuilder {
+ private final NotifyClient client;
+ private final String eventType;
+ private String title;
+ private String message;
+ private SeverityLevel severity = SeverityLevel.INFO;
+ private Map metadata = new HashMap<>();
+ private List actions = new ArrayList<>();
+ private String userId;
+ private String groupKey;
+ private String deduplicationKey;
+ private Instant expiresAt;
+
+ EventBuilder(NotifyClient client, String eventType) {
+ this.client = client;
+ this.eventType = eventType;
+ }
+
+ /**
+ * Sets the notification title.
+ */
+ public EventBuilder withTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Sets the notification message.
+ */
+ public EventBuilder withMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ /**
+ * Sets the severity level.
+ */
+ public EventBuilder withSeverity(SeverityLevel severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ /**
+ * Adds a metadata entry.
+ */
+ public EventBuilder withMetadata(String key, Object value) {
+ this.metadata.put(key, value);
+ return this;
+ }
+
+ /**
+ * Adds multiple metadata entries.
+ */
+ public EventBuilder withMetadata(Map metadata) {
+ this.metadata.putAll(metadata);
+ return this;
+ }
+
+ /**
+ * Adds an action button with a URL.
+ */
+ public EventBuilder withAction(String label, String url) {
+ this.actions.add(new NotificationAction(label, url, null, "default"));
+ return this;
+ }
+
+ /**
+ * Adds an action button with options.
+ */
+ public EventBuilder withAction(String label, String url, String action, String style) {
+ this.actions.add(new NotificationAction(label, url, action, style));
+ return this;
+ }
+
+ /**
+ * Adds an action using the ActionBuilder.
+ */
+ public EventBuilder withAction(NotificationAction action) {
+ this.actions.add(action);
+ return this;
+ }
+
+ /**
+ * Sets the target user ID.
+ */
+ public EventBuilder forUser(String userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ /**
+ * Sets the group key for grouping related notifications.
+ */
+ public EventBuilder withGroupKey(String groupKey) {
+ this.groupKey = groupKey;
+ return this;
+ }
+
+ /**
+ * Sets the deduplication key.
+ */
+ public EventBuilder withDeduplicationKey(String key) {
+ this.deduplicationKey = key;
+ return this;
+ }
+
+ /**
+ * Sets the expiration time from now.
+ */
+ public EventBuilder expiresIn(Duration duration) {
+ this.expiresAt = Instant.now().plus(duration);
+ return this;
+ }
+
+ /**
+ * Sets the expiration time.
+ */
+ public EventBuilder expiresAt(Instant instant) {
+ this.expiresAt = instant;
+ return this;
+ }
+
+ /**
+ * Builds the notification payload.
+ */
+ public NotificationPayload build() {
+ if (title == null || title.isEmpty()) {
+ throw new IllegalStateException("Notification title is required");
+ }
+
+ NotificationPayload payload = new NotificationPayload(eventType, title);
+ payload.setMessage(message);
+ payload.setSeverity(severity);
+ payload.setUserId(userId);
+ payload.setGroupKey(groupKey);
+ payload.setDeduplicationKey(deduplicationKey);
+ payload.setExpiresAt(expiresAt);
+
+ if (!metadata.isEmpty()) {
+ payload.setMetadata(metadata);
+ }
+
+ if (!actions.isEmpty()) {
+ payload.setActions(actions);
+ }
+
+ return payload;
+ }
+
+ /**
+ * Sends the notification synchronously.
+ */
+ public SendResult send() {
+ NotificationPayload payload = build();
+ return client.sendPayload(payload);
+ }
+
+ /**
+ * Sends the notification asynchronously.
+ */
+ public CompletableFuture sendAsync() {
+ NotificationPayload payload = build();
+ return client.sendPayloadAsync(payload);
+ }
+
+ /**
+ * Creates an action builder for complex actions.
+ */
+ public static ActionBuilder action(String label) {
+ return new ActionBuilder(label);
+ }
+
+ /**
+ * Builder for notification actions.
+ */
+ public static class ActionBuilder {
+ private final NotificationAction action;
+
+ ActionBuilder(String label) {
+ this.action = new NotificationAction(label);
+ }
+
+ public ActionBuilder url(String url) {
+ action.setUrl(url);
+ return this;
+ }
+
+ public ActionBuilder handler(String actionHandler) {
+ action.setAction(actionHandler);
+ return this;
+ }
+
+ public ActionBuilder style(String style) {
+ action.setStyle(style);
+ return this;
+ }
+
+ public NotificationAction build() {
+ return action;
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/IronNotify.java b/src/main/java/com/ironservices/notify/IronNotify.java
new file mode 100644
index 0000000..44a758c
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/IronNotify.java
@@ -0,0 +1,173 @@
+package com.ironservices.notify;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Static facade for the IronNotify SDK.
+ * Provides convenient static methods that delegate to the global client.
+ */
+public final class IronNotify {
+
+ private IronNotify() {
+ // Utility class
+ }
+
+ /**
+ * Initializes the SDK with an API key.
+ */
+ public static void init(String apiKey) {
+ NotifyClient.init(apiKey);
+ }
+
+ /**
+ * Initializes the SDK with options.
+ */
+ public static void init(NotifyOptions options) {
+ NotifyClient.init(options);
+ }
+
+ /**
+ * Sends a notification.
+ */
+ public static SendResult notify(String eventType, String title) {
+ return NotifyClient.get().notify(eventType, title);
+ }
+
+ /**
+ * Sends a notification with options.
+ */
+ public static SendResult notify(String eventType, String title, String message,
+ SeverityLevel severity, Map metadata) {
+ return NotifyClient.get().notify(eventType, title, message, severity, metadata);
+ }
+
+ /**
+ * Sends a notification asynchronously.
+ */
+ public static CompletableFuture notifyAsync(String eventType, String title) {
+ return NotifyClient.get().notifyAsync(eventType, title);
+ }
+
+ /**
+ * Sends a notification asynchronously with options.
+ */
+ public static CompletableFuture notifyAsync(String eventType, String title, String message,
+ SeverityLevel severity, Map metadata) {
+ return NotifyClient.get().notifyAsync(eventType, title, message, severity, metadata);
+ }
+
+ /**
+ * Creates an event builder.
+ */
+ public static EventBuilder event(String eventType) {
+ return NotifyClient.get().event(eventType);
+ }
+
+ /**
+ * Gets notifications.
+ */
+ public static List getNotifications() throws IOException {
+ return NotifyClient.get().getNotifications();
+ }
+
+ /**
+ * Gets notifications with options.
+ */
+ public static List getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
+ return NotifyClient.get().getNotifications(limit, offset, unreadOnly);
+ }
+
+ /**
+ * Gets notifications asynchronously.
+ */
+ public static CompletableFuture> getNotificationsAsync() {
+ return NotifyClient.get().getNotificationsAsync();
+ }
+
+ /**
+ * Gets notifications asynchronously with options.
+ */
+ public static CompletableFuture> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
+ return NotifyClient.get().getNotificationsAsync(limit, offset, unreadOnly);
+ }
+
+ /**
+ * Gets the unread count.
+ */
+ public static int getUnreadCount() throws IOException {
+ return NotifyClient.get().getUnreadCount();
+ }
+
+ /**
+ * Gets the unread count asynchronously.
+ */
+ public static CompletableFuture getUnreadCountAsync() {
+ return NotifyClient.get().getUnreadCountAsync();
+ }
+
+ /**
+ * Marks a notification as read.
+ */
+ public static boolean markAsRead(String notificationId) throws IOException {
+ return NotifyClient.get().markAsRead(notificationId);
+ }
+
+ /**
+ * Marks all notifications as read.
+ */
+ public static boolean markAllAsRead() throws IOException {
+ return NotifyClient.get().markAllAsRead();
+ }
+
+ /**
+ * Connects to real-time notifications.
+ */
+ public static void connect() {
+ NotifyClient.get().connect();
+ }
+
+ /**
+ * Disconnects from real-time notifications.
+ */
+ public static void disconnect() {
+ NotifyClient.get().disconnect();
+ }
+
+ /**
+ * Subscribes to a user's notifications.
+ */
+ public static void subscribeToUser(String userId) {
+ NotifyClient.get().subscribeToUser(userId);
+ }
+
+ /**
+ * Subscribes to app-wide notifications.
+ */
+ public static void subscribeToApp() {
+ NotifyClient.get().subscribeToApp();
+ }
+
+ /**
+ * Flushes the offline queue.
+ */
+ public static void flush() {
+ NotifyClient.get().flush();
+ }
+
+ /**
+ * Flushes the offline queue asynchronously.
+ */
+ public static CompletableFuture flushAsync() {
+ return NotifyClient.get().flushAsync();
+ }
+
+ /**
+ * Shuts down the SDK.
+ */
+ public static void shutdown() {
+ NotifyClient.shutdown();
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/Notification.java b/src/main/java/com/ironservices/notify/Notification.java
new file mode 100644
index 0000000..0724c14
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/Notification.java
@@ -0,0 +1,119 @@
+package com.ironservices.notify;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A notification received from the server.
+ */
+public class Notification {
+ private String id;
+ private String eventType;
+ private String title;
+ private String message;
+ private SeverityLevel severity;
+ private Map metadata;
+ private List actions;
+ private String userId;
+ private String groupKey;
+ private boolean read;
+ private Instant createdAt;
+ private Instant expiresAt;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public void setEventType(String eventType) {
+ this.eventType = eventType;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public SeverityLevel getSeverity() {
+ return severity;
+ }
+
+ public void setSeverity(SeverityLevel severity) {
+ this.severity = severity;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Map metadata) {
+ this.metadata = metadata;
+ }
+
+ public List getActions() {
+ return actions;
+ }
+
+ public void setActions(List actions) {
+ this.actions = actions;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public String getGroupKey() {
+ return groupKey;
+ }
+
+ public void setGroupKey(String groupKey) {
+ this.groupKey = groupKey;
+ }
+
+ public boolean isRead() {
+ return read;
+ }
+
+ public void setRead(boolean read) {
+ this.read = read;
+ }
+
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ public void setCreatedAt(Instant createdAt) {
+ this.createdAt = createdAt;
+ }
+
+ public Instant getExpiresAt() {
+ return expiresAt;
+ }
+
+ public void setExpiresAt(Instant expiresAt) {
+ this.expiresAt = expiresAt;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/NotificationAction.java b/src/main/java/com/ironservices/notify/NotificationAction.java
new file mode 100644
index 0000000..bbe30e4
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/NotificationAction.java
@@ -0,0 +1,63 @@
+package com.ironservices.notify;
+
+/**
+ * Action button on a notification.
+ */
+public class NotificationAction {
+ private String label;
+ private String url;
+ private String action;
+ private String style;
+
+ public NotificationAction() {
+ this.style = "default";
+ }
+
+ public NotificationAction(String label) {
+ this.label = label;
+ this.style = "default";
+ }
+
+ public NotificationAction(String label, String url, String action, String style) {
+ this.label = label;
+ this.url = url;
+ this.action = action;
+ this.style = style != null ? style : "default";
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public NotificationAction setLabel(String label) {
+ this.label = label;
+ return this;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public NotificationAction setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ public String getAction() {
+ return action;
+ }
+
+ public NotificationAction setAction(String action) {
+ this.action = action;
+ return this;
+ }
+
+ public String getStyle() {
+ return style;
+ }
+
+ public NotificationAction setStyle(String style) {
+ this.style = style;
+ return this;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/NotificationPayload.java b/src/main/java/com/ironservices/notify/NotificationPayload.java
new file mode 100644
index 0000000..1eba754
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/NotificationPayload.java
@@ -0,0 +1,121 @@
+package com.ironservices.notify;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Payload for creating a notification.
+ */
+public class NotificationPayload {
+ private String eventType;
+ private String title;
+ private String message;
+ private SeverityLevel severity;
+ private Map metadata;
+ private List actions;
+ private String userId;
+ private String groupKey;
+ private String deduplicationKey;
+ private Instant expiresAt;
+
+ public NotificationPayload() {
+ this.severity = SeverityLevel.INFO;
+ }
+
+ public NotificationPayload(String eventType, String title) {
+ this.eventType = eventType;
+ this.title = title;
+ this.severity = SeverityLevel.INFO;
+ }
+
+ public String getEventType() {
+ return eventType;
+ }
+
+ public NotificationPayload setEventType(String eventType) {
+ this.eventType = eventType;
+ return this;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public NotificationPayload setTitle(String title) {
+ this.title = title;
+ return this;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public NotificationPayload setMessage(String message) {
+ this.message = message;
+ return this;
+ }
+
+ public SeverityLevel getSeverity() {
+ return severity;
+ }
+
+ public NotificationPayload setSeverity(SeverityLevel severity) {
+ this.severity = severity;
+ return this;
+ }
+
+ public Map getMetadata() {
+ return metadata;
+ }
+
+ public NotificationPayload setMetadata(Map metadata) {
+ this.metadata = metadata;
+ return this;
+ }
+
+ public List getActions() {
+ return actions;
+ }
+
+ public NotificationPayload setActions(List actions) {
+ this.actions = actions;
+ return this;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public NotificationPayload setUserId(String userId) {
+ this.userId = userId;
+ return this;
+ }
+
+ public String getGroupKey() {
+ return groupKey;
+ }
+
+ public NotificationPayload setGroupKey(String groupKey) {
+ this.groupKey = groupKey;
+ return this;
+ }
+
+ public String getDeduplicationKey() {
+ return deduplicationKey;
+ }
+
+ public NotificationPayload setDeduplicationKey(String deduplicationKey) {
+ this.deduplicationKey = deduplicationKey;
+ return this;
+ }
+
+ public Instant getExpiresAt() {
+ return expiresAt;
+ }
+
+ public NotificationPayload setExpiresAt(Instant expiresAt) {
+ this.expiresAt = expiresAt;
+ return this;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/NotifyClient.java b/src/main/java/com/ironservices/notify/NotifyClient.java
new file mode 100644
index 0000000..0262808
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/NotifyClient.java
@@ -0,0 +1,347 @@
+package com.ironservices.notify;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+/**
+ * IronNotify client for sending and receiving notifications.
+ */
+public class NotifyClient implements AutoCloseable {
+ private final NotifyOptions options;
+ private final Transport transport;
+ private final OfflineQueue queue;
+ private volatile boolean isOnline = true;
+ private volatile ConnectionState connectionState = ConnectionState.DISCONNECTED;
+
+ private Consumer onNotification;
+ private Consumer onUnreadCountChange;
+ private Consumer onConnectionStateChange;
+
+ private static NotifyClient globalClient;
+ private static final Object globalLock = new Object();
+
+ /**
+ * Creates a new NotifyClient with the given options.
+ */
+ public NotifyClient(NotifyOptions options) {
+ if (options.getApiKey() == null || options.getApiKey().isEmpty()) {
+ throw new IllegalArgumentException("API key is required");
+ }
+
+ this.options = options;
+ this.transport = new Transport(
+ options.getApiBaseUrl(),
+ options.getApiKey(),
+ options.getHttpTimeout(),
+ options.isDebug()
+ );
+
+ if (options.isEnableOfflineQueue()) {
+ this.queue = new OfflineQueue(options.getMaxOfflineQueueSize(), options.isDebug());
+ } else {
+ this.queue = null;
+ }
+
+ if (options.isDebug()) {
+ System.out.println("[IronNotify] Client initialized");
+ }
+ }
+
+ /**
+ * Initializes the global client.
+ */
+ public static void init(String apiKey) {
+ init(new NotifyOptions(apiKey));
+ }
+
+ /**
+ * Initializes the global client with options.
+ */
+ public static void init(NotifyOptions options) {
+ synchronized (globalLock) {
+ if (globalClient != null) {
+ globalClient.close();
+ }
+ globalClient = new NotifyClient(options);
+ }
+ }
+
+ /**
+ * Gets the global client.
+ */
+ public static NotifyClient get() {
+ synchronized (globalLock) {
+ if (globalClient == null) {
+ throw new IllegalStateException("IronNotify not initialized. Call init() first.");
+ }
+ return globalClient;
+ }
+ }
+
+ /**
+ * Sends a notification.
+ */
+ public SendResult notify(String eventType, String title) {
+ return notify(eventType, title, null, SeverityLevel.INFO, null);
+ }
+
+ /**
+ * Sends a notification with options.
+ */
+ public SendResult notify(String eventType, String title, String message,
+ SeverityLevel severity, Map metadata) {
+ NotificationPayload payload = new NotificationPayload(eventType, title);
+ payload.setMessage(message);
+ payload.setSeverity(severity);
+ payload.setMetadata(metadata);
+ return sendPayload(payload);
+ }
+
+ /**
+ * Sends a notification asynchronously.
+ */
+ public CompletableFuture notifyAsync(String eventType, String title) {
+ return notifyAsync(eventType, title, null, SeverityLevel.INFO, null);
+ }
+
+ /**
+ * Sends a notification asynchronously with options.
+ */
+ public CompletableFuture notifyAsync(String eventType, String title, String message,
+ SeverityLevel severity, Map metadata) {
+ NotificationPayload payload = new NotificationPayload(eventType, title);
+ payload.setMessage(message);
+ payload.setSeverity(severity);
+ payload.setMetadata(metadata);
+ return sendPayloadAsync(payload);
+ }
+
+ /**
+ * Creates an event builder.
+ */
+ public EventBuilder event(String eventType) {
+ return new EventBuilder(this, eventType);
+ }
+
+ /**
+ * Sends a notification payload.
+ */
+ public SendResult sendPayload(NotificationPayload payload) {
+ SendResult result = transport.send(payload);
+
+ if (!result.isSuccess() && options.isEnableOfflineQueue() && queue != null) {
+ queue.add(payload);
+ isOnline = false;
+ return SendResult.queued(result.getError());
+ }
+
+ return result;
+ }
+
+ /**
+ * Sends a notification payload asynchronously.
+ */
+ public CompletableFuture sendPayloadAsync(NotificationPayload payload) {
+ return transport.sendAsync(payload).thenApply(result -> {
+ if (!result.isSuccess() && options.isEnableOfflineQueue() && queue != null) {
+ queue.add(payload);
+ isOnline = false;
+ return SendResult.queued(result.getError());
+ }
+ return result;
+ });
+ }
+
+ /**
+ * Gets notifications.
+ */
+ public List getNotifications() throws IOException {
+ return getNotifications(null, null, false);
+ }
+
+ /**
+ * Gets notifications with options.
+ */
+ public List getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
+ return transport.getNotifications(limit, offset, unreadOnly);
+ }
+
+ /**
+ * Gets notifications asynchronously.
+ */
+ public CompletableFuture> getNotificationsAsync() {
+ return getNotificationsAsync(null, null, false);
+ }
+
+ /**
+ * Gets notifications asynchronously with options.
+ */
+ public CompletableFuture> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
+ return transport.getNotificationsAsync(limit, offset, unreadOnly);
+ }
+
+ /**
+ * Gets the unread notification count.
+ */
+ public int getUnreadCount() throws IOException {
+ return transport.getUnreadCount();
+ }
+
+ /**
+ * Gets the unread notification count asynchronously.
+ */
+ public CompletableFuture getUnreadCountAsync() {
+ return transport.getUnreadCountAsync();
+ }
+
+ /**
+ * Marks a notification as read.
+ */
+ public boolean markAsRead(String notificationId) throws IOException {
+ return transport.markAsRead(notificationId);
+ }
+
+ /**
+ * Marks a notification as read asynchronously.
+ */
+ public CompletableFuture markAsReadAsync(String notificationId) {
+ return transport.markAsReadAsync(notificationId);
+ }
+
+ /**
+ * Marks all notifications as read.
+ */
+ public boolean markAllAsRead() throws IOException {
+ return transport.markAllAsRead();
+ }
+
+ /**
+ * Marks all notifications as read asynchronously.
+ */
+ public CompletableFuture markAllAsReadAsync() {
+ return transport.markAllAsReadAsync();
+ }
+
+ /**
+ * Sets the notification handler.
+ */
+ public void onNotification(Consumer handler) {
+ this.onNotification = handler;
+ }
+
+ /**
+ * Sets the unread count change handler.
+ */
+ public void onUnreadCountChange(Consumer handler) {
+ this.onUnreadCountChange = handler;
+ }
+
+ /**
+ * Sets the connection state change handler.
+ */
+ public void onConnectionStateChange(Consumer handler) {
+ this.onConnectionStateChange = handler;
+ }
+
+ /**
+ * Gets the current connection state.
+ */
+ public ConnectionState getConnectionState() {
+ return connectionState;
+ }
+
+ /**
+ * Connects to real-time notifications.
+ */
+ public void connect() {
+ setConnectionState(ConnectionState.CONNECTED);
+ if (options.isDebug()) {
+ System.out.println("[IronNotify] Connected (WebSocket not implemented)");
+ }
+ }
+
+ /**
+ * Disconnects from real-time notifications.
+ */
+ public void disconnect() {
+ setConnectionState(ConnectionState.DISCONNECTED);
+ }
+
+ /**
+ * Subscribes to a user's notifications.
+ */
+ public void subscribeToUser(String userId) {
+ if (options.isDebug()) {
+ System.out.println("[IronNotify] Subscribed to user: " + userId);
+ }
+ }
+
+ /**
+ * Subscribes to app-wide notifications.
+ */
+ public void subscribeToApp() {
+ if (options.isDebug()) {
+ System.out.println("[IronNotify] Subscribed to app notifications");
+ }
+ }
+
+ /**
+ * Flushes the offline queue.
+ */
+ public void flush() {
+ if (queue == null || queue.isEmpty()) {
+ return;
+ }
+
+ if (!transport.isOnline()) {
+ return;
+ }
+
+ isOnline = true;
+ List notifications = queue.getAll();
+
+ for (int i = notifications.size() - 1; i >= 0; i--) {
+ SendResult result = transport.send(notifications.get(i));
+ if (result.isSuccess()) {
+ queue.remove(i);
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Flushes the offline queue asynchronously.
+ */
+ public CompletableFuture flushAsync() {
+ return CompletableFuture.runAsync(this::flush);
+ }
+
+ private void setConnectionState(ConnectionState state) {
+ this.connectionState = state;
+ if (onConnectionStateChange != null) {
+ onConnectionStateChange.accept(state);
+ }
+ }
+
+ @Override
+ public void close() {
+ disconnect();
+ transport.close();
+ }
+
+ /**
+ * Closes the global client.
+ */
+ public static void shutdown() {
+ synchronized (globalLock) {
+ if (globalClient != null) {
+ globalClient.close();
+ globalClient = null;
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/NotifyOptions.java b/src/main/java/com/ironservices/notify/NotifyOptions.java
new file mode 100644
index 0000000..730823c
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/NotifyOptions.java
@@ -0,0 +1,187 @@
+package com.ironservices.notify;
+
+import java.time.Duration;
+
+/**
+ * Configuration options for the IronNotify client.
+ */
+public class NotifyOptions {
+ private String apiKey;
+ private String apiBaseUrl = "https://api.ironnotify.com";
+ private String webSocketUrl = "wss://ws.ironnotify.com";
+ private boolean debug = false;
+ private boolean enableOfflineQueue = true;
+ private int maxOfflineQueueSize = 100;
+ private boolean autoReconnect = true;
+ private int maxReconnectAttempts = 5;
+ private Duration reconnectDelay = Duration.ofSeconds(1);
+ private Duration httpTimeout = Duration.ofSeconds(30);
+
+ public NotifyOptions() {
+ }
+
+ public NotifyOptions(String apiKey) {
+ this.apiKey = apiKey;
+ }
+
+ public String getApiKey() {
+ return apiKey;
+ }
+
+ public NotifyOptions setApiKey(String apiKey) {
+ this.apiKey = apiKey;
+ return this;
+ }
+
+ public String getApiBaseUrl() {
+ return apiBaseUrl;
+ }
+
+ public NotifyOptions setApiBaseUrl(String apiBaseUrl) {
+ this.apiBaseUrl = apiBaseUrl;
+ return this;
+ }
+
+ public String getWebSocketUrl() {
+ return webSocketUrl;
+ }
+
+ public NotifyOptions setWebSocketUrl(String webSocketUrl) {
+ this.webSocketUrl = webSocketUrl;
+ return this;
+ }
+
+ public boolean isDebug() {
+ return debug;
+ }
+
+ public NotifyOptions setDebug(boolean debug) {
+ this.debug = debug;
+ return this;
+ }
+
+ public boolean isEnableOfflineQueue() {
+ return enableOfflineQueue;
+ }
+
+ public NotifyOptions setEnableOfflineQueue(boolean enableOfflineQueue) {
+ this.enableOfflineQueue = enableOfflineQueue;
+ return this;
+ }
+
+ public int getMaxOfflineQueueSize() {
+ return maxOfflineQueueSize;
+ }
+
+ public NotifyOptions setMaxOfflineQueueSize(int maxOfflineQueueSize) {
+ this.maxOfflineQueueSize = maxOfflineQueueSize;
+ return this;
+ }
+
+ public boolean isAutoReconnect() {
+ return autoReconnect;
+ }
+
+ public NotifyOptions setAutoReconnect(boolean autoReconnect) {
+ this.autoReconnect = autoReconnect;
+ return this;
+ }
+
+ public int getMaxReconnectAttempts() {
+ return maxReconnectAttempts;
+ }
+
+ public NotifyOptions setMaxReconnectAttempts(int maxReconnectAttempts) {
+ this.maxReconnectAttempts = maxReconnectAttempts;
+ return this;
+ }
+
+ public Duration getReconnectDelay() {
+ return reconnectDelay;
+ }
+
+ public NotifyOptions setReconnectDelay(Duration reconnectDelay) {
+ this.reconnectDelay = reconnectDelay;
+ return this;
+ }
+
+ public Duration getHttpTimeout() {
+ return httpTimeout;
+ }
+
+ public NotifyOptions setHttpTimeout(Duration httpTimeout) {
+ this.httpTimeout = httpTimeout;
+ return this;
+ }
+
+ /**
+ * Creates a builder for NotifyOptions.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Builder for NotifyOptions.
+ */
+ public static class Builder {
+ private final NotifyOptions options = new NotifyOptions();
+
+ public Builder apiKey(String apiKey) {
+ options.setApiKey(apiKey);
+ return this;
+ }
+
+ public Builder apiBaseUrl(String apiBaseUrl) {
+ options.setApiBaseUrl(apiBaseUrl);
+ return this;
+ }
+
+ public Builder webSocketUrl(String webSocketUrl) {
+ options.setWebSocketUrl(webSocketUrl);
+ return this;
+ }
+
+ public Builder debug(boolean debug) {
+ options.setDebug(debug);
+ return this;
+ }
+
+ public Builder enableOfflineQueue(boolean enable) {
+ options.setEnableOfflineQueue(enable);
+ return this;
+ }
+
+ public Builder maxOfflineQueueSize(int size) {
+ options.setMaxOfflineQueueSize(size);
+ return this;
+ }
+
+ public Builder autoReconnect(boolean autoReconnect) {
+ options.setAutoReconnect(autoReconnect);
+ return this;
+ }
+
+ public Builder maxReconnectAttempts(int attempts) {
+ options.setMaxReconnectAttempts(attempts);
+ return this;
+ }
+
+ public Builder reconnectDelay(Duration delay) {
+ options.setReconnectDelay(delay);
+ return this;
+ }
+
+ public Builder httpTimeout(Duration timeout) {
+ options.setHttpTimeout(timeout);
+ return this;
+ }
+
+ public NotifyOptions build() {
+ if (options.getApiKey() == null || options.getApiKey().isEmpty()) {
+ throw new IllegalArgumentException("API key is required");
+ }
+ return options;
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/OfflineQueue.java b/src/main/java/com/ironservices/notify/OfflineQueue.java
new file mode 100644
index 0000000..a8a2e37
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/OfflineQueue.java
@@ -0,0 +1,111 @@
+package com.ironservices.notify;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.*;
+import java.lang.reflect.Type;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Offline queue for storing notifications when offline.
+ */
+class OfflineQueue {
+ private final int maxSize;
+ private final boolean debug;
+ private final List queue;
+ private final Path storagePath;
+ private final Gson gson;
+ private final Object lock = new Object();
+
+ OfflineQueue(int maxSize, boolean debug) {
+ this.maxSize = maxSize;
+ this.debug = debug;
+ this.queue = new ArrayList<>();
+ this.storagePath = Paths.get(System.getProperty("user.home"), ".ironnotify", "offline_queue.json");
+ this.gson = new GsonBuilder().create();
+ loadFromStorage();
+ }
+
+ void add(NotificationPayload payload) {
+ synchronized (lock) {
+ if (queue.size() >= maxSize) {
+ queue.remove(0);
+ if (debug) {
+ System.out.println("[IronNotify] Offline queue full, dropping oldest notification");
+ }
+ }
+
+ queue.add(payload);
+ saveToStorage();
+
+ if (debug) {
+ System.out.println("[IronNotify] Notification queued for later: " + payload.getEventType());
+ }
+ }
+ }
+
+ List getAll() {
+ synchronized (lock) {
+ return new ArrayList<>(queue);
+ }
+ }
+
+ void remove(int index) {
+ synchronized (lock) {
+ if (index >= 0 && index < queue.size()) {
+ queue.remove(index);
+ saveToStorage();
+ }
+ }
+ }
+
+ void clear() {
+ synchronized (lock) {
+ queue.clear();
+ saveToStorage();
+ }
+ }
+
+ int size() {
+ synchronized (lock) {
+ return queue.size();
+ }
+ }
+
+ boolean isEmpty() {
+ synchronized (lock) {
+ return queue.isEmpty();
+ }
+ }
+
+ private void loadFromStorage() {
+ try {
+ if (Files.exists(storagePath)) {
+ String json = new String(Files.readAllBytes(storagePath));
+ Type listType = new TypeToken>(){}.getType();
+ List loaded = gson.fromJson(json, listType);
+ if (loaded != null) {
+ queue.addAll(loaded);
+ }
+ }
+ } catch (Exception e) {
+ // Ignore errors loading from storage
+ }
+ }
+
+ private void saveToStorage() {
+ try {
+ Files.createDirectories(storagePath.getParent());
+ String json = gson.toJson(queue);
+ Files.write(storagePath, json.getBytes());
+ } catch (Exception e) {
+ // Ignore errors saving to storage
+ }
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/SendResult.java b/src/main/java/com/ironservices/notify/SendResult.java
new file mode 100644
index 0000000..097b6c7
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/SendResult.java
@@ -0,0 +1,46 @@
+package com.ironservices.notify;
+
+/**
+ * Result of sending a notification.
+ */
+public class SendResult {
+ private final boolean success;
+ private final String notificationId;
+ private final String error;
+ private final boolean queued;
+
+ private SendResult(boolean success, String notificationId, String error, boolean queued) {
+ this.success = success;
+ this.notificationId = notificationId;
+ this.error = error;
+ this.queued = queued;
+ }
+
+ public static SendResult success(String notificationId) {
+ return new SendResult(true, notificationId, null, false);
+ }
+
+ public static SendResult failure(String error) {
+ return new SendResult(false, null, error, false);
+ }
+
+ public static SendResult queued(String error) {
+ return new SendResult(false, null, error, true);
+ }
+
+ public boolean isSuccess() {
+ return success;
+ }
+
+ public String getNotificationId() {
+ return notificationId;
+ }
+
+ public String getError() {
+ return error;
+ }
+
+ public boolean isQueued() {
+ return queued;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/SeverityLevel.java b/src/main/java/com/ironservices/notify/SeverityLevel.java
new file mode 100644
index 0000000..4d595d0
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/SeverityLevel.java
@@ -0,0 +1,38 @@
+package com.ironservices.notify;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * Severity level for notifications.
+ */
+public enum SeverityLevel {
+ @SerializedName("info")
+ INFO("info"),
+
+ @SerializedName("success")
+ SUCCESS("success"),
+
+ @SerializedName("warning")
+ WARNING("warning"),
+
+ @SerializedName("error")
+ ERROR("error"),
+
+ @SerializedName("critical")
+ CRITICAL("critical");
+
+ private final String value;
+
+ SeverityLevel(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/src/main/java/com/ironservices/notify/Transport.java b/src/main/java/com/ironservices/notify/Transport.java
new file mode 100644
index 0000000..588b4f7
--- /dev/null
+++ b/src/main/java/com/ironservices/notify/Transport.java
@@ -0,0 +1,235 @@
+package com.ironservices.notify;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+import okhttp3.*;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.time.Duration;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * HTTP transport for IronNotify API.
+ */
+class Transport {
+ private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+
+ private final String baseUrl;
+ private final String apiKey;
+ private final boolean debug;
+ private final OkHttpClient client;
+ private final Gson gson;
+
+ Transport(String baseUrl, String apiKey, Duration timeout, boolean debug) {
+ this.baseUrl = baseUrl;
+ this.apiKey = apiKey;
+ this.debug = debug;
+ this.client = new OkHttpClient.Builder()
+ .connectTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
+ .readTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
+ .writeTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
+ .build();
+ this.gson = new GsonBuilder()
+ .setFieldNamingStrategy(field -> {
+ String name = field.getName();
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ if (Character.isUpperCase(c)) {
+ if (i > 0) {
+ sb.append('_');
+ }
+ sb.append(Character.toLowerCase(c));
+ } else {
+ sb.append(c);
+ }
+ }
+ return sb.toString();
+ })
+ .create();
+ }
+
+ SendResult send(NotificationPayload payload) {
+ try {
+ String json = gson.toJson(payload);
+
+ if (debug) {
+ System.out.println("[IronNotify] Sending notification: " + payload.getEventType());
+ }
+
+ Request request = new Request.Builder()
+ .url(baseUrl + "/api/v1/notify")
+ .header("Authorization", "Bearer " + apiKey)
+ .post(RequestBody.create(json, JSON))
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ String body = response.body() != null ? response.body().string() : "";
+
+ if (response.isSuccessful()) {
+ SendResponse result = gson.fromJson(body, SendResponse.class);
+ return SendResult.success(result != null ? result.notificationId : null);
+ }
+
+ ErrorResponse error = gson.fromJson(body, ErrorResponse.class);
+ return SendResult.failure(error != null && error.error != null
+ ? error.error
+ : "HTTP " + response.code() + ": " + body);
+ }
+ } catch (IOException e) {
+ return SendResult.failure(e.getMessage());
+ }
+ }
+
+ CompletableFuture sendAsync(NotificationPayload payload) {
+ return CompletableFuture.supplyAsync(() -> send(payload));
+ }
+
+ List getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
+ HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + "/api/v1/notifications").newBuilder();
+
+ if (limit != null && limit > 0) {
+ urlBuilder.addQueryParameter("limit", String.valueOf(limit));
+ }
+ if (offset != null && offset > 0) {
+ urlBuilder.addQueryParameter("offset", String.valueOf(offset));
+ }
+ if (unreadOnly) {
+ urlBuilder.addQueryParameter("unread_only", "true");
+ }
+
+ Request request = new Request.Builder()
+ .url(urlBuilder.build())
+ .header("Authorization", "Bearer " + apiKey)
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("HTTP " + response.code());
+ }
+
+ String body = response.body() != null ? response.body().string() : "[]";
+ Type listType = new TypeToken>(){}.getType();
+ return gson.fromJson(body, listType);
+ }
+ }
+
+ CompletableFuture> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return getNotifications(limit, offset, unreadOnly);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ int getUnreadCount() throws IOException {
+ Request request = new Request.Builder()
+ .url(baseUrl + "/api/v1/notifications/unread-count")
+ .header("Authorization", "Bearer " + apiKey)
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ if (!response.isSuccessful()) {
+ throw new IOException("HTTP " + response.code());
+ }
+
+ String body = response.body() != null ? response.body().string() : "{}";
+ CountResponse result = gson.fromJson(body, CountResponse.class);
+ return result != null ? result.count : 0;
+ }
+ }
+
+ CompletableFuture getUnreadCountAsync() {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return getUnreadCount();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ boolean markAsRead(String notificationId) throws IOException {
+ Request request = new Request.Builder()
+ .url(baseUrl + "/api/v1/notifications/" + notificationId + "/read")
+ .header("Authorization", "Bearer " + apiKey)
+ .post(RequestBody.create("", JSON))
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ return response.isSuccessful();
+ }
+ }
+
+ CompletableFuture markAsReadAsync(String notificationId) {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return markAsRead(notificationId);
+ } catch (IOException e) {
+ return false;
+ }
+ });
+ }
+
+ boolean markAllAsRead() throws IOException {
+ Request request = new Request.Builder()
+ .url(baseUrl + "/api/v1/notifications/read-all")
+ .header("Authorization", "Bearer " + apiKey)
+ .post(RequestBody.create("", JSON))
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ return response.isSuccessful();
+ }
+ }
+
+ CompletableFuture markAllAsReadAsync() {
+ return CompletableFuture.supplyAsync(() -> {
+ try {
+ return markAllAsRead();
+ } catch (IOException e) {
+ return false;
+ }
+ });
+ }
+
+ boolean isOnline() {
+ try {
+ Request request = new Request.Builder()
+ .url(baseUrl + "/health")
+ .get()
+ .build();
+
+ try (Response response = client.newCall(request).execute()) {
+ return response.isSuccessful();
+ }
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ void close() {
+ client.dispatcher().executorService().shutdown();
+ client.connectionPool().evictAll();
+ }
+
+ private static class SendResponse {
+ String notificationId;
+ }
+
+ private static class ErrorResponse {
+ String error;
+ }
+
+ private static class CountResponse {
+ int count;
+ }
+}