@dubsdotapp/expo
v0.2.69
Published
React Native SDK for the Dubs betting platform
Downloads
7,787
Maintainers
Readme
@dubsdotapp/expo
React Native SDK for the Dubs betting platform.
Install
npx expo install @dubsdotapp/expo @solana/web3.js expo-secure-storeFor the built-in Mobile Wallet Adapter (optional):
npx expo install @solana-mobile/mobile-wallet-adapter-protocol-web3jsCustom 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:androidThis 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
defaultAmountautomatically) - 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 viadistribute_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
