@beta-gamer/react-native
v0.1.35
Published
React Native SDK for Beta Gamer GaaS — composable game components
Readme
@beta-gamer/react-native
React Native SDK for Beta Gamer — embed multiplayer games into your mobile app as composable components. You control the layout; we handle matchmaking, game logic, and real-time sync.
Installation
npm install @beta-gamer/react-native
react-native-webviewis only required for Subway Runner. Chess, Checkers, Connect 4, and Tic-tac-toe are fully native.
# Only if using subway-runner:
npx expo install react-native-webview # Expo
npm install react-native-webview && cd ios && pod install # Bare RNRequirements
- React Native 0.73+ or Expo SDK 50+
- A Beta Gamer tenant account and API key → beta-gamer.com
Quick start
1. Create a session on your backend (never call the Beta Gamer API from the app)
// Your server — e.g. Express, Next.js API route, Cloudflare Worker
app.post('/api/game/start', async (req, res) => {
const { userId, userName, game, matchType } = req.body;
const response = await fetch('https://api.beta-gamer.com/v1/sessions', {
method: 'POST',
headers: {
'Authorization': 'Bearer bg_live_xxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
game,
matchType,
players: [{ id: userId, displayName: userName }],
}),
});
const { sessionToken } = await response.json();
res.json({ sessionToken });
});⚠️ Never call the Beta Gamer API from your app. Your API key would be bundled into the binary and is trivially extractable from any APK or IPA.
2. Fetch the token and render
import { BetaGamerProvider, ChessBoard } from '@beta-gamer/react-native';
export function ChessGameScreen({ sessionToken }: { sessionToken: string }) {
return (
<BetaGamerProvider token={sessionToken} connectSocket={false}>
<ChessBoard layout="default" style={{ flex: 1 }} onLeave={() => navigation.goBack()} />
</BetaGamerProvider>
);
}BetaGamerProvider props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| token | string | required | Session token from your backend |
| serverUrl | string | 'https://api.beta-gamer.com' | Beta Gamer server URL |
| socketPath | string | '/socket.io' | Socket.IO path (change if self-hosting) |
| connectSocket | boolean | true | Set false when using board components — they manage their own socket. Leaving this true alongside a board component creates a duplicate connection causing AFK and turn bugs. |
Components
Chess
import { ChessBoard, ChessMoveHistory } from '@beta-gamer/react-native';
<ChessBoard
layout="default" // 'board-only' (default) | 'default' (adds player rows, clocks, resign/draw)
orientation="auto" // 'auto' | 'white' | 'black'
showCoords // rank/file labels (default: true)
showLastMove // highlight last-moved square (default: true)
lastMoveStyle="highlight" // 'highlight' | 'glow' | 'pulse' | 'dot'
showLegalMoves // green dots on legal squares (default: true)
showAfkWarning // built-in AFK banner (default: true, layout="default" only)
style={{ flex: 1 }}
onLeave={() => navigation.goBack()}
/>
<ChessMoveHistory style={styles.sidebar} textStyle={{ color: '#fff' }} rowStyle={{ paddingVertical: 4 }} />Checkers
import { CheckersBoard } from '@beta-gamer/react-native';
<CheckersBoard
layout="default"
showLastMove
lastMoveStyle="glow" // 'highlight' | 'glow' | 'pulse' | 'dot'
showAfkWarning
style={{ flex: 1 }}
onLeave={() => navigation.goBack()}
boardStyles={{
lightCell: { backgroundColor: '#f0d9b5' },
darkCell: { backgroundColor: '#b58863' },
selectedCell: { borderWidth: 3, borderColor: '#facc15' },
validCell: { borderWidth: 3, borderColor: '#4ade80' },
pieceRed: { backgroundColor: '#ef4444', borderWidth: 2, borderColor: '#991b1b' },
pieceBlack: { backgroundColor: '#374151', borderWidth: 2, borderColor: '#111827' },
pieceSelected: { transform: [{ scale: 1.1 }] },
moveDotNormal: { backgroundColor: 'rgba(74,222,128,0.6)' },
moveDotCapture: { backgroundColor: 'rgba(251,146,60,0.7)' },
}}
/>Connect 4
import { Connect4Board } from '@beta-gamer/react-native';
<Connect4Board
layout="default"
showLastMove
lastMoveStyle="glow"
showAfkWarning
style={{ flex: 1 }}
onLeave={() => navigation.goBack()}
boardStyles={{
board: { backgroundColor: '#2563eb' },
cell: { backgroundColor: '#1e3a5f' },
cellWin: { backgroundColor: '#15803d' },
pieceRed: { backgroundColor: '#ef4444' },
pieceYellow: { backgroundColor: '#eab308' },
pieceWin: { transform: [{ scale: 1.1 }] },
}}
/>Tic-tac-toe
import { TictactoeBoard } from '@beta-gamer/react-native';
<TictactoeBoard
layout="default"
showLastMove
lastMoveStyle="glow"
showAfkWarning
style={{ flex: 1 }}
onLeave={() => navigation.goBack()}
cellStyles={{
active: { backgroundColor: '#4c1d95' },
idle: { backgroundColor: '#1e1b4b' },
win: { backgroundColor: '#15803d' },
border: { borderWidth: 2, borderColor: '#1e1b4b' },
}}
markStyles={{
x: { color: '#60a5fa' },
o: { color: '#f472b6' },
win: { color: '#fff' },
base: { fontSize: 48, fontWeight: 'bold' },
}}
/>Subway Runner (WebView-based)
import { SubwayRunnerGame, SubwayRunnerScore, SubwayRunnerLives } from '@beta-gamer/react-native';
<SubwayRunnerGame style={{ flex: 1 }} />
<SubwayRunnerScore style={styles.score} textStyle={{ color: '#fff', fontSize: 24 }} />
<SubwayRunnerLives style={styles.lives} lifeStyle={styles.lifeIcon} initialLives={3} />Shared UI
| Component | Props | Description |
|-----------|-------|-------------|
| PlayerCard | player ('self' | 'opponent'), style?, nameStyle? | Player name + active-turn indicator |
| Timer | player, initialSeconds?, style?, textStyle? | Live countdown clock |
layout prop
All four native board components support two layouts:
| Value | Description |
|-------|-------------|
| 'board-only' (default) | Renders just the board. Build your own layout around it. |
| 'default' | Full built-in layout: player rows, clocks (chess), resign/draw buttons, AFK banner, game-over modal. |
Headless mode — game hooks
Each board is powered by a hook internally. Call the hook yourself to access game state and the socket, then pass the instance to the board via the game prop — this shares one socket instead of creating a duplicate.
import { BetaGamerProvider, ChessBoard, useChessGame } from '@beta-gamer/react-native';
import { useEffect } from 'react';
function GameUI() {
const game = useChessGame();
// game.chess — chess.js instance
// game.myColor — 'white' | 'black'
// game.isMyTurn — boolean
// game.legalMoves — string[] of target squares
// game.lastMove — { from, to } | null
// game.clocks — { self: number, opponent: number } (seconds)
// game.gameOver — boolean
// game.gameResult — { winner: string | null, reason: string } | null
// game.afkWarning — { playerId, secondsRemaining } | null
// game.players — any[]
// game.myPlayerId — string
// game.handleSquarePress(sq) — handles selection + move emission
// game.emitMove(from, to, promotion?) — emit a move directly
// game.socket — raw Socket.IO socket
// game.roomId — current room ID
useEffect(() => {
if (!game.socket) return;
game.socket.on('game:over', ({ winner }) => console.log('Winner:', winner));
return () => { game.socket?.off('game:over'); };
}, [game.socket]);
return <ChessBoard game={game} layout="board-only" onLeave={handleLeave} />;
}
export function GameScreen({ sessionToken }) {
return (
<BetaGamerProvider token={sessionToken} connectSocket={false}>
<GameUI />
</BetaGamerProvider>
);
}The same pattern applies to useCheckersGame, useConnect4Game, and useTictactoeGame.
Game state shapes
| Hook | Key fields |
|------|-----------|
| useChessGame | chess, myColor, fen, selected, legalMoves, lastMove, clocks, promotionMove, handleSquarePress, emitMove |
| useCheckersGame | myColor, board, selectedPiece, validMoves, lastMove, handleCellPress |
| useConnect4Game | myColor, board, lastMove, winningCells, handleColumnPress |
| useTictactoeGame | myMark, board, winningLine, lastMove, handleCellPress |
All hooks also expose: socket, roomId, myPlayerId, players, isMyTurn, gameOver, gameResult, afkWarning.
EventBus
A typed singleton event bus that fires for every socket event. Works anywhere in your app as long as a game hook or board component is active.
import { EventBus, useEventBus } from '@beta-gamer/react-native';
// Outside React
EventBus.on('game:over', ({ winner, reason }) => { /* analytics, etc. */ });
EventBus.once('game:started', (data) => { /* fires once then removes itself */ });
const unsub = EventBus.on('game:move:made', handler);
unsub(); // remove listener
// Inside a component — auto-cleanup on unmount
function MyComponent() {
const { on } = useEventBus();
useEffect(() => on('game:over', ({ winner }) => console.log(winner)), []);
}Available events
| Event | Payload | Games |
|-------|---------|-------|
| game:started | { roomId, playerId, players, currentTurn, ...gameSpecific } | all |
| game:over | { winner: string \| null, reason: string } | all |
| game:move:made | { board, currentTurn, ...gameSpecific } | all |
| game:valid_moves | { position: number, moves: Move[] } | checkers |
| timer:update | { playerTimers: Record<string, number> } | chess |
| chess:afk_warning | { playerId, secondsRemaining } | chess |
| chess:afk_warning_cleared | void | chess |
| checkers:afk_warning | { playerId, secondsRemaining } | checkers |
| checkers:afk_warning_cleared | void | checkers |
| connect4:afk_warning | { playerId, secondsRemaining } | connect4 |
| connect4:afk_warning_cleared | void | connect4 |
| tictactoe:afk_warning | { playerId, secondsRemaining } | tictactoe |
| tictactoe:afk_warning_cleared | void | tictactoe |
| afk:status | { playerId, expiresAt } \| null | all |
| connection_error | { message: string } | all |
All payloads are fully typed via BetaGamerEvents:
import type { BetaGamerEvents, BetaGamerEventName } from '@beta-gamer/react-native';Other hooks
import { useGameState, useSession, useSocket, useConnectionError, useEventBus } from '@beta-gamer/react-native';
const { status, winner, reason } = useGameState();
const { game, matchType, players } = useSession();
const socket = useSocket(); // active socket — may be null briefly on first render
const error = useConnectionError(); // string | null — no built-in alert, handle it yourself
const { on } = useEventBus(); // typed event bus with auto-cleanup on unmountuseConnectionError example
function GameScreen({ sessionToken }) {
const error = useConnectionError();
if (error) return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text style={{ color: 'red' }}>{error}</Text>
</View>
);
return <ChessBoard layout="default" onLeave={handleLeave} />;
}AFK warnings
Set showAfkWarning={false} to suppress the built-in banner and handle it yourself via the EventBus:
const { on } = useEventBus();
useEffect(() => {
const off1 = on('chess:afk_warning', ({ secondsRemaining }) => { /* show custom UI */ });
const off2 = on('chess:afk_warning_cleared', () => { /* hide custom UI */ });
return () => { off1(); off2(); };
}, []);Supported games
chess · checkers · connect4 · tictactoe · subway-runner
