atozas-push-notification
Version:
Real-time push notifications across platforms using socket.io
326 lines (283 loc) • 8.3 kB
text/typescript
import { io, Socket } from 'socket.io-client';
import {
ClientConfig,
UserInfo,
NotificationData,
NotificationOptions,
NotificationCallback,
ConnectionCallback,
ErrorCallback,
UserStatusCallback,
NotificationPermission
} from './types';
import { NotificationDisplayManager } from './notification-display';
export class AtozasPushNotificationClient {
private socket: Socket | null = null;
private config: ClientConfig;
private userInfo: UserInfo | null = null;
private isConnected: boolean = false;
private reconnectAttempts: number = 0;
private displayManager: NotificationDisplayManager;
// Event handlers
private onNotificationCallback: NotificationCallback | null = null;
private onConnectionCallback: ConnectionCallback | null = null;
private onErrorCallback: ErrorCallback | null = null;
private onUserStatusCallback: UserStatusCallback | null = null;
constructor(config: ClientConfig) {
this.config = {
autoConnect: true,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
forceNew: false,
requestPermission: true,
fallbackToUI: true,
showNotifications: true,
enableVibration: true,
...config
};
// Initialize notification display manager
this.displayManager = new NotificationDisplayManager(this.config);
if (this.config.autoConnect) {
this.connect();
}
}
/**
* Connect to the notification server
*/
public connect(): Promise<void> {
return new Promise((resolve, reject) => {
try {
this.socket = io(this.config.url, {
autoConnect: false,
reconnection: this.config.reconnection,
reconnectionAttempts: this.config.reconnectionAttempts,
reconnectionDelay: this.config.reconnectionDelay,
reconnectionDelayMax: this.config.reconnectionDelayMax,
timeout: this.config.timeout,
forceNew: this.config.forceNew
});
this.setupEventHandlers();
this.socket.connect();
this.socket.on('connect', () => {
this.isConnected = true;
this.reconnectAttempts = 0;
// Re-authenticate if user info exists
if (this.userInfo) {
this.authenticate(this.userInfo);
}
if (this.onConnectionCallback) {
this.onConnectionCallback(true);
}
resolve();
});
this.socket.on('connect_error', (error) => {
this.isConnected = false;
if (this.onErrorCallback) {
this.onErrorCallback(error);
}
reject(error);
});
} catch (error) {
reject(error);
}
});
}
/**
* Disconnect from the notification server
*/
public disconnect(): void {
if (this.socket) {
this.socket.disconnect();
this.isConnected = false;
if (this.onConnectionCallback) {
this.onConnectionCallback(false);
}
}
}
/**
* Authenticate user with the server
*/
public authenticate(userInfo: UserInfo): void {
this.userInfo = userInfo;
if (this.socket && this.isConnected) {
this.socket.emit('authenticate', userInfo);
}
}
/**
* Join a group for group notifications
*/
public joinGroup(groupId: string): void {
if (this.socket && this.isConnected) {
this.socket.emit('join_group', groupId);
}
}
/**
* Leave a group
*/
public leaveGroup(groupId: string): void {
if (this.socket && this.isConnected) {
this.socket.emit('leave_group', groupId);
}
}
/**
* Set notification callback handler
*/
public onNotification(callback: NotificationCallback): void {
this.onNotificationCallback = callback;
}
/**
* Set connection status callback handler
*/
public onConnection(callback: ConnectionCallback): void {
this.onConnectionCallback = callback;
}
/**
* Set error callback handler
*/
public onError(callback: ErrorCallback): void {
this.onErrorCallback = callback;
}
/**
* Set user status callback handler
*/
public onUserStatus(callback: UserStatusCallback): void {
this.onUserStatusCallback = callback;
}
/**
* Get connection status
*/
public getConnectionStatus(): boolean {
return this.isConnected;
}
/**
* Get current user info
*/
public getUserInfo(): UserInfo | null {
return this.userInfo;
}
/**
* Send acknowledgment for received notification
*/
public acknowledgeNotification(notificationId: string): void {
if (this.socket && this.isConnected) {
this.socket.emit('notification_ack', notificationId);
}
}
/**
* Request user status
*/
public requestUserStatus(userId: string): void {
if (this.socket && this.isConnected) {
this.socket.emit('request_user_status', userId);
}
}
/**
* Request notification permission
*/
public async requestNotificationPermission(): Promise<NotificationPermission> {
return await this.displayManager.requestPermission();
}
/**
* Get notification permission status
*/
public getNotificationPermission(): NotificationPermission {
return this.displayManager.getPermissionStatus();
}
/**
* Set notification click handler
*/
public onNotificationClick(callback: (notification: NotificationData) => void): void {
this.displayManager.onNotificationClick(callback);
}
/**
* Close all active notifications
*/
public closeAllNotifications(): void {
this.displayManager.closeAllNotifications();
}
/**
* Display a local notification (without sending through socket)
*/
public displayLocalNotification(notification: NotificationData, options?: NotificationOptions): void {
this.displayManager.displayNotification(notification, options);
}
/**
* Setup socket event handlers
*/
private setupEventHandlers(): void {
if (!this.socket) return;
// Handle notifications
this.socket.on('notification', (data: { notification: NotificationData; options?: NotificationOptions }) => {
// Display notification visually
if (this.config.showNotifications) {
this.displayManager.displayNotification(data.notification, data.options);
}
// Call user callback
if (this.onNotificationCallback) {
this.onNotificationCallback(data.notification, data.options);
}
});
// Handle disconnection
this.socket.on('disconnect', (reason) => {
this.isConnected = false;
if (this.onConnectionCallback) {
this.onConnectionCallback(false);
}
// Auto-reconnect logic
if (this.config.reconnection && reason === 'io server disconnect') {
this.handleReconnection();
}
});
// Handle reconnection
this.socket.on('reconnect', () => {
this.isConnected = true;
this.reconnectAttempts = 0;
// Re-authenticate
if (this.userInfo) {
this.authenticate(this.userInfo);
}
if (this.onConnectionCallback) {
this.onConnectionCallback(true);
}
});
// Handle reconnection attempts
this.socket.on('reconnect_attempt', () => {
this.reconnectAttempts++;
});
// Handle user status updates
this.socket.on('user_status', (data: { userId: string; online: boolean }) => {
if (this.onUserStatusCallback) {
this.onUserStatusCallback(data.userId, data.online);
}
});
// Handle errors
this.socket.on('error', (error) => {
if (this.onErrorCallback) {
this.onErrorCallback(error);
}
});
}
/**
* Handle reconnection logic
*/
private handleReconnection(): void {
if (this.reconnectAttempts >= (this.config.reconnectionAttempts || 5)) {
if (this.onErrorCallback) {
this.onErrorCallback(new Error('Max reconnection attempts reached'));
}
return;
}
const delay = Math.min(
this.config.reconnectionDelay! * Math.pow(2, this.reconnectAttempts),
this.config.reconnectionDelayMax!
);
setTimeout(() => {
if (!this.isConnected && this.socket) {
this.socket.connect();
}
}, delay);
}
}