# @dubsdotapp/expo

React Native SDK for the [Dubs](https://dubs.fun) betting platform.

## Install

```bash
npx expo install @dubsdotapp/expo @solana/web3.js expo-secure-store
```

For the built-in Mobile Wallet Adapter (optional):

```bash
npx expo install @solana-mobile/mobile-wallet-adapter-protocol-web3js
```

### Custom Dev Builds

The SDK includes `expo-crypto` as a dependency to provide `crypto.getRandomValues` for Solana transaction signing, wallet encryption, and other cryptographic operations. This works automatically in Expo Go and EAS builds.

If your app uses a **custom dev build** (has `ios/` or `android/` directories), you need to rebuild after installing so the native `ExpoCrypto` module is linked:

```bash
npx expo prebuild --clean
npx expo run:ios   # or npx expo run:android
```

This is standard Expo behavior for any package with native code — you only need to do this once after installing the SDK.

## Quick Start

### 1. Set up the provider

```tsx
import { DubsProvider } from '@dubsdotapp/expo';

export default function App() {
  return (
    <DubsProvider apiKey="your_api_key_here" appName="My App" network="devnet">
      <HomeScreen />
    </DubsProvider>
  );
}
```

That's it — 5 lines. `DubsProvider` handles everything:

- **Connect screen** — shows a "Connect Wallet" button, opens Phantom/Solflare/etc.
- **Silent reconnect** — returning users skip straight to the app
- **3-step onboarding** — avatar selection (6 DiceBear styles), real-time username validation, optional referral code
- **Token persistence** — MWA + JWT tokens saved in `expo-secure-store`
- **Session management** — sign-in, registration, restore, and full disconnect/logout

### 2. Access the user

```tsx
import { useAuth } from '@dubsdotapp/expo';

function Profile() {
  const { user, isAuthenticated } = useAuth();
  if (!isAuthenticated) return null;
  return <Text>@{user.username}</Text>;
}
```

### 3. Browse events

```tsx
import { useEvents } from '@dubsdotapp/expo';

function EventsList() {
  const { data, loading, error, refetch } = useEvents({ type: 'sports' });

  if (loading) return <Text>Loading...</Text>;
  if (error) return <Text>Error: {error.message}</Text>;

  return (
    <FlatList
      data={data?.events}
      renderItem={({ item }) => (
        <Text>{item.title} — {item.startTime}</Text>
      )}
    />
  );
}
```

### 4. Create a bet

```tsx
import { useCreateGame } from '@dubsdotapp/expo';

function CreateBet({ eventId }: { eventId: string }) {
  const { execute, status, error } = useCreateGame();

  const handleCreate = async () => {
    const result = await execute({
      id: eventId,                // e.g. "sports:NBA:espn-nba-401..."
      playerWallet: 'YOUR_WALLET',
      teamChoice: 'home',
      wagerAmount: 0.1,           // SOL
    });

    console.log('Bet placed!', result.explorerUrl);
  };

  return (
    <View>
      <Button title="Place Bet" onPress={handleCreate} disabled={status !== 'idle'} />
      {status !== 'idle' && <Text>Status: {status}</Text>}
      {error && <Text>Error: {error.message}</Text>}
    </View>
  );
}
```

## Provider Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `apiKey` | `string` | *required* | Your Dubs API key |
| `appName` | `string` | `'Dubs'` | App name shown on connect/auth screens |
| `network` | `'devnet' \| 'mainnet-beta'` | `'mainnet-beta'` | Network preset (sets baseUrl, rpcUrl, cluster) |
| `wallet` | `WalletAdapter` | *auto (MWA)* | Bring your own wallet adapter |
| `tokenStorage` | `TokenStorage` | *SecureStore* | Custom token persistence |
| `baseUrl` | `string` | *from network* | Override API base URL |
| `rpcUrl` | `string` | *from network* | Override Solana RPC URL |
| `renderConnectScreen` | `fn \| false` | *default UI* | Custom connect screen, or `false` to hide |
| `renderLoading` | `fn` | *default UI* | Custom loading screen during auth |
| `renderError` | `fn` | *default UI* | Custom error screen |
| `renderRegistration` | `fn` | *default UI* | Custom registration screen |
| `managed` | `boolean` | `true` | Set `false` for headless mode (no connect screen or auth gate) |
| `redirectUri` | `string` | — | Deeplink redirect URI for Phantom wallet (required for iOS) |
| `appUrl` | `string` | — | App URL shown in Phantom's connect screen |

### Disconnect

```tsx
import { useDubs } from '@dubsdotapp/expo';

function LogoutButton() {
  const { disconnect } = useDubs();
  return <Button title="Log Out" onPress={disconnect} />;
}
```

`disconnect()` clears wallet connection, MWA token, JWT, and returns to the connect screen.

## Custom Wallet Adapter (BYOA)

If you're using Privy, Dynamic, or another wallet provider, pass a `wallet` prop to skip managed MWA:

```tsx
import { DubsProvider } from '@dubsdotapp/expo';
import type { WalletAdapter } from '@dubsdotapp/expo';

const myAdapter: WalletAdapter = {
  publicKey: new PublicKey('...'),
  connected: true,
  signTransaction: async (tx) => { /* ... */ return signedTx; },
  connect: async () => { /* ... */ },
  disconnect: () => { /* ... */ },
};

<DubsProvider apiKey="..." wallet={myAdapter}>
  <App />
</DubsProvider>
```

## Custom Token Storage

By default, `DubsProvider` uses `expo-secure-store` for persisting auth tokens. To use a different storage:

```tsx
import { DubsProvider } from '@dubsdotapp/expo';
import type { TokenStorage } from '@dubsdotapp/expo';
import AsyncStorage from '@react-native-async-storage/async-storage';

const myStorage: TokenStorage = {
  getItem: (key) => AsyncStorage.getItem(key),
  setItem: (key, value) => AsyncStorage.setItem(key, value),
  deleteItem: (key) => AsyncStorage.removeItem(key),
};

<DubsProvider apiKey="..." tokenStorage={myStorage}>
  <App />
</DubsProvider>
```

## API Reference

### Hooks

| Hook | Type | Description |
|------|------|-------------|
| `useAuth()` | Auth | Full auth state — `user`, `isAuthenticated`, `authenticate()`, `register()`, `logout()` |
| `useEvents(params?)` | Query | Fetch upcoming events (sports + esports) |
| `useGame(gameId)` | Query | Single game detail |
| `useGames(params?)` | Query | List games with filters |
| `useNetworkGames(params?)` | Query | List games across the entire Dubs network |
| `useCreateGame()` | Mutation | Create game on a sports/esports event (build → sign → confirm) |
| `useCreateCustomGame()` | Mutation | Create a custom 1v1 game with arbitrary buy-in |
| `useJoinGame()` | Mutation | Join an existing game |
| `useClaim()` | Mutation | Claim prize or refund after game resolves |
| `useHasClaimed(gameId)` | Query | Check if current wallet already claimed — returns `hasClaimed`, `amountClaimed`, `claimSignature` |
| `useUFCFightCard()` | Query | Fetch all upcoming UFC fight cards with fighters and fight data |
| `useUFCFighterDetail(athleteId)` | Query | Fetch detailed fighter profile (height, weight, reach, stance, gym, age, etc.) — only fetches when athleteId is non-null |

### Mutation Status

Mutation hooks expose granular status:

```
'idle' → 'building' → 'signing' → 'confirming' → 'saving' → 'success'
                                                              ↘ 'error'
```

### DubsClient (headless)

For use outside React or for custom integrations:

```ts
import { DubsClient } from '@dubsdotapp/expo';

const client = new DubsClient({ apiKey: 'dubs_live_...' });

const { events } = await client.getUpcomingEvents({ type: 'sports', game: 'nba' });
const game = await client.getGame('sport-123...');
const codes = client.getErrorCodesLocal();
```

## UI Components

### Game Sheets

Drop-in bottom sheets that handle the full transaction lifecycle (build → sign → confirm) internally.

#### CreateCustomGameSheet

Bottom sheet for creating a custom game with configurable buy-in.

```tsx
import { CreateCustomGameSheet } from '@dubsdotapp/expo';

<CreateCustomGameSheet
  visible={showSheet}
  onDismiss={() => setShowSheet(false)}
  presetAmounts={[0.01, 0.1, 0.5]}
  defaultAmount={0.01}
  metadata={{ matchType: 'battleship' }}
  onSuccess={(result) => console.log('Created!', result.gameId)}
  onError={(err) => console.error(err)}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `visible` | `boolean` | *required* | Show/hide the sheet |
| `onDismiss` | `() => void` | *required* | Called when user closes the sheet |
| `title` | `string` | `'New Game'` | Sheet header title |
| `maxPlayers` | `number` | `2` | Max players (currently 1v1) |
| `fee` | `number` | `5` | Platform fee percentage (0–100) |
| `presetAmounts` | `number[]` | `[0.01, 0.1, 0.5, 1]` | Quick-select buy-in chips |
| `defaultAmount` | `number` | `0.01` | Pre-selected buy-in |
| `metadata` | `Record<string, unknown>` | — | Arbitrary metadata attached to the game |
| `isPoolModeEnabled` | `boolean` | `false` | Pool mode — see below |
| `onSuccess` | `(result) => void` | — | Called with `{ gameId, gameAddress, signature, explorerUrl, buyIn }` |
| `onError` | `(error) => void` | — | Called on failure |

**Pool Mode** (`isPoolModeEnabled`): For winner-takes-all pools (e.g. UFC Pick'em) where all players pay a fixed buy-in and winners split the pot. When enabled:
- Hides buy-in selection UI (uses `defaultAmount` automatically)
- Auto-assigns `teamChoice: 'home'` (no side selection — oracle resolves winners)
- Labels change to "Create Pool" instead of "New Game"
- Summary shows "Max players" and "Max pot" instead of 1v1 display

```tsx
// Pick'em pool — first player creates the on-chain game
<CreateCustomGameSheet
  isPoolModeEnabled
  visible={showCreate}
  title="UFC 313 Pick'em"
  maxPlayers={50}
  defaultAmount={0.1}
  metadata={{ pickemPoolId: pool.id }}
  onSuccess={(result) => handlePoolCreated(result)}
  onDismiss={() => setShowCreate(false)}
/>
```

#### JoinGameSheet

Bottom sheet for joining an existing game. Shows buy-in, team selection, pool summary, and potential winnings.

```tsx
import { JoinGameSheet } from '@dubsdotapp/expo';

<JoinGameSheet
  visible={showSheet}
  onDismiss={() => setShowSheet(false)}
  game={gameData}
  onSuccess={(result) => console.log('Joined!', result.signature)}
  onError={(err) => console.error(err)}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `visible` | `boolean` | *required* | Show/hide the sheet |
| `onDismiss` | `() => void` | *required* | Called when user closes the sheet |
| `game` | `GameDetail` | *required* | Game data from `useGame()` |
| `ImageComponent` | `ComponentType` | — | Custom image component (e.g. expo-image) for team logos |
| `shortName` | `(name) => string` | — | Custom team label formatter |
| `homeColor` | `string` | `'#3B82F6'` | Home team accent color |
| `awayColor` | `string` | `'#EF4444'` | Away team accent color |
| `isPoolModeEnabled` | `boolean` | `false` | Pool mode — see below |
| `onSuccess` | `(result) => void` | — | Called with `{ signature, explorerUrl }` |
| `onError` | `(error) => void` | — | Called on failure |

**Pool Mode** (`isPoolModeEnabled`): For winner-takes-all pools where there are no sides. When enabled:
- Hides team selection UI entirely
- Auto-assigns `teamChoice: 'home'` (oracle distributes to winners via `distribute_survivor_winnings`)
- Labels change to "Join Pool" instead of "Join Game"
- Summary shows "Players in" and "Current pot" instead of side/odds/potential winnings

```tsx
// Pick'em pool — subsequent players join the existing on-chain game
<JoinGameSheet
  isPoolModeEnabled
  visible={showJoin}
  game={gameData}
  onSuccess={(result) => handlePoolJoined(result)}
  onDismiss={() => setShowJoin(false)}
/>
```

#### ClaimPrizeSheet

Bottom sheet for claiming a prize or refund after a game resolves. Includes a celebration animation on success.

```tsx
import { ClaimPrizeSheet } from '@dubsdotapp/expo';

<ClaimPrizeSheet
  visible={showSheet}
  onDismiss={() => setShowSheet(false)}
  gameId="abc123..."
  prizeAmount={0.019}
  isRefund={false}
  onSuccess={(result) => console.log('Claimed!', result.explorerUrl)}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `visible` | `boolean` | *required* | Show/hide the sheet |
| `onDismiss` | `() => void` | *required* | Called when user closes the sheet |
| `gameId` | `string` | *required* | Game ID to claim |
| `prizeAmount` | `number` | *required* | Prize amount in SOL |
| `isRefund` | `boolean` | `false` | Show refund language instead of prize language |
| `onSuccess` | `(result) => void` | — | Called with `{ signature, explorerUrl }` |
| `onError` | `(error) => void` | — | Called on failure |

### ClaimButton

Drop-in button that handles the entire claim flow internally — eligibility checks, prize/refund display, sheet lifecycle, and claimed badge. Renders nothing when the user is ineligible.

```tsx
import { ClaimButton } from '@dubsdotapp/expo';

<ClaimButton
  gameId="abc123..."
  onSuccess={(result) => console.log('Claimed!', result.explorerUrl)}
  onError={(err) => console.error(err)}
/>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `gameId` | `string` | *required* | Game ID to check and claim |
| `style` | `ViewStyle` | — | Custom button style |
| `onSuccess` | `(result) => void` | — | Called after successful claim |
| `onError` | `(error) => void` | — | Called on failure |

**Render states:**
- Loading / no wallet / no data → renders nothing
- Not eligible (lost, not resolved, not a bettor) → renders nothing
- Eligible + unclaimed → solid accent button: "Claim Prize — X SOL" or "Claim Refund — X SOL"
- Already claimed → outlined badge: "Prize Claimed!" or "Refund Claimed!"

### Game Display Cards

| Component | Description |
|-----------|-------------|
| `GamePoster` | Hero card with team logos, names, countdown, and live/locked badges |
| `LivePoolsCard` | Pool breakdown — each side's SOL amount, bar chart, and implied odds |
| `PickWinnerCard` | Team selection card for choosing which side to bet on |
| `PlayersCard` | List of all bettors with avatar, username, team, and wager amount |
| `JoinGameButton` | Fixed bottom bar with buy-in info and join CTA (hides when already joined / locked / resolved) |

### Profile & Settings

| Component | Description |
|-----------|-------------|
| `UserProfileCard` | Displays avatar, username, wallet address, and member-since date |
| `SettingsSheet` | Full settings screen with profile card, copy address, support link, and logout |

### Theming

All UI components respect the developer-configured accent color from the Dubs dashboard. You can also override the theme programmatically:

```tsx
import { useDubsTheme, mergeTheme } from '@dubsdotapp/expo';
import type { DubsTheme } from '@dubsdotapp/expo';

// Inside a component — automatically respects light/dark mode + accent override
const theme = useDubsTheme();

// Or merge custom overrides manually
const custom = mergeTheme(baseTheme, { accent: '#EF4444' });
```

## Event ID Format

- Sports: `sports:{LEAGUE}:{EVENT_ID}` (e.g. `sports:NBA:espn-nba-401...`)
- Esports: `esports:{MATCH_ID}` (e.g. `esports:1353988`)

## License

MIT
