@dubsdotapp/expo 0.2.45 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dubsdotapp/expo",
3
- "version": "0.2.45",
3
+ "version": "0.2.47",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/src/client.ts CHANGED
@@ -36,6 +36,7 @@ import type {
36
36
  LiveScore,
37
37
  UiConfig,
38
38
  UFCEvent,
39
+ UFCFighterDetail,
39
40
  } from './types';
40
41
 
41
42
  export interface DubsClientConfig {
@@ -156,6 +157,14 @@ export class DubsClient {
156
157
  return res.events;
157
158
  }
158
159
 
160
+ async getUFCFighterDetail(athleteId: string): Promise<UFCFighterDetail> {
161
+ const res = await this.request<{ success: true; fighter: UFCFighterDetail }>(
162
+ 'GET',
163
+ `/ufc/fighters/${encodeURIComponent(athleteId)}`,
164
+ );
165
+ return res.fighter;
166
+ }
167
+
159
168
  // ── Game Lifecycle ──
160
169
 
161
170
  async validateEvent(id: string): Promise<ValidateEventResult> {
@@ -15,3 +15,4 @@ export type { ClaimStatus } from './useHasClaimed';
15
15
  export { useAuth } from './useAuth';
16
16
  export type { UseAuthResult } from './useAuth';
17
17
  export { useUFCFightCard } from './useUFCFightCard';
18
+ export { useUFCFighterDetail } from './useUFCFighterDetail';
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { useDubs } from '../provider';
3
+ import type { UFCFighterDetail, QueryResult } from '../types';
4
+
5
+ export function useUFCFighterDetail(athleteId: string | null): QueryResult<UFCFighterDetail> {
6
+ const { client } = useDubs();
7
+ const [data, setData] = useState<UFCFighterDetail | null>(null);
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState<Error | null>(null);
10
+
11
+ const fetchData = useCallback(async () => {
12
+ if (!athleteId) {
13
+ setData(null);
14
+ setLoading(false);
15
+ return;
16
+ }
17
+ setLoading(true);
18
+ setError(null);
19
+ try {
20
+ const result = await client.getUFCFighterDetail(athleteId);
21
+ setData(result);
22
+ } catch (err) {
23
+ setError(err instanceof Error ? err : new Error(String(err)));
24
+ } finally {
25
+ setLoading(false);
26
+ }
27
+ }, [client, athleteId]);
28
+
29
+ useEffect(() => {
30
+ fetchData();
31
+ }, [fetchData]);
32
+
33
+ return { data, loading, error, refetch: fetchData };
34
+ }
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export type {
60
60
  LiveScoreCompetitor,
61
61
  UiConfig,
62
62
  UFCFighter,
63
+ UFCFighterDetail,
63
64
  UFCData,
64
65
  UFCFight,
65
66
  UFCEvent,
@@ -89,6 +90,7 @@ export {
89
90
  useHasClaimed,
90
91
  useAuth,
91
92
  useUFCFightCard,
93
+ useUFCFighterDetail,
92
94
  } from './hooks';
93
95
  export type {
94
96
  CreateGameMutationResult,
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
2
- import { Platform } from 'react-native';
2
+ import { Linking, Platform } from 'react-native';
3
3
  import { MwaWalletAdapter } from './wallet/mwa-adapter';
4
4
  import { PhantomDeeplinkAdapter } from './wallet/phantom-deeplink';
5
5
  import type { PhantomSession } from './wallet/phantom-deeplink';
@@ -11,6 +11,42 @@ import { STORAGE_KEYS } from './storage';
11
11
 
12
12
  const TAG = '[Dubs:ManagedWallet]';
13
13
 
14
+ /**
15
+ * Auto-detect the app's redirect URI for Phantom deeplinks on iOS.
16
+ * Uses expo-linking (available in all Expo apps) to construct a URI
17
+ * from the app's scheme defined in app.json.
18
+ */
19
+ function getDefaultRedirectUri(): string | undefined {
20
+ if (Platform.OS !== 'ios') return undefined;
21
+ try {
22
+ // expo-linking re-exports Linking with createURL that uses the app scheme
23
+ const expoLinking = require('expo-linking');
24
+ if (expoLinking.createURL) {
25
+ const uri = expoLinking.createURL('phantom-callback');
26
+ console.log(TAG, 'Auto-detected redirect URI via expo-linking:', uri);
27
+ return uri;
28
+ }
29
+ } catch (e) {
30
+ console.log(TAG, 'expo-linking createURL failed:', e instanceof Error ? e.message : e);
31
+ }
32
+
33
+ // Fallback: construct from Constants if available
34
+ try {
35
+ const Constants = require('expo-constants').default;
36
+ const scheme = Constants.expoConfig?.scheme;
37
+ if (scheme) {
38
+ const uri = `${scheme}://phantom-callback`;
39
+ console.log(TAG, 'Auto-detected redirect URI via expo-constants:', uri);
40
+ return uri;
41
+ }
42
+ } catch (e) {
43
+ console.log(TAG, 'expo-constants fallback failed:', e instanceof Error ? e.message : e);
44
+ }
45
+
46
+ console.log(TAG, 'Could not auto-detect redirect URI on iOS — pass redirectUri to DubsProvider');
47
+ return undefined;
48
+ }
49
+
14
50
  // ── Module-level Phantom adapter singleton ──
15
51
  // Persists across React remounts (e.g. when the app backgrounds to open Phantom).
16
52
  // This prevents the Linking listener from being torn down mid-flow.
@@ -95,11 +131,13 @@ export function ManagedWalletProvider({
95
131
  const [error, setError] = useState<string | null>(null);
96
132
 
97
133
  // Determine which adapter to use:
98
- // - Android (all devices) → MWA (Phantom + other wallets support Mobile Wallet Adapter natively)
134
+ // - Android → MWA (Phantom + other wallets support Mobile Wallet Adapter natively)
99
135
  // - iOS → Phantom deeplinks (MWA not available on iOS)
100
- const usePhantom = Platform.OS === 'ios' && !!redirectUri;
136
+ const usePhantom = Platform.OS === 'ios';
137
+ // Auto-detect redirect URI on iOS if not explicitly provided
138
+ const resolvedRedirectUri = usePhantom ? (redirectUri || getDefaultRedirectUri()) : undefined;
101
139
 
102
- console.log(TAG, `Platform: ${Platform.OS}, redirectUri: ${redirectUri ? 'provided' : 'not set'}, usePhantom: ${usePhantom}`);
140
+ console.log(TAG, `Platform: ${Platform.OS}, redirectUri: ${resolvedRedirectUri ?? 'none'} (explicit: ${!!redirectUri}), usePhantom: ${usePhantom}`);
103
141
 
104
142
  const adapterRef = useRef<WalletAdapter | null>(null);
105
143
  const transactRef = useRef<any>(null);
@@ -107,8 +145,14 @@ export function ManagedWalletProvider({
107
145
  // Lazily create adapter — Phantom uses a module-level singleton to survive remounts
108
146
  if (!adapterRef.current) {
109
147
  if (usePhantom) {
148
+ if (!resolvedRedirectUri) {
149
+ throw new Error(
150
+ '@dubsdotapp/expo: Could not auto-detect redirect URI on iOS. ' +
151
+ 'Either set a "scheme" in your app.json or pass redirectUri to <DubsProvider>.',
152
+ );
153
+ }
110
154
  adapterRef.current = getOrCreatePhantomAdapter({
111
- redirectUri: redirectUri!,
155
+ redirectUri: resolvedRedirectUri,
112
156
  appUrl,
113
157
  cluster,
114
158
  storage,
package/src/types.ts CHANGED
@@ -419,6 +419,7 @@ export interface LiveScore {
419
419
 
420
420
  export interface UFCFighter {
421
421
  name: string;
422
+ athleteId: string | null;
422
423
  headshotUrl: string | null;
423
424
  flagUrl: string | null;
424
425
  country: string | null;
@@ -427,6 +428,35 @@ export interface UFCFighter {
427
428
  winner: boolean;
428
429
  }
429
430
 
431
+ export interface UFCFighterDetail {
432
+ athleteId: string;
433
+ firstName: string | null;
434
+ lastName: string | null;
435
+ fullName: string | null;
436
+ nickname: string | null;
437
+ shortName: string | null;
438
+ height: string | null;
439
+ heightInches: number | null;
440
+ weight: string | null;
441
+ weightLbs: number | null;
442
+ reach: string | null;
443
+ reachInches: number | null;
444
+ age: number | null;
445
+ dateOfBirth: string | null;
446
+ stance: string | null;
447
+ weightClass: string | null;
448
+ citizenship: string | null;
449
+ citizenshipAbbreviation: string | null;
450
+ gym: string | null;
451
+ gymCountry: string | null;
452
+ active: boolean;
453
+ headshotUrl: string | null;
454
+ flagUrl: string | null;
455
+ stanceImageUrl: string | null;
456
+ espnUrl: string | null;
457
+ slug: string | null;
458
+ }
459
+
430
460
  export interface UFCData {
431
461
  currentRound: number;
432
462
  totalRounds: number;
@@ -28,6 +28,8 @@ export interface CreateCustomGameSheetProps {
28
28
  onAmountChange?: (amount: number | null) => void;
29
29
  onSuccess?: (result: CreateCustomGameMutationResult) => void;
30
30
  onError?: (error: Error) => void;
31
+ /** Pool mode: hides buy-in selection, uses defaultAmount, auto-assigns team, shows "Create Pool" labels */
32
+ isPoolModeEnabled?: boolean;
31
33
  }
32
34
 
33
35
  const STATUS_LABELS: Record<string, string> = {
@@ -49,6 +51,7 @@ export function CreateCustomGameSheet({
49
51
  onAmountChange,
50
52
  onSuccess,
51
53
  onError,
54
+ isPoolModeEnabled = false,
52
55
  }: CreateCustomGameSheetProps) {
53
56
  const t = useDubsTheme();
54
57
  const { wallet } = useDubs();
@@ -122,20 +125,24 @@ export function CreateCustomGameSheet({
122
125
 
123
126
  const effectiveAmount = selectedAmount;
124
127
  const playerCount = maxPlayers || 2;
125
- const pot = effectiveAmount ? effectiveAmount * playerCount : 0;
128
+
129
+ // In pool mode, always use defaultAmount
130
+ const finalAmount = isPoolModeEnabled ? (defaultAmount ?? 0.1) : effectiveAmount;
131
+
132
+ const pot = finalAmount ? finalAmount * playerCount : 0;
126
133
  const winnerTakes = pot * (1 - fee / 100);
127
134
 
128
135
  const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
129
- const canCreate = effectiveAmount !== null && effectiveAmount > 0 && !isMutating && mutation.status !== 'success';
136
+ const canCreate = finalAmount !== null && finalAmount > 0 && !isMutating && mutation.status !== 'success';
130
137
 
131
138
  const handleCreate = useCallback(async () => {
132
- if (!effectiveAmount || !wallet.publicKey) return;
139
+ if (!finalAmount || !wallet.publicKey) return;
133
140
 
134
141
  try {
135
142
  await mutation.execute({
136
143
  playerWallet: wallet.publicKey.toBase58(),
137
144
  teamChoice: 'home',
138
- wagerAmount: effectiveAmount,
145
+ wagerAmount: finalAmount,
139
146
  title,
140
147
  maxPlayers,
141
148
  metadata,
@@ -143,7 +150,7 @@ export function CreateCustomGameSheet({
143
150
  } catch {
144
151
  // Error is already captured in mutation state
145
152
  }
146
- }, [effectiveAmount, wallet.publicKey, mutation.execute, title, maxPlayers, metadata]); // eslint-disable-line react-hooks/exhaustive-deps
153
+ }, [finalAmount, wallet.publicKey, mutation.execute, title, maxPlayers, metadata]); // eslint-disable-line react-hooks/exhaustive-deps
147
154
 
148
155
  const statusLabel = STATUS_LABELS[mutation.status] || '';
149
156
 
@@ -173,84 +180,86 @@ export function CreateCustomGameSheet({
173
180
 
174
181
  {/* Header */}
175
182
  <View style={styles.header}>
176
- <Text style={[styles.headerTitle, { color: t.text }]}>New Game</Text>
183
+ <Text style={[styles.headerTitle, { color: t.text }]}>{isPoolModeEnabled ? 'Create Pool' : 'New Game'}</Text>
177
184
  <TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
178
185
  <Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
179
186
  </TouchableOpacity>
180
187
  </View>
181
188
 
182
- {/* Buy-In Section */}
183
- <View style={styles.section}>
184
- <Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Buy-In Amount</Text>
185
- <View style={styles.chipsRow}>
186
- {presetAmounts.map((amount) => {
187
- const active = !isCustom && selectedAmount === amount;
188
- return (
189
- <TouchableOpacity
190
- key={amount}
191
- style={[
192
- styles.chip,
193
- { borderColor: active ? t.accent : t.border },
194
- active && { backgroundColor: t.accent },
195
- ]}
196
- onPress={() => handlePresetSelect(amount)}
197
- activeOpacity={0.8}
198
- >
199
- <Text style={[styles.chipText, { color: active ? '#FFFFFF' : t.text }]}>
200
- {amount} SOL
201
- </Text>
202
- </TouchableOpacity>
203
- );
204
- })}
205
- <TouchableOpacity
206
- style={[
207
- styles.chip,
208
- { borderColor: isCustom ? t.accent : t.border },
209
- isCustom && { backgroundColor: t.accent },
210
- ]}
211
- onPress={handleCustomSelect}
212
- activeOpacity={0.8}
213
- >
214
- <Text style={[styles.chipText, { color: isCustom ? '#FFFFFF' : t.text }]}>
215
- Custom
216
- </Text>
217
- </TouchableOpacity>
218
- </View>
189
+ {/* Buy-In Section — hidden in pool mode (uses defaultAmount) */}
190
+ {!isPoolModeEnabled && (
191
+ <View style={styles.section}>
192
+ <Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Buy-In Amount</Text>
193
+ <View style={styles.chipsRow}>
194
+ {presetAmounts.map((amount) => {
195
+ const active = !isCustom && selectedAmount === amount;
196
+ return (
197
+ <TouchableOpacity
198
+ key={amount}
199
+ style={[
200
+ styles.chip,
201
+ { borderColor: active ? t.accent : t.border },
202
+ active && { backgroundColor: t.accent },
203
+ ]}
204
+ onPress={() => handlePresetSelect(amount)}
205
+ activeOpacity={0.8}
206
+ >
207
+ <Text style={[styles.chipText, { color: active ? '#FFFFFF' : t.text }]}>
208
+ {amount} SOL
209
+ </Text>
210
+ </TouchableOpacity>
211
+ );
212
+ })}
213
+ <TouchableOpacity
214
+ style={[
215
+ styles.chip,
216
+ { borderColor: isCustom ? t.accent : t.border },
217
+ isCustom && { backgroundColor: t.accent },
218
+ ]}
219
+ onPress={handleCustomSelect}
220
+ activeOpacity={0.8}
221
+ >
222
+ <Text style={[styles.chipText, { color: isCustom ? '#FFFFFF' : t.text }]}>
223
+ Custom
224
+ </Text>
225
+ </TouchableOpacity>
226
+ </View>
219
227
 
220
- {isCustom && (
221
- <TextInput
222
- style={[styles.input, { backgroundColor: t.surface, color: t.text, borderColor: t.accent }]}
223
- placeholder="Enter amount in SOL"
224
- placeholderTextColor={t.textDim}
225
- keyboardType="decimal-pad"
226
- value={customAmount}
227
- onChangeText={handleCustomAmountChange}
228
- autoFocus
229
- />
230
- )}
231
- </View>
228
+ {isCustom && (
229
+ <TextInput
230
+ style={[styles.input, { backgroundColor: t.surface, color: t.text, borderColor: t.accent }]}
231
+ placeholder="Enter amount in SOL"
232
+ placeholderTextColor={t.textDim}
233
+ keyboardType="decimal-pad"
234
+ value={customAmount}
235
+ onChangeText={handleCustomAmountChange}
236
+ autoFocus
237
+ />
238
+ )}
239
+ </View>
240
+ )}
232
241
 
233
242
  {/* Summary Card */}
234
243
  <View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
235
244
  <View style={styles.summaryRow}>
236
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your buy-in</Text>
245
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
237
246
  <Text style={[styles.summaryValue, { color: t.text }]}>
238
- {effectiveAmount ? `${effectiveAmount} SOL` : '—'}
247
+ {finalAmount ? `${finalAmount} SOL` : '—'}
239
248
  </Text>
240
249
  </View>
241
250
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
242
251
  <View style={styles.summaryRow}>
243
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Players</Text>
244
- <Text style={[styles.summaryValue, { color: t.text }]}>{playersLabel}</Text>
252
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>{isPoolModeEnabled ? 'Max players' : 'Players'}</Text>
253
+ <Text style={[styles.summaryValue, { color: t.text }]}>{isPoolModeEnabled ? playerCount : playersLabel}</Text>
245
254
  </View>
246
255
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
247
256
  <View style={styles.summaryRow}>
248
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Winner Takes</Text>
257
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>{isPoolModeEnabled ? 'Max pot' : 'Winner Takes'}</Text>
249
258
  <View style={styles.winnerCol}>
250
259
  <Text style={[styles.summaryValue, { color: t.success }]}>
251
- {effectiveAmount ? `${winnerTakes.toFixed(4)} SOL` : '—'}
260
+ {finalAmount ? `${(finalAmount * playerCount * (1 - fee / 100)).toFixed(4)} SOL` : '—'}
252
261
  </Text>
253
- {effectiveAmount ? (
262
+ {finalAmount ? (
254
263
  <Text style={[styles.feeNote, { color: t.textDim }]}>{fee}% platform fee</Text>
255
264
  ) : null}
256
265
  </View>
@@ -280,10 +289,12 @@ export function CreateCustomGameSheet({
280
289
  <Text style={styles.ctaText}>{statusLabel}</Text>
281
290
  </View>
282
291
  ) : mutation.status === 'success' ? (
283
- <Text style={styles.ctaText}>{STATUS_LABELS.success}</Text>
292
+ <Text style={styles.ctaText}>{isPoolModeEnabled ? 'Pool Created!' : STATUS_LABELS.success}</Text>
284
293
  ) : (
285
294
  <Text style={[styles.ctaText, !canCreate && { opacity: 0.5 }]}>
286
- {effectiveAmount ? `Create Game \u2014 ${effectiveAmount} SOL` : 'Select buy-in amount'}
295
+ {isPoolModeEnabled
296
+ ? `Create Pool \u2014 ${finalAmount} SOL`
297
+ : effectiveAmount ? `Create Game \u2014 ${effectiveAmount} SOL` : 'Select buy-in amount'}
287
298
  </Text>
288
299
  )}
289
300
  </TouchableOpacity>
@@ -30,6 +30,8 @@ export interface JoinGameSheetProps {
30
30
  /** Callbacks */
31
31
  onSuccess?: (result: JoinGameMutationResult) => void;
32
32
  onError?: (error: Error) => void;
33
+ /** Pool mode: hides team selection, auto-assigns team, shows "Join Pool" labels */
34
+ isPoolModeEnabled?: boolean;
33
35
  }
34
36
 
35
37
  const STATUS_LABELS: Record<string, string> = {
@@ -51,6 +53,7 @@ export function JoinGameSheet({
51
53
  awayColor = '#EF4444',
52
54
  onSuccess,
53
55
  onError,
56
+ isPoolModeEnabled = false,
54
57
  }: JoinGameSheetProps) {
55
58
  const t = useDubsTheme();
56
59
  const { wallet } = useDubs();
@@ -58,7 +61,7 @@ export function JoinGameSheet({
58
61
 
59
62
  const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
60
63
 
61
- // For custom games the joiner is always away — no team selection needed.
64
+ // Pool mode and custom games auto-assign team — no team selection needed.
62
65
  // For sports/esports games the user picks a team.
63
66
  const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
64
67
 
@@ -76,7 +79,7 @@ export function JoinGameSheet({
76
79
  // Reset state when sheet opens
77
80
  useEffect(() => {
78
81
  if (visible) {
79
- setSelectedTeam(isCustomGame ? 'away' : null);
82
+ setSelectedTeam(isPoolModeEnabled ? 'home' : isCustomGame ? 'away' : null);
80
83
  mutation.reset();
81
84
  }
82
85
  }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -173,14 +176,14 @@ export function JoinGameSheet({
173
176
 
174
177
  {/* Header */}
175
178
  <View style={styles.header}>
176
- <Text style={[styles.headerTitle, { color: t.text }]}>Join Game</Text>
179
+ <Text style={[styles.headerTitle, { color: t.text }]}>{isPoolModeEnabled ? 'Join Pool' : 'Join Game'}</Text>
177
180
  <TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
178
181
  <Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
179
182
  </TouchableOpacity>
180
183
  </View>
181
184
 
182
- {/* Team Selection — only for non-custom (sports/esports) games */}
183
- {!isCustomGame && (
185
+ {/* Team Selection — hidden in pool mode and custom games */}
186
+ {!isCustomGame && !isPoolModeEnabled && (
184
187
  <View style={styles.section}>
185
188
  <Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Pick Your Side</Text>
186
189
  <View style={styles.teamsRow}>
@@ -217,28 +220,44 @@ export function JoinGameSheet({
217
220
  <Text style={[styles.summaryValue, { color: t.text }]}>{buyIn} SOL</Text>
218
221
  </View>
219
222
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
220
- <View style={styles.summaryRow}>
221
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your side</Text>
222
- <Text style={[styles.summaryValue, { color: t.text }]}>{selectedName}</Text>
223
- </View>
224
- <View style={[styles.summarySep, { backgroundColor: t.border }]} />
225
- <View style={styles.summaryRow}>
226
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
227
- <Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
228
- </View>
229
- <View style={[styles.summarySep, { backgroundColor: t.border }]} />
230
- <View style={styles.summaryRow}>
231
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Potential winnings</Text>
232
- <Text style={[styles.summaryValue, { color: t.success }]}>
233
- {potentialWinnings !== '—' ? `${potentialWinnings} SOL` : '—'}
234
- </Text>
235
- </View>
223
+ {isPoolModeEnabled ? (
224
+ <>
225
+ <View style={styles.summaryRow}>
226
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Players in</Text>
227
+ <Text style={[styles.summaryValue, { color: t.text }]}>{bettors.length}</Text>
228
+ </View>
229
+ <View style={[styles.summarySep, { backgroundColor: t.border }]} />
230
+ <View style={styles.summaryRow}>
231
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Current pot</Text>
232
+ <Text style={[styles.summaryValue, { color: t.success }]}>{totalPool} SOL</Text>
233
+ </View>
234
+ </>
235
+ ) : (
236
+ <>
237
+ <View style={styles.summaryRow}>
238
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your side</Text>
239
+ <Text style={[styles.summaryValue, { color: t.text }]}>{selectedName}</Text>
240
+ </View>
241
+ <View style={[styles.summarySep, { backgroundColor: t.border }]} />
242
+ <View style={styles.summaryRow}>
243
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
244
+ <Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
245
+ </View>
246
+ <View style={[styles.summarySep, { backgroundColor: t.border }]} />
247
+ <View style={styles.summaryRow}>
248
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Potential winnings</Text>
249
+ <Text style={[styles.summaryValue, { color: t.success }]}>
250
+ {potentialWinnings !== '—' ? `${potentialWinnings} SOL` : '—'}
251
+ </Text>
252
+ </View>
253
+ </>
254
+ )}
236
255
  </View>
237
256
 
238
257
  {/* Already Joined Notice */}
239
258
  {alreadyJoined && (
240
259
  <View style={[styles.errorBox, { backgroundColor: t.surface, borderColor: t.border }]}>
241
- <Text style={[styles.errorText, { color: t.textMuted }]}>You've already joined this game.</Text>
260
+ <Text style={[styles.errorText, { color: t.textMuted }]}>{isPoolModeEnabled ? "You've already joined this pool." : "You've already joined this game."}</Text>
242
261
  </View>
243
262
  )}
244
263
 
@@ -265,14 +284,16 @@ export function JoinGameSheet({
265
284
  <Text style={styles.ctaText}>{statusLabel}</Text>
266
285
  </View>
267
286
  ) : mutation.status === 'success' ? (
268
- <Text style={styles.ctaText}>{STATUS_LABELS.success}</Text>
287
+ <Text style={styles.ctaText}>{isPoolModeEnabled ? 'Joined!' : STATUS_LABELS.success}</Text>
269
288
  ) : (
270
289
  <Text style={[styles.ctaText, !canJoin && { opacity: 0.5 }]}>
271
290
  {alreadyJoined
272
291
  ? 'Already Joined'
273
- : selectedTeam
274
- ? `Join Game \u2014 ${buyIn} SOL`
275
- : 'Pick a side to join'}
292
+ : isPoolModeEnabled
293
+ ? `Join Pool \u2014 ${buyIn} SOL`
294
+ : selectedTeam
295
+ ? `Join Game \u2014 ${buyIn} SOL`
296
+ : 'Pick a side to join'}
276
297
  </Text>
277
298
  )}
278
299
  </TouchableOpacity>