@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/README.md +45 -4
- package/dist/index.d.mts +39 -3
- package/dist/index.d.ts +39 -3
- package/dist/index.js +217 -128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +191 -103
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +9 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useUFCFighterDetail.ts +34 -0
- package/src/index.ts +2 -0
- package/src/managed-wallet.tsx +49 -5
- package/src/types.ts +30 -0
- package/src/ui/game/CreateCustomGameSheet.tsx +75 -64
- package/src/ui/game/JoinGameSheet.tsx +47 -26
package/package.json
CHANGED
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> {
|
package/src/hooks/index.ts
CHANGED
|
@@ -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,
|
package/src/managed-wallet.tsx
CHANGED
|
@@ -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
|
|
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'
|
|
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: ${
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
136
|
+
const canCreate = finalAmount !== null && finalAmount > 0 && !isMutating && mutation.status !== 'success';
|
|
130
137
|
|
|
131
138
|
const handleCreate = useCallback(async () => {
|
|
132
|
-
if (!
|
|
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:
|
|
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
|
-
}, [
|
|
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
|
-
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
{
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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 }]}>
|
|
245
|
+
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
|
|
237
246
|
<Text style={[styles.summaryValue, { color: t.text }]}>
|
|
238
|
-
{
|
|
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
|
-
{
|
|
260
|
+
{finalAmount ? `${(finalAmount * playerCount * (1 - fee / 100)).toFixed(4)} SOL` : '—'}
|
|
252
261
|
</Text>
|
|
253
|
-
{
|
|
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
|
-
{
|
|
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
|
-
//
|
|
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 —
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
|
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
|
-
:
|
|
274
|
-
? `Join
|
|
275
|
-
:
|
|
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>
|