npm package: @hyve-sdk/js
Description: Hyve SDK - TypeScript wrapper for Hyve game server integration
Author: Hyve
Latest version: 2.11.2

# @hyve-sdk/js

TypeScript SDK for integrating games with the Hyve platform. Provides authentication, telemetry, persistent storage, ads, billing, and native bridge capabilities.

## Installation

```bash
bun add @hyve-sdk/js
```

## Quick Start

```tsx
import { HyveSdkProvider, useHyveSdk } from "@hyve-sdk/js/react";

function App() {
  return (
    <HyveSdkProvider>
      <Game />
    </HyveSdkProvider>
  );
}

function Game() {
  const hyve = useHyveSdk();

  const handleScore = () => {
    hyve.sendTelemetry("game", "player", "score_submitted", null, null, { score: 1000 });
  };

  return <button onClick={handleScore}>Submit Score</button>;
}
```

You can also pass a pre-created client:

```tsx
import { HyveClient } from "@hyve-sdk/js";
import { HyveSdkProvider } from "@hyve-sdk/js/react";

const client = new HyveClient();

<HyveSdkProvider client={client}>
  <App />
</HyveSdkProvider>
```

The SDK reads authentication from URL parameters automatically during construction:

```
https://your-game.com?hyve-access=eyJhbGci...&game-id=my-game
```

**Environment detection** happens automatically from the parent page URL (games run in iframes):
- Dev: `marvin.dev.hyve.gg` or `dev.hyve.gg`
- Prod: `marvin.hyve.gg` or `hyve.gg`

## Telemetry

```ts
const hyve = useHyveSdk();

await hyve.sendTelemetry(
  "game",        // location
  "player",      // category
  "action",      // action
  "combat",      // sub-category (optional)
  "attack",      // sub-action (optional)
  {              // event details (optional)
    damage: 100,
    targetId: "enemy-123",
  },
  "playgama"     // platform ID (optional)
);
```

Requires a valid `hyve-access` JWT and `game-id` in the URL.

## Persistent Game Data

Save and retrieve game data with either cloud (default) or local storage:

```ts
const hyve = useHyveSdk();

// Save a value
await hyve.saveGameData("player_level", 5);
await hyve.saveGameData("settings", { volume: 0.8, fullscreen: true });

// Atomic operations — read-modify-write server-side
await hyve.saveGameData("coins", 100, "add");           // increment
await hyve.saveGameData("high_score", 9999, "max");      // keep highest
await hyve.saveGameData("stats", 1, "add", "level");     // nested path

// Get a value
const item = await hyve.getGameData("player_level");
console.log(item?.value); // 5

// Batch save (per-item operations supported)
await hyve.batchSaveGameData([
  { key: "coins", value: 1200 },
  { key: "lives", value: 3 },
  { key: "high_score", value: 5000, operation: "max" },
]);

// Batch get
const items = await hyve.getMultipleGameData(["coins", "lives"]);

// Delete
await hyve.deleteGameData("temp_key");
await hyve.deleteMultipleGameData(["key1", "key2"]);
```

### Storage Modes

```ts
// Set mode at construction
const client = new HyveClient({ storageMode: "local" });

// Or override per-call (storage is the last parameter)
await hyve.saveGameData("key", "value", undefined, undefined, "local");
await hyve.getGameData("key", "cloud");

// Change mode at runtime
hyve.configureStorage("cloud");
console.log(hyve.getStorageMode()); // "cloud"
```

| Mode | Description |
|------|-------------|
| `cloud` | Synced to backend API (default) |
| `local` | Browser `localStorage`, device-only |

## Leaderboards

Rank players by numeric values stored in persistent game data.

```ts
const hyve = useHyveSdk();

// 1. Store the player's score
await hyve.saveGameData("high_score", 4200);

// 2. Fetch the leaderboard
const board = await hyve.getGameDataLeaderboard({ key: "high_score" });

for (const entry of board.entries) {
  console.log(`#${entry.rank} ${entry.username}: ${entry.score}`);
}

// The caller's own rank is included automatically
if (board.user_position) {
  console.log("Your rank:", board.user_position.rank);
}
```

Use `score_path` to rank by a nested field, and `sort`/`limit`/`cursor` for pagination:

```ts
const board = await hyve.getGameDataLeaderboard({
  key: "tetris_progress",
  score_path: "stats.score",  // dot-notation path to numeric field
  sort: "desc",               // "asc" | "desc" (default: "desc")
  limit: 20,                  // 1–100 (default: 10)
});

if (board.has_more && board.next_cursor) {
  const next = await hyve.getGameDataLeaderboard({
    key: "tetris_progress",
    cursor: board.next_cursor,
  });
}
```

## Ads

Ads are disabled by default and must be explicitly configured.

```tsx
const client = new HyveClient({
  ads: {
    enabled: true,
    onBeforeAd: (type) => game.pause(),
    onAfterAd: (type) => game.resume(),
    onRewardEarned: () => { player.coins += 100; },
  },
});

<HyveSdkProvider client={client}>
  <App />
</HyveSdkProvider>
```

```ts
const hyve = useHyveSdk();

const result = await hyve.showAd("rewarded");
if (result.success) {
  console.log("User watched the ad");
}

await hyve.showAd("interstitial");  // between levels
await hyve.showAd("preroll");       // game start
```

| Ad Type | Use Case |
|---------|----------|
| `rewarded` | User watches full ad for a reward |
| `interstitial` | Between levels or game screens |
| `preroll` | Before the game starts |

The SDK automatically routes ad calls through the appropriate platform SDK (CrazyGames, Playgama, or the default Google H5 Ads SDK) based on the current domain.

## Platform Integrations

### CrazyGames

When running on CrazyGames, the SDK auto-initializes the CrazyGames SDK and routes ads through it. Use these additional lifecycle methods:

```ts
const hyve = useHyveSdk();

// Call when gameplay begins (required by CrazyGames policy)
await hyve.gameplayStart();

// Call when gameplay stops (paused, died, menu, etc.)
await hyve.gameplayStop();

// Trigger a celebration effect on the CrazyGames website
await hyve.happytime();
```

### Playgama

When running on Playgama, the SDK auto-initializes the Playgama Bridge and routes ads through it. No additional setup required.

## Billing

Billing supports web (Stripe) and native (In-App Purchases) — platform is detected automatically.

```tsx
const client = new HyveClient({
  billing: {
    stripePublishableKey: "pk_live_...",
  },
});

<HyveSdkProvider client={client}>
  <App />
</HyveSdkProvider>
```

```ts
const hyve = useHyveSdk();

if (hyve.isBillingAvailable()) {
  hyve.onPurchaseComplete((result) => {
    console.log("Purchase successful:", result.transactionId);
  });

  hyve.onPurchaseError((result) => {
    console.error("Purchase failed:", result.error?.message);
  });

  const products = await hyve.getBillingProducts();
  await hyve.purchaseProduct("price_1234");
}
```

For web, add a container element for the Stripe checkout UI:

```html
<div id="stripe-checkout-element"></div>
```

| Platform | Payment Method | Detection |
|----------|---------------|-----------|
| Web | Stripe Embedded Checkout | Default |
| Native iOS/Android | In-App Purchases | `ReactNativeWebView` in window |

## Native Bridge

Type-safe bidirectional communication between your web game and a React Native WebView host.

```ts
import { NativeBridge } from "@hyve-sdk/js";

if (NativeBridge.isNativeContext()) {
  NativeBridge.initialize();

  // Listen for IAP availability
  NativeBridge.on("IAP_AVAILABILITY_RESULT", (payload) => {
    if (payload.available) {
      console.log("IAP available on:", payload.platform);
    }
  });
  NativeBridge.checkIAPAvailability();

  // Request push notification permission
  NativeBridge.on("PUSH_PERMISSION_GRANTED", (payload) => {
    console.log("Token:", payload?.token);
  });
  NativeBridge.requestNotificationPermission();

  // Send/receive custom messages
  NativeBridge.send("GAME_EVENT", { action: "level_complete", level: 3 });
  NativeBridge.on("CUSTOM_RESPONSE", (payload) => {
    console.log("Received:", payload);
  });
}
```

### NativeBridge API

| Method | Description |
|--------|-------------|
| `isNativeContext()` | Check if running in React Native WebView |
| `initialize()` | Start the message listener |
| `send(type, payload?)` | Send a message to the native app |
| `on(type, handler)` | Register a message handler |
| `off(type)` | Remove a message handler |
| `clearHandlers()` | Remove all handlers |
| `checkIAPAvailability()` | Request IAP availability from native |
| `requestNotificationPermission()` | Request push notification permission |

## Logger

```ts
import { logger } from "@hyve-sdk/js";

logger.debug("Debug info", { data: "value" });
logger.info("Informational message");
logger.warn("Warning message");
logger.error("Error message", error);

// Namespaced child logger
const gameLogger = logger.child("Game");
gameLogger.info("Game started");
// Output: [Hyve SDK] [Game] [INFO] [timestamp] Game started
```

Override log level in the browser:

```js
localStorage.setItem("HYVE_SDK_LOG_LEVEL", "error,warn");
```

## API Reference

### HyveClient Config

```ts
new HyveClient(config?: {
  apiBaseUrl?: string;         // Override API base URL
  storageMode?: "cloud" | "local";
  ads?: AdConfig;
  billing?: BillingConfig;
})
```

### Authentication

| Method | Returns | Description |
|--------|---------|-------------|
| `getUserId()` | `string \| null` | Authenticated user ID |
| `getGameId()` | `string \| null` | Game ID from URL or JWT |
| `getSessionId()` | `string` | Unique session ID |
| `getJwtToken()` | `string \| null` | Raw JWT string |
| `getApiBaseUrl()` | `string` | Current API base URL |
| `isUserAuthenticated()` | `boolean` | Whether a user ID was extracted |
| `hasJwtToken()` | `boolean` | Whether a JWT is present |
| `logout()` | `void` | Clear auth state |
| `reset()` | `void` | Clear auth and generate new session ID |

### API

| Method | Returns | Description |
|--------|---------|-------------|
| `callApi<T>(endpoint, options?)` | `Promise<T>` | Authenticated fetch to the Hyve API |
| `getInventory()` | `Promise<Inventory>` | Get user inventory |
| `getInventoryItem(itemId)` | `Promise<InventoryItem>` | Get a specific inventory item |
| `getMachineRender(machineId)` | `Promise<string>` | Fetch rendered HTML for a project machine |

### Telemetry

| Method | Returns | Description |
|--------|---------|-------------|
| `sendTelemetry(location, category, action, subCategory?, subAction?, details?, platformId?)` | `Promise<boolean>` | Send an analytics event |
| `updateTelemetryConfig(config)` | `void` | Update telemetry settings at runtime |

### Storage

| Method | Returns | Description |
|--------|---------|-------------|
| `saveGameData(key, value, operation?, path?, storage?)` | `Promise<SaveGameDataResponse>` | Save a single value; optional atomic operation and nested path |
| `batchSaveGameData(items, storage?)` | `Promise<BatchSaveGameDataResponse>` | Save multiple values; each item may include an operation and path |
| `getGameData(key, storage?)` | `Promise<GameDataItem \| null>` | Get a single value |
| `getMultipleGameData(keys, storage?)` | `Promise<GameDataItem[]>` | Get multiple values |
| `deleteGameData(key, storage?)` | `Promise<boolean>` | Delete a single value |
| `deleteMultipleGameData(keys, storage?)` | `Promise<number>` | Delete multiple values; returns count |
| `getGameDataLeaderboard(params)` | `Promise<GameDataLeaderboardResponse>` | Fetch players ranked by a game data key |
| `configureStorage(mode)` | `void` | Set default storage mode |
| `getStorageMode()` | `"cloud" \| "local"` | Get current storage mode |

### Ads

| Method | Returns | Description |
|--------|---------|-------------|
| `configureAds(config)` | `void` | Configure the ads service |
| `showAd(type)` | `Promise<AdResult>` | Show an ad |
| `areAdsReady()` | `boolean` | Check if ads have initialized |
| `gameplayStart()` | `Promise<void>` | Notify gameplay started (CrazyGames) |
| `gameplayStop()` | `Promise<void>` | Notify gameplay stopped (CrazyGames) |
| `happytime()` | `Promise<void>` | Trigger celebration effect (CrazyGames) |

### Billing

| Method | Returns | Description |
|--------|---------|-------------|
| `getBillingPlatform()` | `BillingPlatform` | `"web"` \| `"native"` \| `"unknown"` |
| `isBillingAvailable()` | `boolean` | Check if billing is ready |
| `getBillingProducts()` | `Promise<BillingProduct[]>` | Fetch available products |
| `purchaseProduct(productId, options?)` | `Promise<PurchaseResult>` | Initiate a purchase |
| `onPurchaseComplete(callback)` | `void` | Register purchase success handler |
| `onPurchaseError(callback)` | `void` | Register purchase error handler |
| `unmountBillingCheckout()` | `void` | Clean up Stripe checkout UI |

## Build Output

| Format | File | Use |
|--------|------|-----|
| CJS | `dist/index.js` | Node.js / bundler |
| ESM | `dist/index.mjs` | Bundler (tree-shakeable) |
| CJS (React) | `dist/react.js` | React integration |
| ESM (React) | `dist/react.mjs` | React integration (tree-shakeable) |

## Development

```bash
pnpm run check-types   # Type check
pnpm run lint          # Lint
pnpm run build         # Build
```

## License

MIT