# NexusWager Dart SDK — Developer Documentation

> **Version:** 1.0.0  
> **Dart SDK Requirement:** `^3.5.4`  
> **License:** Proprietary

---

## Table of Contents

1. [Overview](#overview)
2. [Installation](#installation)
3. [Initialization](#initialization)
4. [Authentication](#authentication)
5. [User Management](#user-management)
6. [Wallet Management](#wallet-management)
7. [Game Management](#game-management)
8. [WebSocket Events](#websocket-events)
9. [Data Models](#data-models)
10. [Error Handling](#error-handling)
11. [Complete API Reference](#complete-api-reference)
12. [End-to-End Examples](#end-to-end-examples)
13. [Best Practices](#best-practices)

---

## Overview

The **NexusWager SDK** is a Dart/Flutter client library that enables game developers to integrate real-money wagering and real-time multiplayer matchmaking into their games. It handles the full lifecycle of a competitive wagering session:

- Authenticating players through a secure two-layer gateway system with automatic token refresh
- Fetching and managing user profiles and multi-currency wallet balances
- Connecting players to a real-time WebSocket matchmaking server
- Supporting public queue matchmaking (solo and team-based), private match challenges, and solo play
- Launching matched game sessions with signed URLs containing match parameters
- Registering completed game sessions for downstream payout processing

The SDK is designed as a **singleton** — one initialized instance is shared throughout the application lifetime. All configuration is passed directly at initialization; no `.env` file is required.

### Architecture Overview

```
NexusWagerSDK (singleton entry point)
│
├── WebSocketClient (sdk.game)       # Real-time matchmaking + game events
│   ├── AuthClient                   # Login, token storage, token refresh
│   │   ├── Gateway                  # Internal gateway token acquisition + refresh
│   │   └── UserClient               # User profile retrieval
│   └── WalletClient                 # Multi-currency wallet balance checks
│
└── SDKContext                       # Shared state container
    ├── ConfigManager                # All SDK config (URLs, keys, credentials)
    ├── HttpClient (Dio)             # HTTP layer with auth interceptors + auto-refresh
    ├── TokenManager                 # In-memory token + login data storage
    ├── UserDataStore                # In-memory user profile + wallet list
    └── Logger                       # Console logging ([NW] prefix)
```

### Key Design Decisions

| Decision | Detail |
|----------|--------|
| **Singleton** | `NexusWagerSDK.initialize()` creates one instance and caches it |
| **Code-first config** | All secrets and URLs are passed as constructor arguments — no `.env` dependency |
| **Auto token refresh** | The HTTP interceptor silently refreshes expired user and gateway tokens on 401 responses |
| **Event-driven** | All matchmaking state changes are communicated via registered callbacks, not return values |
| **Multi-currency wallets** | A player can hold multiple currency wallets; balance checks are always currency-specific |

---

## Installation

Add the package to your `pubspec.yaml`:

```yaml
dependencies:
  nexuswager_sdk:
    path: ./nexuswager_sdk   # local path; replace with pub.dev ref when published
```

Install dependencies:

```bash
dart pub get
# or for Flutter:
flutter pub get
```

### Transitive Dependencies

The SDK pulls in these packages automatically:

| Package | Version | Purpose |
|---------|---------|---------|
| `dio` | `^5.9.2` | HTTP client with interceptor support |
| `web_socket_channel` | `^3.0.3` | WebSocket connection management |

---

## Initialization

The SDK exposes a `NexusWagerSDK.initialize()` factory that creates and caches a singleton. All configuration is supplied as named parameters — there is no dependency on environment files or platform channels.

### Method Signature

```dart
static NexusWagerSDK initialize({
  required String apiKey,
  required String apiSecret,
  required String gatewayUrl,
  required String walletUrl,
  required String sdkApiKey,
  required String gatewayEmail,
  required String gatewayPassword,
  required String websocket,
  required String sdkBackendUrl,
  bool production = false,
})
```

### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `apiKey` | `String` | ✅ | Your application API key |
| `apiSecret` | `String` | ✅ | Your application API secret |
| `gatewayUrl` | `String` | ✅ | Base URL for the gateway service (ignored when `production: true`) |
| `walletUrl` | `String` | ✅ | Base URL for the wallet service (ignored when `production: true`) |
| `sdkApiKey` | `String` | ✅ | Per-request SDK authentication key sent as `SDK-API-KEY` header |
| `gatewayEmail` | `String` | ✅ | Internal gateway service account email |
| `gatewayPassword` | `String` | ✅ | Internal gateway service account password |
| `websocket` | `String` | ✅ | WebSocket server URL (e.g. `wss://ws.example.com`) |
| `sdkBackendUrl` | `String` | ✅ | NexusWager backend URL for game data and session registration |
| `production` | `bool` | ❌ | When `true`, uses hardcoded production URLs instead of provided ones. Default: `false` |

### Return Value

Returns a `NexusWagerSDK` instance. Throws an `Exception` if `apiKey` or `apiSecret` is empty.

### Example

```dart
import 'package:nexuswager_sdk/nexuswager_sdk.dart';

void main() {
  final sdk = NexusWagerSDK.initialize(
    apiKey: 'your_api_key',
    apiSecret: 'your_api_secret',
    gatewayUrl: 'https://gateway.staging.example.com/api/v1',
    walletUrl: 'https://wallet.staging.example.com/api/v1',
    sdkApiKey: 'your_sdk_api_key',
    gatewayEmail: 'service@example.com',
    gatewayPassword: 'service_password',
    websocket: 'wss://ws.staging.example.com',
    sdkBackendUrl: 'https://backend.staging.example.com',
  );

  print('SDK initialized: v${sdk.context.version}');
}
```

### Production Mode

```dart
final sdk = NexusWagerSDK.initialize(
  apiKey: 'your_api_key',
  apiSecret: 'your_api_secret',
  gatewayUrl: '',         // ignored in production mode
  walletUrl: '',          // ignored in production mode
  sdkApiKey: 'your_sdk_api_key',
  gatewayEmail: 'service@example.com',
  gatewayPassword: 'service_password',
  websocket: 'wss://ws.nexuswager.com',
  sdkBackendUrl: 'https://backend.nexuswager.com',
  production: true,       // switches to hardcoded production URLs
);
```

### Singleton Behavior

Calling `initialize()` more than once silently returns the existing instance:

```dart
final sdk1 = NexusWagerSDK.initialize(/* ... */);
final sdk2 = NexusWagerSDK.initialize(/* ... */);
assert(identical(sdk1, sdk2)); // true — same instance

// Access anywhere without re-initializing:
final sdk = NexusWagerSDK.instance!;
```

---

## Authentication

### Overview

Authentication uses a **two-layer flow**. Both layers are handled automatically when you call `connect()` or `authenticateUser()`.

```
Developer App
    │
    ▼
[1] Gateway Login  (automatic, SDK-internal)
    POST /nexus-wager/auth/login
    ← gatewayToken stored in TokenManager
    │
    ▼
[2] User Login
    POST /users/auth/login  ← email + password supplied by you
    ← userToken stored in TokenManager
    │
    ▼
[3] Profile Fetch  (automatic, post-login)
    GET /users/account/me
    ← profile stored in UserDataStore
```

### Automatic Token Refresh

The `HttpClient` interceptor watches for `401` responses and silently refreshes the appropriate token before retrying the original request:

| Request path contains | Token refreshed | Method called |
|-----------------------|-----------------|---------------|
| `/users/` | User token | `auth.refreshUserToken()` |
| `/nexus-wager/` | Gateway token | `gateway.refreshGatewayToken()` |

You do not need to handle token expiry manually.

---

### `connect(data)` — Primary Authentication Entry Point

`sdk.game.connect()` is the recommended way to authenticate. It runs the full two-layer auth flow, then establishes the WebSocket connection and identifies the player on the socket server.

See [Game Management — connect()](#gameconnectdata) for the full reference.

---

### `authenticateUser(login)` — Standalone Authentication

Available on `sdk.context.auth` if you need to authenticate without opening a WebSocket connection.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `login` | `Map<String, dynamic>` | ✅ | Must contain `email` and `password` |

#### Return Value

**Success:**
```json
{
  "success": true,
  "message": "User Authenticated"
}
```

**Failure:**
```json
{
  "success": false,
  "errorMessage": "Authentication failed",
  "status": 401,
  "message": "Invalid credentials",
  "response": { }
}
```

#### Side Effects

- Saves `login` credentials in `TokenManager` for later use by `refreshUserToken()`
- Stores the user token via `TokenManager.set()`
- Fetches and stores the user profile in `UserDataStore`

#### Example

```dart
final result = await sdk.context.auth.authenticateUser({
  "email": "player@example.com",
  "password": "s3cur3P@ss",
});

if (result["success"]) {
  print("Authenticated!");
} else {
  print("Failed: ${result["message"]}");
}
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `AUTH_FAILED` | Credentials rejected or network error |
| `GATEWAY_ACCESS_ERROR` | Internal gateway login failed |
| `USER_ERROR` | Profile fetch failed after successful login |

---

### `refreshUserToken()`

Re-authenticates the user using credentials saved during the last `authenticateUser()` call. Called automatically by the HTTP interceptor on 401 errors; you rarely need to call this directly.

```dart
await sdk.context.auth.refreshUserToken();
```

Throws `Exception("User not authenticated")` if no login data has been saved.

---

### `requireAuth()`

Checks whether a user is currently authenticated by verifying the stored profile has a non-empty `unique_id`.

```dart
final check = sdk.context.requireAuth();

if (!check["success"]) {
  // Redirect to login screen
  print(check["message"]); // "User must be authenticated before using this service"
}
```

**Returns:**

| `success` | `message` |
|-----------|-----------|
| `true` | `"User Authenticated"` |
| `false` | `"User must be authenticated before using this service"` |

---

## User Management

### `getProfile()`

Fetches the authenticated user's profile and stores it in `UserDataStore`. Called automatically after `authenticateUser()`, but can be called again to refresh data.

#### Parameters

None.

#### Return Value

**Success:**
```json
{
  "success": true,
  "message": "User information retrieved"
}
```

#### Example

```dart
final result = await sdk.context.auth.users.getProfile();

if (result["success"]) {
  final profile = sdk.context.userDataStore.getProfileData();
  print("${profile["name"]} — ${profile["player_username"]}");
}
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `USER_ERROR` | Profile endpoint returned an error |

---

### Accessing Stored Profile Data

```dart
final profile = sdk.context.userDataStore.getProfileData();

print(profile["unique_id"]);       // String — platform-wide player UUID
print(profile["name"]);            // String — full display name
print(profile["player_username"]); // String — in-game username
print(profile["email"]);           // String
print(profile["kyc_status"]);      // String
print(profile["has_pin"]);         // num — 1 if PIN is set
print(profile["enable2FA"]);       // num — 1 if 2FA is enabled
```

> **Note:** The username field in this SDK version is `player_username`, not `username`. The SDK reads `player_username` when building socket identity messages.

---

## Wallet Management

### `getBalance(currency)`

Fetches all wallets for the authenticated user, stores the full list, then returns the `Wallet` object matching the requested currency.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `currency` | `String` | ✅ | Currency code to look up (e.g., `"NXC"`, `"NGN"`) |

#### Return Value

**Success:**
```dart
{
  "success": true,
  "walletDetails": Wallet,  // Wallet object for the requested currency
}
```

If no wallet exists for the requested currency, a zero-balance placeholder `Wallet` is returned (not an error).

**Failure:**
```json
{
  "success": false,
  "errorMessage": "User wallet request failed",
  "status": "...",
  "message": "...",
  "response": { }
}
```

#### Example

```dart
final result = await sdk.game.wallet.getBalance("NXC");

if (result["success"]) {
  final Wallet wallet = result["walletDetails"];
  print("${wallet.walletCurrency}: ${wallet.walletBalance}");
  print("Locked: ${wallet.lockedBalance}");
  print("Frozen: ${wallet.isFreeze}");
}
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `WALLET_ERROR` | Could not reach the wallet service |

---

### Accessing All Stored Wallets

```dart
final List<Wallet> wallets = sdk.context.userDataStore.getWalletBalance();

for (final wallet in wallets) {
  print("${wallet.walletCurrency}: ${wallet.walletBalance}");
}
```

---

## Game Management

All game management flows through `sdk.game` (the `WebSocketClient`). The SDK follows an event-driven model: you register callbacks, call `connect()`, then call queue or match methods. Responses arrive asynchronously via the registered event handlers.

### Lifecycle Overview

```
register callbacks
      │
      ▼
  connect()              ← authenticates + opens socket
      │
      ▼
joinQueue() / soloPlay() / sendPrivateMatchRequest()
      │
      ▼ (async, via socket events)
onMatchProposal → acceptMatch() / declineMatch()
      │
      ▼
onMatchFound → (internal) → onLaunchGame
      │
      ▼
Game session runs
      │
      ▼
disconnect()
```

---

### `connect(data)`

Authenticates the user and establishes the WebSocket connection in a single call. After authentication, the player's identity (`playerId` + `username`) is sent to the socket server via an `identify` event.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `data` | `Map<String, dynamic>` | ✅ | Must include `email` and `password` |

#### Return Value

`Future<void>`. Sets status to `"error"` and calls `errorCallback` if authentication fails; otherwise resolves when the `identify` event is confirmed by the server.

#### Example

```dart
await sdk.game.connect({
  "email": "player@example.com",
  "password": "s3cur3P@ss",
});
```

#### Connection Status Callback

In addition to `onStatusChange`, use `onConnectStatus` to track raw connection events:

```dart
sdk.game.onConnectStatus((String status) {
  // "Connected" or "Disconnected"
  print("Socket: $status");
});
```

---

### `disconnect()`

Closes the WebSocket connection. Sets status to `"idle"` and fires the connection status callback with `"Disconnected"`.

```dart
sdk.game.disconnect();
```

Always call this when leaving a matchmaking screen to release the socket cleanly.

---

### `joinQueue(data)`

Adds the player to the public matchmaking queue after verifying they have sufficient unfrozen balance for the chosen currency.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `data` | `Map<String, dynamic>` | ✅ | See fields below |

**`data` fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gameId` | `String` | ✅ | Game identifier |
| `stake` | `num` | ✅ | Wager amount |
| `currency` | `String` | ✅ | Currency code (e.g., `"NXC"`) |
| `teamSize` | `num` | ✅ | Number of players per team (`1` for 1v1) |
| `teamId` | `String` | ❌ | Optional team identifier for pre-formed teams |

#### Return Value

**Success:**
```json
{
  "success": true,
  "message": "You have joined game successfuly"
}
```

**Failure (insufficient balance or frozen wallet):**
```json
{
  "success": false,
  "message": "Insufficient balance. You need NXC 200",
  "errorMessage": "Failed to stake"
}
```

#### Example

```dart
// 1v1 queue
final result = await sdk.game.joinQueue({
  "gameId": "game_abc123",
  "stake": 500,
  "currency": "NXC",
  "teamSize": 1,
});

// Team queue (2v2)
final result = await sdk.game.joinQueue({
  "gameId": "game_abc123",
  "stake": 500,
  "currency": "NXC",
  "teamSize": 2,
  "teamId": "MY_TEAM",
});

if (!result["success"]) {
  print(result["message"]);
}
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `STAKE_ERROR` | Balance below stake or wallet is frozen |
| `JOIN_QUEUE` | General failure adding to queue |
| `WALLET_ERROR` | Could not fetch balance to validate |

---

### `soloPlay(gameId)`

Starts a non-competitive solo play session without entering the matchmaking queue. No balance check is performed.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `gameId` | `String` | ✅ | Game identifier |

#### Return Value

```json
{
  "success": true,
  "message": "Solo match started"
}
```

#### Example

```dart
final result = await sdk.game.soloPlay("game_abc123");
```

---

### `acceptMatch(matchId)`

Accepts a match proposal from the matchmaking system. Sends an `accept_match` event with the player's identity.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `matchId` | `String` | ✅ | Match identifier from the `match_proposal` payload |

#### Example

```dart
sdk.game.onMatchProposal((payload) {
  sdk.game.acceptMatch(payload["matchId"]);
});
```

---

### `declineMatch(matchId)`

Declines a match proposal. The player remains in the queue and status resets to `"searching"`.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `matchId` | `String` | ✅ | Match identifier |

#### Example

```dart
sdk.game.declineMatch("match_xyz789");
```

---

### `sendPrivateMatchRequest(data)`

Sends a private team match challenge to specific named players. The sender's username is automatically appended to `challengerTeam` by the SDK.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `data` | `Map<String, dynamic>` | ✅ | See fields below |

**`data` fields:**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `gameId` | `String` | ✅ | Game identifier |
| `stake` | `num` | ✅ | Wager amount |
| `currency` | `String` | ✅ | Currency code |
| `teamSize` | `num` | ✅ | Number of players per team |
| `challengerTeam` | `List<String>` | ✅ | Usernames on the challenger's team (sender added automatically) |
| `opponentTeam` | `List<String>` | ✅ | Usernames of the opponent team |

#### Return Value

```json
{
  "success": true,
  "message": "Private match request sent"
}
```

#### Example

```dart
// 1v1
final result = await sdk.game.sendPrivateMatchRequest({
  "gameId": "game_abc123",
  "stake": 400,
  "currency": "NXC",
  "teamSize": 1,
  "challengerTeam": [],        // sender is added automatically
  "opponentTeam": ["Magnifico"],
});

// 2v2 team match
final result = await sdk.game.sendPrivateMatchRequest({
  "gameId": "game_abc123",
  "stake": 400,
  "currency": "NXC",
  "teamSize": 2,
  "challengerTeam": ["Nitaaa"],   // sender + Nitaaa
  "opponentTeam": ["Magnifico", "Vozinha"],
});
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `STAKE_ERROR` | Balance below stake or wallet is frozen |
| `INVITE_PLAYER` | Challenge could not be sent |

---

### `acceptPrivateMatch(requestId, stake, currency)`

Accepts an incoming private match challenge after validating the player's balance.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `requestId` | `String` | ✅ | Request ID from the `private_match_incoming` payload |
| `stake` | `int` | ✅ | Stake amount to validate against (from the incoming payload) |
| `currency` | `String` | ✅ | Currency code (from the incoming payload) |

#### Return Value

```json
{
  "success": true,
  "message": "Private match request sent"
}
```

#### Example

```dart
sdk.game.onPrivateMatchIncoming((payload) async {
  final result = await sdk.game.acceptPrivateMatch(
    payload["requestId"],
    payload["stake"],
    payload["currency"],
  );

  if (!result["success"]) {
    print(result["message"]);
  }
});
```

#### Possible Errors

| Error Code | Meaning |
|------------|---------|
| `STAKE_ERROR` | Insufficient balance or wallet frozen |
| `ACCEPT_INVITE` | General acceptance failure |

---

### `declinePrivateTeamMatch(requestId)`

Declines an incoming private match challenge. Sends a `private_match_declined` event with the player's identity.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `requestId` | `String` | ✅ | Request ID from the incoming challenge |

#### Example

```dart
sdk.game.declinePrivateTeamMatch("req_001");
```

---

### `cancelPrivateTeamMatch(requestId)`

Cancels an outgoing private match challenge that has not yet been accepted. Sends a `private_match_cancel` event with the challenger's identity.

#### Parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `requestId` | `String` | ✅ | Request ID of the outgoing challenge |

#### Example

```dart
sdk.game.cancelPrivateTeamMatch("req_001");
```

---

## WebSocket Events

The `WebSocketClient` maintains an internal `Map<String, Function>` of event listeners. Every `on*` convenience method is a thin wrapper around `on(eventName, callback)`. Callbacks receive the raw decoded JSON payload as `dynamic`.

### Matchmaking Status Values

The `onStatusChange` callback is called whenever the internal status changes:

| Status | Triggered By |
|--------|-------------|
| `"idle"` | `disconnect()` or socket closed |
| `"searching"` | `joinQueue()` succeeds |
| `"confirming"` | Server sends `match_proposal` |
| `"matched"` | Match confirmed and game URL fetched |
| `"error"` | Any failure in socket or game flow |
| `"game_over"` | Game session ended (set by server event) |

---

### Global Callbacks

These two callbacks are always recommended:

```dart
sdk.game.onStatusChange((String status) {
  // Drive UI state machine from here
});

sdk.game.onErrorMessage((String message) {
  // Show error dialog or toast
});

sdk.game.onConnectStatus((String status) {
  // "Connected" or "Disconnected"
});
```

---

### Full Event Reference

| Method | Server Event | Status Change | Description |
|--------|-------------|---------------|-------------|
| `onIdentified(cb)` | `identify` | — | Server confirmed player identity |
| `onMatchProposal(cb)` | `match_proposal` | → `confirming` | Opponent found; awaiting acceptance |
| `onMatchDeclined(cb)` | `match_declined` | → `searching` | Opponent declined; back in queue |
| `onMatchFound(cb)` | `match_found` | → `matched` | Match locked; triggers game launch |
| `onMatchStarted(cb)` | `match_started` | — | Both players connected; game is live |
| `onPlayerJoined(cb)` | `player_joined` | — | Opponent joined the lobby |
| `onProposalAccepted(cb)` | `proposal_accepted` | — | Your acceptance was acknowledged |
| `onOpponentAccepted(cb)` | `opponent_accepted` | — | Opponent accepted the proposal |
| `onPrivateMatchIncoming(cb)` | `private_match_incoming` | — | Received a private challenge |
| `onPrivateMatchSent(cb)` | `private_match_sent` | — | Challenge delivery confirmed |
| `onPrivateMatchAccepted(cb)` | `private_match_accepted` | → `matched` | Opponent accepted; triggers game launch |
| `onPrivateMatchDeclined(cb)` | `private_match_declined` | — | Opponent declined the challenge |
| `onPrivateMatchNotFound(cb)` | `private_match_not_found` | — | Target player not found |
| `onPrivateMatchCancel(cb)` | `private_match_cancel` | — | Challenger cancelled the challenge |
| `onPrivateMatchTimeout(cb)` | `private_match_timeout` | — | Challenge expired before response |
| `onTeamForming(cb)` | `team_forming` | — | Team is being assembled |
| `onTeamComplete(cb)` | `team_complete` | — | All team slots are filled |
| `onTeamPlayerLeft(cb)` | `team_player_left` | — | A team member disconnected |
| `onLaunchGame(cb)` | `launch_game` | — | Game URL ready; load the game |

---

### `onLaunchGame` Payload Structure

```dart
sdk.game.onLaunchGame((Map<String, dynamic> data) {
  final String gameUrl = data["url"];
  // URL already contains matchId and gameId as query parameters

  final Map matchData = data["matchData"];
  final String matchId = matchData["matchId"];
  final dynamic player = matchData["player"]; // the "me" object from match payload

  // Open WebView or navigate to game URL
  openGameView(gameUrl);
});
```

The game URL is constructed by appending `matchId` and `gameId` as query parameters to the URL returned by the SDK backend:

```
https://game.example.com/play?existingParam=x&matchId=match_xyz&gameId=game_abc
```

---

### Low-Level Event Listener

Register a handler for any socket event, including server-custom events:

```dart
sdk.game.on("custom_server_event", (dynamic payload) {
  print(payload);
});
```

---

## Data Models

### `NexusWagerSDK`

| Property | Type | Description |
|----------|------|-------------|
| `game` | `WebSocketClient` | All matchmaking, socket, and game methods |
| `context` | `SDKContext` | Shared SDK state and sub-clients |
| `instance` | `NexusWagerSDK?` | Static singleton reference |

---

### `SDKContext`

| Property | Type | Description |
|----------|------|-------------|
| `config` | `ConfigManager` | All SDK configuration |
| `tokens` | `TokenManager` | In-memory token and login data storage |
| `userDataStore` | `UserDataStore` | In-memory profile and wallet data |
| `http` | `HttpClient` | Dio HTTP client with interceptors |
| `logger` | `Logger` | Console logger |
| `auth` | `AuthClient` | Authentication client |
| `gateway` | `Gateway` | Gateway authentication client |
| `version` | `String` | SDK version string (e.g., `"1.0.0"`) |

---

### `ConfigManager`

| Property | Type | Description |
|----------|------|-------------|
| `apiKey` | `String` | Application API key |
| `apiSecret` | `String` | Application API secret |
| `baseUrl` | `Map<String, String>` | `{"gateway": "...", "wallet": "..."}` |
| `sdkApiKey` | `String` | SDK API key for `SDK-API-KEY` header |
| `gatewayEmail` | `String` | Gateway service account email |
| `gatewayPassword` | `String` | Gateway service account password |
| `websocket` | `String` | WebSocket server URL |
| `sdkBackendUrl` | `String` | NexusWager backend base URL |

---

### `Wallet`

The strongly-typed model for a single currency wallet. Returned by `getBalance()` and stored in `UserDataStore`.

| Property | Type | Description |
|----------|------|-------------|
| `walletId` | `String` | Unique wallet identifier |
| `walletCurrency` | `String` | Currency code (e.g., `"NXC"`, `"NGN"`) |
| `walletBalance` | `double` | Available (spendable) balance |
| `lockedBalance` | `double` | Balance locked in escrow or pending transactions |
| `coinAlias` | `String` | Display name for the currency |
| `isFreeze` | `bool` | `true` if the wallet is frozen and cannot be used |

**Constructor:**
```dart
Wallet({
  required String walletId,
  required String walletCurrency,
  required double walletBalance,
  required double lockedBalance,
  required String coinAlias,
  required bool isFreeze,
})
```

**Factory:**
```dart
final wallet = Wallet.fromJson(jsonMap);
```

**Serialization:**
```dart
final json = wallet.toJson();
// {
//   "walletId": "...",
//   "wallet_currency": "NXC",
//   "wallet_balance": 1500.0,
//   "locked_balance": 200.0,
//   "coin_alias": "NexusCoin",
//   "isFreeze": false,
// }
```

---

### `UserDataStore` — Profile Fields

| Field | Type | Description |
|-------|------|-------------|
| `id` | `num` | Internal numeric user ID |
| `unique_id` | `String` | Platform-wide UUID |
| `name` | `String` | Full display name |
| `email` | `String` | Email address |
| `player_username` | `String` | In-game username (used for socket identity) |
| `profile_picture` | `dynamic` | URL string or `null` |
| `phone_number` | `String` | Phone number |
| `status` | `String` | Account status |
| `last_login_date` | `String` | ISO date string |
| `joined_date` | `String` | ISO date string |
| `access_level` | `String` | Permission tier |
| `has_pin` | `num` | `1` = PIN set |
| `kyc_status` | `String` | KYC verification status |
| `kyc_doc_type` | `dynamic` | Document type or `null` |
| `wallet_balance` | `String` | Legacy balance field (use `Wallet.walletBalance` instead) |
| `enable2FA` | `num` | `1` = 2FA active |
| `canReceiveEmail` | `num` | Email notification preference |
| `canPushNotification` | `num` | Push notification preference |
| `canReceiveWeeklyReport` | `num` | Weekly report preference |
| `canReceivePlayerFeedback` | `num` | Player feedback preference |
| `canReceiveEarningUpdate` | `num` | Earnings update preference |
| `push_notification_id` | `dynamic` | Push token or `null` |
| `last_logged_ip` | `String` | Last login IP address |
| `loginAttempts` | `dynamic` | Failed attempt count or `null` |

---

### `TokenManager`

| Method | Return | Description |
|--------|--------|-------------|
| `set(String token)` | `void` | Store user session token |
| `get()` | `String?` | Retrieve user token |
| `setGatewayToken(String token)` | `void` | Store gateway bearer token |
| `getGatewayToken()` | `String?` | Retrieve gateway token |
| `clearGatewayToken()` | `void` | Remove gateway token |
| `saveLoginData(Map<String, dynamic>)` | `void` | Save login credentials for token refresh |
| `getLoginData()` | `Map<String, dynamic>?` | Retrieve saved login credentials |
| `clearAll()` | `void` | Clear all tokens and login data |

---

### `MatchmakingStatus` Constants

```dart
MatchmakingStatus.idle        // "idle"
MatchmakingStatus.searching   // "searching"
MatchmakingStatus.confirming  // "confirming"
MatchmakingStatus.matched     // "matched"
MatchmakingStatus.error       // "error"
MatchmakingStatus.game_over   // "game_over"
```

---

### Type Definitions (`lib/src/types/index.dart`)

These types are available for use in consumer code.

#### `LoginCredentials`
| Field | Type |
|-------|------|
| `email` | `String` |
| `password` | `String` |

#### `WagerRequest`
| Field | Type |
|-------|------|
| `game` | `String` |
| `amount` | `num` |

#### `Wager`
| Field | Type |
|-------|------|
| `id` | `String` |
| `game` | `String` |
| `amount` | `num` |
| `status` | `String` |

#### `WalletBalance`
| Field | Type |
|-------|------|
| `currency` | `String` |
| `balance` | `num` |

#### `WalletTransaction`
| Field | Type |
|-------|------|
| `id` | `String` |
| `amount` | `num` |
| `type` | `String` |
| `reference` | `String` |
| `createdAt` | `String` |

#### `GameData`
| Field | Type |
|-------|------|
| `gameName` | `String` |
| `gameId` | `String?` |
| `developerId` | `String?` |
| `gameSessionId` | `String?` |

#### `Stake`
| Field | Type |
|-------|------|
| `playerId` | `String` |
| `username` | `String` |
| `gameId` | `String` |
| `amount` | `num` |

#### `QUEUE_DATA`
| Field | Type |
|-------|------|
| `gameId` | `String` |
| `stake` | `num` |
| `currency` | `String` |

#### `GAME_RESULTS_ITEM`
| Field | Type |
|-------|------|
| `matchId` | `String` |
| `winnerId` | `String` |
| `loserId` | `String` |
| `isDraw` | `bool` |
| `outcomeReason` | `String` |
| `startedAt` | `String` |
| `endedAt` | `String` |
| `durationMs` | `num` |
| `roundsPlayed` | `String?` |
| `players` | `List<GameResultPlayer>` |
| `additionalGameData` | `Map<String, dynamic>` |

#### `GameResultPlayer`
| Field | Type |
|-------|------|
| `id` | `String` |
| `username` | `String` |
| `isWinner` | `bool` |
| `score` | `num` |
| `stats` | `Map<String, dynamic>` |

---

## Error Handling

All public async methods return `Map<String, dynamic>` with a `"success"` key rather than throwing (except socket operations, which may throw for unconnected socket calls). Always check `result["success"]` before reading other fields.

### Standard Error Response Shape

```dart
{
  "success": false,
  "errorMessage": "...",  // ErrorCodes enum value string
  "status": ...,          // HTTP status code (int or empty string)
  "message": "...",       // Server-provided message
  "response": { ... },    // Raw response body
}
```

### Error Code Reference

| Enum Constant | Value | When Triggered |
|---------------|-------|----------------|
| `AUTH_FAILED` | `"Authentication failed"` | User login rejected or network error |
| `GATEWAY_ACCESS_ERROR` | `"Gateway access denied"` | Internal gateway login failed |
| `INVALID_TOKEN` | `"Invalid token"` | Token validation failure |
| `NETWORK_ERROR` | `"Network error"` | General connectivity failure |
| `CONFIG_ERROR` | `"Configuration error"` | Invalid or missing SDK config |
| `UNKNOWN_ERROR` | `"Unknown error"` | Unhandled exception |
| `USER_ERROR` | `"Failed to get user info"` | Profile endpoint error |
| `WALLET_ERROR` | `"User wallet request failed"` | Wallet service error |
| `ESCROW_ERROR` | `"Escrow requests failed"` | Escrow operation failure (reserved) |
| `WAGER_ERROR` | `"Failed wager request"` | Generic wager failure |
| `SEARCH_ERROR` | `"Failed to search for opponent"` | Matchmaking search failure |
| `GAME_INITIALIZATION` | `"Failed to initialize game"` | Game setup error |
| `GAME_UPLOAD` | `"Failed to upload game"` | Game upload error |
| `FILE_UPLOAD` | `"File failed to upload"` | File upload failure |
| `STAKE_ERROR` | `"Failed to stake"` | Insufficient balance or frozen wallet |
| `JOIN_QUEUE` | `"Failed to join queue"` | Could not add player to queue |
| `GET_GAME_INFO` | `"Failed to get game information"` | Game data retrieval failure |
| `INVITE_PLAYER` | `"Failed to invite player"` | Private match invitation failed |
| `ACCEPT_INVITE` | `"Failed to accept invite"` | Private match acceptance failed |

### HTTP-Level Behavior

| HTTP Status | SDK Behavior |
|-------------|-------------|
| `200` | Success — returns parsed response body |
| `401` | Triggers automatic token refresh + retry (via interceptor) |
| `403` | Returns the string `"Invalid SDK API Key"` |
| Other non-200 | Throws `Exception('HTTP <statusCode>')` |

### Initialization Exceptions

| Exception Message | Cause |
|-------------------|-------|
| `"Missing SDK credentials"` | `apiKey` or `apiSecret` is an empty string |

### Frozen Wallet Behavior

When a wallet's `isFreeze` is `true`, `joinQueue()`, `sendPrivateMatchRequest()`, and `acceptPrivateMatch()` all reject the operation and call `errorCallback` with `"Insufficient balance. You need <currency> <amount>"`, treating a frozen wallet identically to an insufficient balance. Check `wallet.isFreeze` directly if you need to distinguish the two cases.

---

## Complete API Reference

### `NexusWagerSDK`

| Method / Property | Return | Description |
|-------------------|--------|-------------|
| `NexusWagerSDK.initialize({...})` | `NexusWagerSDK` | Create or retrieve singleton |
| `game` | `WebSocketClient` | Game and matchmaking client |
| `context` | `SDKContext` | Shared SDK state |
| `instance` | `NexusWagerSDK?` | Direct singleton accessor |

---

### `WebSocketClient` (`sdk.game`)

| Method | Return | Description |
|--------|--------|-------------|
| `connect(data)` | `Future<void>` | Authenticate + open socket |
| `disconnect()` | `void` | Close socket |
| `joinQueue(data)` | `Future<Map>` | Join public matchmaking queue |
| `soloPlay(String gameId)` | `Future<Map>` | Start solo session |
| `acceptMatch(String matchId)` | `void` | Accept match proposal |
| `declineMatch(String matchId)` | `void` | Decline match proposal |
| `sendPrivateMatchRequest(data)` | `Future<Map>` | Challenge specific players |
| `acceptPrivateMatch(requestId, stake, currency)` | `Future<Map>` | Accept incoming challenge |
| `declinePrivateTeamMatch(String requestId)` | `void` | Decline incoming challenge |
| `cancelPrivateTeamMatch(String requestId)` | `void` | Cancel outgoing challenge |
| `onStatusChange(Function(String))` | `void` | Register status change handler |
| `onErrorMessage(Function(String))` | `void` | Register error handler |
| `onConnectStatus(Function(String))` | `void` | Register connection status handler |
| `onLaunchGame(Function(Map))` | `void` | Register game launch handler |
| `onIdentified(Function)` | `void` | Socket identity confirmed |
| `onMatchProposal(Function)` | `void` | Match proposal received |
| `onMatchDeclined(Function)` | `void` | Match declined by opponent |
| `onMatchFound(Function)` | `void` | Match confirmed |
| `onMatchStarted(Function)` | `void` | Game started |
| `onPlayerJoined(Function)` | `void` | Opponent joined lobby |
| `onProposalAccepted(Function)` | `void` | Acceptance acknowledged |
| `onOpponentAccepted(Function)` | `void` | Opponent accepted |
| `onPrivateMatchIncoming(Function)` | `void` | Incoming private challenge |
| `onPrivateMatchSent(Function)` | `void` | Challenge sent confirmation |
| `onPrivateMatchAccepted(Function)` | `void` | Challenge accepted |
| `onPrivateMatchDeclined(Function)` | `void` | Challenge declined |
| `onPrivateMatchNotFound(Function)` | `void` | Target player not found |
| `onPrivateMatchCancel(Function)` | `void` | Challenge cancelled |
| `onPrivateMatchTimeout(Function)` | `void` | Challenge timed out |
| `onTeamForming(Function)` | `void` | Team assembly started |
| `onTeamComplete(Function)` | `void` | Team fully assembled |
| `onTeamPlayerLeft(Function)` | `void` | Team member disconnected |
| `on(String event, Function)` | `void` | Register any event listener |

---

### `AuthClient` (`sdk.context.auth`)

| Method | Return | Description |
|--------|--------|-------------|
| `authenticateUser(Map login)` | `Future<dynamic>` | Full two-layer auth flow |
| `refreshUserToken()` | `Future<void>` | Re-authenticate with saved credentials |

---

### `UserClient` (`sdk.context.auth.users`)

| Method | Return | Description |
|--------|--------|-------------|
| `getProfile()` | `Future<dynamic>` | Fetch and store user profile |

---

### `WalletClient` (`sdk.game.wallet`)

| Method | Return | Description |
|--------|--------|-------------|
| `getBalance(String currency)` | `Future<dynamic>` | Fetch all wallets; return specific currency |

---

### `SDKContext` (`sdk.context`)

| Method | Return | Description |
|--------|--------|-------------|
| `requireAuth()` | `dynamic` | Check if user is authenticated |

---

### `Gateway` (`sdk.context.gateway`)

| Method | Return | Description |
|--------|--------|-------------|
| `gatewayAccess()` | `Future<dynamic>` | Obtain gateway token (called internally) |
| `refreshGatewayToken()` | `Future<void>` | Re-obtain gateway token (called by interceptor) |

---

### `TokenManager` (`sdk.context.tokens`)

| Method | Return | Description |
|--------|--------|-------------|
| `set(String)` | `void` | Store user token |
| `get()` | `String?` | Retrieve user token |
| `setGatewayToken(String)` | `void` | Store gateway token |
| `getGatewayToken()` | `String?` | Retrieve gateway token |
| `clearGatewayToken()` | `void` | Remove gateway token |
| `saveLoginData(Map)` | `void` | Persist login credentials |
| `getLoginData()` | `Map?` | Retrieve login credentials |
| `clearAll()` | `void` | Clear everything |

---

## End-to-End Examples

### Example 1: Login and Display Profile

```dart
import 'package:nexuswager_sdk/nexuswager_sdk.dart';

Future<void> main() async {
  final sdk = NexusWagerSDK.initialize(
    apiKey: 'your_api_key',
    apiSecret: 'your_api_secret',
    gatewayUrl: 'https://gateway.staging.example.com/api/v1',
    walletUrl: 'https://wallet.staging.example.com/api/v1',
    sdkApiKey: 'your_sdk_api_key',
    gatewayEmail: 'service@example.com',
    gatewayPassword: 'service_password',
    websocket: 'wss://ws.staging.example.com',
    sdkBackendUrl: 'https://backend.staging.example.com',
  );

  final result = await sdk.context.auth.authenticateUser({
    "email": "player@example.com",
    "password": "s3cur3P@ss",
  });

  if (!result["success"]) {
    print("Login failed: ${result["message"]}");
    return;
  }

  final profile = sdk.context.userDataStore.getProfileData();
  print("Welcome, ${profile["name"]}");
  print("Username: ${profile["player_username"]}");
  print("KYC: ${profile["kyc_status"]}");
}
```

---

### Example 2: Fetching Wallet Balance

```dart
Future<void> checkWallet(NexusWagerSDK sdk) async {
  final result = await sdk.game.wallet.getBalance("NXC");

  if (result["success"]) {
    final Wallet wallet = result["walletDetails"];
    print("${wallet.walletCurrency}: ${wallet.walletBalance}");
    print("Locked: ${wallet.lockedBalance}");
    print("Frozen: ${wallet.isFreeze}");
  } else {
    print("Error: ${result["errorMessage"]}");
  }
}
```

---

### Example 3: Public 1v1 Matchmaking

```dart
Future<void> joinPublicQueue(NexusWagerSDK sdk) async {
  // Step 1: Register all event handlers BEFORE connecting
  sdk.game.onConnectStatus((s) => print("[CONN] $s"));
  sdk.game.onStatusChange((s) => print("[STATUS] $s"));
  sdk.game.onErrorMessage((e) => print("[ERROR] $e"));

  sdk.game.onIdentified((data) {
    print("Identified: ${data["message"]}");
  });

  sdk.game.onMatchProposal((payload) {
    print("Opponent found: ${payload["matchId"]}");
    // Accept automatically, or show a UI prompt
    sdk.game.acceptMatch(payload["matchId"]);
  });

  sdk.game.onMatchDeclined((payload) {
    print("Opponent declined, still searching...");
  });

  sdk.game.onLaunchGame((data) {
    print("Launch game: ${data["url"]}");
    // Open WebView with data["url"]
  });

  // Step 2: Connect (authenticates + opens socket)
  await sdk.game.connect({
    "email": "player@example.com",
    "password": "s3cur3P@ss",
  });

  // Step 3: Join queue
  final result = await sdk.game.joinQueue({
    "gameId": "game_abc123",
    "stake": 500,
    "currency": "NXC",
    "teamSize": 1,
  });

  if (!result["success"]) {
    print("Queue failed: ${result["message"]}");
  }
}
```

---

### Example 4: Team Queue (2v2)

```dart
Future<void> joinTeamQueue(NexusWagerSDK sdk) async {
  sdk.game.onStatusChange((s) => print("[STATUS] $s"));
  sdk.game.onErrorMessage((e) => print("[ERROR] $e"));
  sdk.game.onTeamForming((data) => print("Team forming: $data"));
  sdk.game.onTeamComplete((data) => print("Team complete: $data"));
  sdk.game.onLaunchGame((data) => openGame(data["url"]));

  await sdk.game.connect({
    "email": "player@example.com",
    "password": "s3cur3P@ss",
  });

  await sdk.game.joinQueue({
    "gameId": "game_abc123",
    "stake": 400,
    "currency": "NXC",
    "teamSize": 2,
    "teamId": "SQUAD_ALPHA",
  });
}

void openGame(String url) {
  print("Opening: $url");
}
```

---

### Example 5: Private Match — Challenger Side

```dart
Future<void> sendChallenge(NexusWagerSDK sdk) async {
  sdk.game.onStatusChange((s) => print("[STATUS] $s"));
  sdk.game.onErrorMessage((e) => print("[ERROR] $e"));
  sdk.game.onPrivateMatchSent((data) => print("Challenge sent: $data"));
  sdk.game.onPrivateMatchDeclined((data) => print("Challenge declined"));
  sdk.game.onPrivateMatchTimeout((data) => print("Challenge timed out"));
  sdk.game.onLaunchGame((data) => openGame(data["url"]));

  await sdk.game.connect({
    "email": "challenger@example.com",
    "password": "pass123",
  });

  final result = await sdk.game.sendPrivateMatchRequest({
    "gameId": "game_abc123",
    "stake": 400,
    "currency": "NXC",
    "teamSize": 2,
    "challengerTeam": ["Nitaaa"],       // sender added automatically
    "opponentTeam": ["Magnifico", "Vozinha"],
  });

  if (!result["success"]) {
    print("Challenge failed: ${result["message"]}");
  }
}
```

---

### Example 6: Private Match — Challenged Side

```dart
Future<void> receiveChallenge(NexusWagerSDK sdk) async {
  sdk.game.onStatusChange((s) => print("[STATUS] $s"));
  sdk.game.onErrorMessage((e) => print("[ERROR] $e"));
  sdk.game.onLaunchGame((data) => openGame(data["url"]));

  sdk.game.onPrivateMatchIncoming((payload) async {
    print("Challenge from: ${payload["challengerUsername"]}");
    print("Stake: ${payload["currency"]} ${payload["stake"]}");

    // Accept after a UI confirmation delay
    Future.delayed(const Duration(seconds: 5), () async {
      final result = await sdk.game.acceptPrivateMatch(
        payload["requestId"],
        payload["stake"] as int,
        payload["currency"],
      );
      if (!result["success"]) {
        print("Could not accept: ${result["message"]}");
      }
    });
  });

  await sdk.game.connect({
    "email": "opponent@example.com",
    "password": "pass456",
  });
}
```

---

### Example 7: Accepting a Match with Balance Check

```dart
Future<void> joinWithCheck(NexusWagerSDK sdk) async {
  // Manually check balance before joining
  final walletResult = await sdk.game.wallet.getBalance("NXC");

  if (!walletResult["success"]) {
    print("Cannot check balance");
    return;
  }

  final Wallet wallet = walletResult["walletDetails"];
  const double stakeAmount = 500.0;

  if (wallet.isFreeze) {
    print("Wallet is frozen. Please contact support.");
    return;
  }

  if (wallet.walletBalance < stakeAmount) {
    final needed = stakeAmount - wallet.walletBalance;
    print("Top-up NXC $needed to play.");
    return;
  }

  // Proceed with queue entry
  sdk.game.onLaunchGame((data) => openGame(data["url"]));
  sdk.game.onStatusChange((s) => print(s));
  sdk.game.onErrorMessage((e) => print(e));

  await sdk.game.connect({
    "email": "player@example.com",
    "password": "s3cur3P@ss",
  });

  await sdk.game.joinQueue({
    "gameId": "game_abc123",
    "stake": stakeAmount.toInt(),
    "currency": "NXC",
    "teamSize": 1,
  });
}
```

---

### Example 8: Solo Play

```dart
Future<void> playSolo(NexusWagerSDK sdk) async {
  sdk.game.onLaunchGame((data) {
    print("Solo game: ${data["url"]}");
  });

  await sdk.game.connect({
    "email": "player@example.com",
    "password": "s3cur3P@ss",
  });

  final result = await sdk.game.soloPlay("game_abc123");
  print(result["message"]);
}
```

---

## Best Practices

### 1. Initialize Once at App Startup

The SDK is a singleton. Create it once in your app's entry point, then access it anywhere via `NexusWagerSDK.instance!`:

```dart
// main.dart
void main() {
  NexusWagerSDK.initialize(
    apiKey: const String.fromEnvironment('NW_API_KEY'),
    apiSecret: const String.fromEnvironment('NW_API_SECRET'),
    // ...remaining config
  );
  runApp(MyApp());
}

// Anywhere else in the app
final sdk = NexusWagerSDK.instance!;
```

Pass config with `--dart-define` at build time rather than hardcoding:

```bash
flutter run \
  --dart-define=NW_API_KEY=xxx \
  --dart-define=NW_API_SECRET=yyy
```

---

### 2. Register Event Handlers Before Connecting

The socket may emit `identify` and other early events before your callbacks are registered if you call `connect()` first:

```dart
// ✅ Correct order
sdk.game.onStatusChange(statusHandler);
sdk.game.onErrorMessage(errorHandler);
sdk.game.onLaunchGame(launchHandler);
sdk.game.onMatchProposal(proposalHandler);
await sdk.game.connect(credentials);

// ❌ Wrong — handlers registered after connect may miss events
await sdk.game.connect(credentials);
sdk.game.onMatchProposal(proposalHandler); // too late
```

---

### 3. Always Check `success` Before Reading Data

```dart
final result = await sdk.game.joinQueue(data);
if (!result["success"]) {
  // Always handle the error case
  showErrorToast(result["message"]);
  return;
}
// Only proceed here
```

---

### 4. Gate Game Actions Behind `requireAuth()`

Before presenting matchmaking UI, verify the user is authenticated:

```dart
final check = sdk.context.requireAuth();
if (!check["success"]) {
  navigateToLogin();
  return;
}
```

---

### 5. Disconnect in Widget `dispose()`

Always release the socket when leaving the matchmaking screen:

```dart
@override
void dispose() {
  NexusWagerSDK.instance?.game.disconnect();
  super.dispose();
}
```

---

### 6. Handle Frozen Wallets Separately from Insufficient Balance

The SDK treats a frozen wallet identically to an insufficient balance in error messages. If your UI needs to differentiate:

```dart
final walletResult = await sdk.game.wallet.getBalance("NXC");
final Wallet wallet = walletResult["walletDetails"];

if (wallet.isFreeze) {
  showFrozenWalletDialog();
} else if (wallet.walletBalance < stakeAmount) {
  showTopUpDialog(stakeAmount - wallet.walletBalance);
}
```

---

### 7. Use `onConnectStatus` for Connection UI

`onStatusChange` tracks matchmaking state. `onConnectStatus` tracks the raw socket connection:

```dart
sdk.game.onConnectStatus((status) {
  // "Connected" or "Disconnected"
  updateConnectionBadge(status);
});

sdk.game.onStatusChange((status) {
  // "idle", "searching", "confirming", "matched", "error"
  updateMatchmakingUI(status);
});
```

---

### 8. Include `teamSize` in All Queue and Private Match Calls

`teamSize` is used by the server to determine match grouping and by the SDK internally when deciding whether to register a game session (`teamSize > 0` triggers session registration). Always supply it accurately:

```dart
// 1v1
"teamSize": 1

// 2v2
"teamSize": 2
```

---

### 9. Pass Currency Explicitly to Balance Checks

The wallet system is multi-currency. Always pass the same currency that will be used for the stake:

```dart
// ✅ Currency matches the queue stake
await sdk.game.wallet.getBalance("NXC");
await sdk.game.joinQueue({"stake": 500, "currency": "NXC", ...});

// ❌ Currency mismatch — balance check may pass but queue will fail
await sdk.game.wallet.getBalance("NGN");
await sdk.game.joinQueue({"stake": 500, "currency": "NXC", ...});
```

---

### 10. Log SDK Output in Development

The SDK prefixes all console output with `[NW]`:

```
[NW][INFO]  NexusWager SDK v1.0.0 initialized
[NW][WARN]  Disconnected from socket
[NW][ERROR] Failed to connect to socket
```

Filter your console for `[NW]` during development to monitor SDK activity separately from your own logs.
