# Chat socket — Flutter

Socket.IO **4.x** (`socket.io` `4.2.0`, `allowEIO3: true`). The chat text is saved by the Laravel API. The socket only registers which room the device is in. Do not write messages on the socket.

## Connection

| Item | Value |
|---|---|
| Library | `socket_io_client` compatible with Socket.IO 4 |
| Path | `/socket.io/` (default) |
| Host | server `NODE_HOST` |
| Port | server `NODE_PORT` |
| Scheme | `https` when `NODE_MODE=live`, otherwise `http` |

Ask backend for the deployed host and port. They are not fixed in the app repo.

Mobile clients are not blocked by the browser CORS rule (`origin` is `APP_URL`).

## 1. Get a socket token

The API Bearer token is not a socket token. Request a short-lived token after login and refresh it before it expires.

`GET /api/user/socket-token`

Headers:

- `Authorization: Bearer <user access token>`
- `Accept: application/json`
- `lang: ar` or `en`

```json
{
  "key": "success",
  "msg": "تم التنفيذ بنجاح",
  "code": 200,
  "data": {
    "socket_token": "<payload>.<signature>",
    "expires_in": 300
  }
}
```

`expires_in` is 300 seconds. Refresh the token on a timer (for example at 240 seconds) and on `socket_auth_required`.

The token payload is `{ user_id, user_type: "user", exp }`. The server ignores any user id sent by the app.

## 2. Connect and enter a room

Connect, then emit `enterChat`. There is no success acknowledgement. A response means the join failed.

```dart
socket.emit('enterChat', {
  'socket_token': socketToken,
  'room_id': roomId, // int or string; server uses String(room_id)
});
```

`room_id` comes from `GET /api/user/chats` (`data.rooms[].id`).

Leave the room when the chat screen closes:

```dart
socket.emit('exitChat');
```

`disconnect` also drops the room binding. One socket connection tracks one room (`socket.room_id`). Open a new `enterChat` when switching rooms.

## 3. Send a message on REST, not the socket

`POST /api/user/chats/{room}/messages`

Body (JSON or form):

| Field | Rules |
|---|---|
| `body` | required string, max 5000, text only |
| `client_message_id` | required string, max 191, unique per sender in that room |

Reuse the same `client_message_id` on retry. The server returns the original message instead of inserting a second row.

`201` data:

```json
{
  "id": 12,
  "room_id": 4,
  "body": "hello",
  "type": "text",
  "client_message_id": "flutter-uuid",
  "is_deleted": false,
  "sent_at": "2026-09-30T13:00:00+03:00",
  "sender": {
    "id": 8,
    "name": "Name",
    "image": null
  }
}
```

History: `GET /api/user/chats/{room}/messages?after_id={lastId}&per_page=30`

Mark read: `POST /api/user/chats/{room}/read` with `last_message_id`.

`capabilities.text_only` is `true`. Do not send image, file, voice, map, or video on this API.

## 4. Socket events

### Client emits

| Event | Payload | Notes |
|---|---|---|
| `enterChat` | `socket_token`, `room_id` | Join. No success event. |
| `exitChat` | none | Leave the current room. |
| `sendMessage` | — | Do not use. Server replies `use_laravel_message_api`. |

`socket_token` may also be sent as `token` or `auth_token`. Prefer `socket_token`.

### Server emits (errors only today)

| Event | When |
|---|---|
| `enterChatRes` | Join rejected |
| `sendMessageRes` | Socket write rejected, or (only if direct write is enabled on the server) a payload to the **other** user |

Error shape:

```json
{ "key": "fail", "msg": "socket_auth_required" }
```

| `msg` | Meaning |
|---|---|
| `socket_auth_required` | Missing, bad, or expired `socket_token`. Refresh the token and `enterChat` again. |
| `room_required` | `room_id` was omitted. |
| `use_laravel_message_api` | Send the message with the REST endpoint. |
| `sender_spoof_rejected` | `sender_id` does not match the token. Omit sender fields. |
| `user_type_spoof_rejected` | `sender_type` is not `user`. Omit it. |
| `user_spoof_rejected` | `user_id` does not match the token. Omit it. |

## 5. Incoming messages

Laravel stores the message, then only writes a server log (`chat.message.created`). It does not push that message onto Socket.IO.

Until that push exists, after `enterChat`:

- Show the message from the REST `201` response for the sender.
- For the other user, load `GET /api/user/chats/{room}/messages?after_id=`.
- Do not wait on `sendMessageRes` to append a message.

`sendMessageRes` from the old Node path (disabled) looked like:

```json
{
  "id": 12,
  "sender_id": 8,
  "receiver_id": 9,
  "room_id": "4",
  "body": "hello",
  "type": "text",
  "duration": 0,
  "avatar": "",
  "is_sender": 0,
  "name": "Name",
  "created_at": "04:10 PM"
}
```

That object is not emitted for REST sends. Do not parse it as the live contract.

## 6. Suggested client flow

1. `GET /api/user/chats` and open `room.id`.
2. `GET /api/user/socket-token`.
3. Connect Socket.IO.
4. `enterChat` with `socket_token` and `room_id`.
5. `GET /api/user/chats/{room}/messages` for history.
6. Send with `POST /api/user/chats/{room}/messages`.
7. On the chat screen, poll messages with `after_id` until realtime push is added.
8. On close, `exitChat` then disconnect.
9. Refresh `socket_token` before 300 seconds.

A block in either direction makes send and read-ack return an error. History `GET` still works.
