# Client-Side: Hybrid Notification (Heartbeat & Presence)

Panduan implementasi di sisi client (Flutter / Web) untuk mendukung **notifikasi hybrid** (Channels saat online, Beams saat offline).

---

## 1. Endpoint yang Digunakan

| Method | Endpoint | Deskripsi |
|--------|----------|-----------|
| POST | `/api/mobile/device/register` | Daftar device (saat login) — sudah ada |
| POST | `/api/mobile/device/heartbeat` | Update presence tiap 30–60 detik |
| POST | `/api/mobile/device/offline` | Tandai device offline (saat logout) |
| DELETE | `/api/mobile/auth/device/{device_id}` | Unregister device |

---

## 2. Flutter (Dart) Example

### 2.1 Dependencies

```yaml
# pubspec.yaml
dependencies:
  flutter_local_notifications: ^17.0.0
  pusher_beams: ^1.0.0  # atau package Beams yang dipakai
  http: ^1.0.0
  uuid: ^4.0.0
```

### 2.2 Device ID (persistent)

```dart
import 'package:uuid/uuid.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<String> getOrCreateDeviceId() async {
  final prefs = await SharedPreferences.getInstance();
  String? deviceId = prefs.getString('device_uuid');
  if (deviceId == null || deviceId.isEmpty) {
    deviceId = const Uuid().v4();
    await prefs.setString('device_uuid', deviceId);
  }
  return deviceId;
}
```

### 2.3 Heartbeat Service

```dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;

class PresenceHeartbeatService {
  static const _interval = Duration(seconds: 45); // 30–60 detik
  Timer? _timer;
  final String baseUrl;
  final String token;
  final String deviceId;
  String? _socketId; // dari Pusher connection (opsional)

  PresenceHeartbeatService({
    required this.baseUrl,
    required this.token,
    required this.deviceId,
    String? socketId,
  }) : _socketId = socketId;

  void start() {
    stop();
    _timer = Timer.periodic(_interval, (_) => _sendHeartbeat());
    // Kirim segera saat start
    _sendHeartbeat();
  }

  void stop() {
    _timer?.cancel();
    _timer = null;
  }

  void updateSocketId(String? socketId) {
    _socketId = socketId;
  }

  Future<void> _sendHeartbeat() async {
    try {
      final body = <String, dynamic>{
        'device_id': deviceId,
        if (_socketId != null) 'socket_id': _socketId,
      };
      final res = await http.post(
        Uri.parse('$baseUrl/api/mobile/device/heartbeat'),
        headers: {
          'Authorization': 'Bearer $token',
          'Content-Type': 'application/json',
        },
        body: jsonEncode(body),
      );
      if (res.statusCode != 200) {
        debugPrint('Heartbeat failed: ${res.statusCode}');
      }
    } catch (e) {
      debugPrint('Heartbeat error: $e');
    }
  }

  Future<void> markOffline() async {
    try {
      await http.post(
        Uri.parse('$baseUrl/api/mobile/device/offline'),
        headers: {
          'Authorization': 'Bearer $token',
          'Content-Type': 'application/json',
        },
        body: jsonEncode({'device_id': deviceId}),
      );
    } catch (e) {
      debugPrint('Mark offline error: $e');
    }
  }
}
```

### 2.4 Integrasi di App Lifecycle

```dart
// Di main.dart atau AuthProvider
class AppState extends ChangeNotifier {
  PresenceHeartbeatService? _heartbeatService;

  Future<void> onLogin(String token) async {
    final deviceId = await getOrCreateDeviceId();
    // 1. Register device (sudah ada di login flow)
    // POST /api/mobile/auth/device/register
    // ...

    // 2. Start heartbeat
    _heartbeatService = PresenceHeartbeatService(
      baseUrl: 'https://your-api.com',
      token: token,
      deviceId: deviceId,
    );
    _heartbeatService!.start();
  }

  Future<void> onLogout() async {
    if (_heartbeatService != null) {
      await _heartbeatService!.markOffline();
      _heartbeatService!.stop();
      _heartbeatService = null;
    }
    // ... sisa logout
  }
}

// Lifecycle: pause / resume
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) {
      // App di background — stop heartbeat (akan dianggap offline setelah 90 detik)
      context.read<AppState>().pauseHeartbeat();
    } else if (state == AppLifecycleState.resumed) {
      context.read<AppState>().resumeHeartbeat();
    }
  }
}
```

### 2.5 Subscribe ke Pusher Channels (realtime)

```dart
// Subscribe ke channel client / agent sesuai role
// Channel: private-client.{clientId} atau private-inbox.agent.{agentId}
Pusher pusher = Pusher(
  'YOUR_PUSHER_KEY',
  PusherOptions(cluster: 'ap1'),
);

Channel channel = pusher.subscribe('private-client.123');
channel.bind('incoming.message', (data) {
  // Update UI — pesan baru masuk (realtime)
  // Tidak perlu push karena user online
});
```

---

## 3. Web (JavaScript / React) Example

### 3.1 Heartbeat saat user aktif di web

```javascript
// presenceHeartbeat.js
const HEARTBEAT_INTERVAL_MS = 45 * 1000; // 45 detik

let heartbeatTimer = null;
let deviceId = null;

function getOrCreateDeviceId() {
  let id = localStorage.getItem('device_uuid');
  if (!id) {
    id = 'web-' + crypto.randomUUID();
    localStorage.setItem('device_uuid', id);
  }
  return id;
}

function startHeartbeat(token) {
  stopHeartbeat();
  deviceId = getOrCreateDeviceId();

  const sendHeartbeat = async () => {
    try {
      const socketId = window.Pusher?.connection?.socket_id || null;
      const res = await fetch('/api/mobile/device/heartbeat', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          device_id: deviceId,
          socket_id: socketId,
        }),
      });
      if (!res.ok) console.warn('Heartbeat failed:', res.status);
    } catch (e) {
      console.warn('Heartbeat error:', e);
    }
  };

  sendHeartbeat(); // segera
  heartbeatTimer = setInterval(sendHeartbeat, HEARTBEAT_INTERVAL_MS);
}

function stopHeartbeat() {
  if (heartbeatTimer) {
    clearInterval(heartbeatTimer);
    heartbeatTimer = null;
  }
}

async function markOffline(token) {
  if (!deviceId) return;
  try {
    await fetch('/api/mobile/device/offline', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ device_id: deviceId }),
    });
  } catch (e) {
    console.warn('Mark offline error:', e);
  }
}

export { startHeartbeat, stopHeartbeat, markOffline, getOrCreateDeviceId };
```

### 3.2 React: usePresence hook

```jsx
// usePresence.js
import { useEffect, useRef } from 'react';
import { startHeartbeat, stopHeartbeat, markOffline } from './presenceHeartbeat';

export function usePresence(token) {
  const started = useRef(false);

  useEffect(() => {
    if (!token) return;
    startHeartbeat(token);
    started.current = true;

    const handleVisibilityChange = () => {
      if (document.hidden) {
        stopHeartbeat(); // Tab tidak aktif — stop, akan offline setelah 90s
      } else {
        startHeartbeat(token); // Tab aktif lagi
      }
    };

    document.addEventListener('visibilitychange', handleVisibilityChange);

    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityChange);
      if (started.current) {
        markOffline(token).finally(() => stopHeartbeat());
      }
    };
  }, [token]);
}
```

### 3.3 Integrasi di Login/Logout

```jsx
// Di AuthContext atau layout
function AppLayout() {
  const { token } = useAuth();

  usePresence(token);

  return (
    // ...
  );
}
```

---

## 4. Alur Singkat

```
[Login]
   → Register device (POST /device/register)
   → Start heartbeat (POST /device/heartbeat setiap 45 detik)

[App aktif]
   → Heartbeat berjalan
   → last_seen_at ter-update
   → is_online = true
   → Notifikasi via Channels (realtime), tidak ada push

[App di background / tab tidak aktif]
   → Stop heartbeat
   → Setelah 90 detik: dianggap offline
   → Notifikasi via Beams (push)

[Logout]
   → POST /device/offline
   → Stop heartbeat
   → (Opsional) DELETE /device/{id}
```

---

## 5. Catatan

- **Interval heartbeat:** 30–60 detik. Disarankan 45 detik.
- **Timeout offline:** 90 detik tanpa heartbeat → device dianggap offline.
- **Web:** Jika pakai web client dengan auth Sanctum, pastikan base URL `/api/mobile` bisa diakses (CORS, prefix route).
- **Device ID:** Harus persistent (SharedPreferences / localStorage). Jangan generate baru tiap session.
