npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dubsdotapp/expo

v0.2.69

Published

React Native SDK for the Dubs betting platform

Downloads

7,787

Readme

@dubsdotapp/expo

React Native SDK for the Dubs betting platform.

Install

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

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

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:

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

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

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

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

3. Browse events

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

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

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:

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:

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:

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.

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
// 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.

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
// 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.

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.

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:

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