@dubsdotapp/expo 0.5.6 → 0.5.7
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/dist/index.d.mts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +117 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +120 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ui/game/JoinGameSheet.tsx +145 -11
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
|
+
Image,
|
|
5
6
|
TouchableOpacity,
|
|
6
7
|
ActivityIndicator,
|
|
7
8
|
Modal,
|
|
@@ -32,6 +33,8 @@ export interface JoinGameSheetProps {
|
|
|
32
33
|
onError?: (error: Error) => void;
|
|
33
34
|
/** Called when the user taps a team card (useful for sound/haptics) */
|
|
34
35
|
onTeamSelect?: (team: 'home' | 'away') => void;
|
|
36
|
+
/** Called when the join succeeds (useful for success sound/animation) */
|
|
37
|
+
onJoinSuccess?: (result: JoinGameMutationResult) => void;
|
|
35
38
|
/** Pool mode: hides team selection, auto-assigns team, shows "Join Pool" labels */
|
|
36
39
|
isPoolModeEnabled?: boolean;
|
|
37
40
|
}
|
|
@@ -45,6 +48,16 @@ const STATUS_LABELS: Record<string, string> = {
|
|
|
45
48
|
|
|
46
49
|
const CUSTOM_GAME_MODE = 6;
|
|
47
50
|
|
|
51
|
+
function formatSol(n: number): string {
|
|
52
|
+
// Remove trailing zeros: 0.06000000 → 0.06, 1.00000 → 1
|
|
53
|
+
return parseFloat(n.toFixed(9)).toString();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toPng(url: string | null | undefined): string | null {
|
|
57
|
+
if (!url) return null;
|
|
58
|
+
return url.replace('/svg?', '/png?');
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
export function JoinGameSheet({
|
|
49
62
|
visible,
|
|
50
63
|
onDismiss,
|
|
@@ -56,6 +69,7 @@ export function JoinGameSheet({
|
|
|
56
69
|
onSuccess,
|
|
57
70
|
onError,
|
|
58
71
|
onTeamSelect,
|
|
72
|
+
onJoinSuccess,
|
|
59
73
|
isPoolModeEnabled = false,
|
|
60
74
|
}: JoinGameSheetProps) {
|
|
61
75
|
const t = useDubsTheme();
|
|
@@ -64,11 +78,12 @@ export function JoinGameSheet({
|
|
|
64
78
|
|
|
65
79
|
const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
|
|
66
80
|
|
|
67
|
-
// Pool mode and custom games auto-assign team — no team selection needed.
|
|
68
|
-
// For sports/esports games the user picks a team.
|
|
69
81
|
const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
|
|
82
|
+
const [showSuccess, setShowSuccess] = useState(false);
|
|
70
83
|
|
|
71
84
|
const overlayOpacity = useRef(new Animated.Value(0)).current;
|
|
85
|
+
const successScale = useRef(new Animated.Value(0)).current;
|
|
86
|
+
const successOpacity = useRef(new Animated.Value(0)).current;
|
|
72
87
|
|
|
73
88
|
// Animate overlay on visibility change
|
|
74
89
|
useEffect(() => {
|
|
@@ -83,17 +98,31 @@ export function JoinGameSheet({
|
|
|
83
98
|
useEffect(() => {
|
|
84
99
|
if (visible) {
|
|
85
100
|
setSelectedTeam(isPoolModeEnabled ? 'home' : isCustomGame ? 'away' : null);
|
|
101
|
+
setShowSuccess(false);
|
|
102
|
+
successScale.setValue(0);
|
|
103
|
+
successOpacity.setValue(0);
|
|
86
104
|
mutation.reset();
|
|
87
105
|
}
|
|
88
106
|
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
89
107
|
|
|
90
|
-
//
|
|
108
|
+
// Handle success with animation
|
|
91
109
|
useEffect(() => {
|
|
92
110
|
if (mutation.status === 'success' && mutation.data) {
|
|
111
|
+
setShowSuccess(true);
|
|
93
112
|
onSuccess?.(mutation.data);
|
|
113
|
+
onJoinSuccess?.(mutation.data);
|
|
114
|
+
|
|
115
|
+
// Animate success
|
|
116
|
+
Animated.parallel([
|
|
117
|
+
Animated.spring(successScale, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
|
|
118
|
+
Animated.timing(successOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
|
|
119
|
+
]).start();
|
|
120
|
+
|
|
94
121
|
const timer = setTimeout(() => {
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
Animated.timing(successOpacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
|
|
123
|
+
onDismiss();
|
|
124
|
+
});
|
|
125
|
+
}, 2500);
|
|
97
126
|
return () => clearTimeout(timer);
|
|
98
127
|
}
|
|
99
128
|
}, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
@@ -123,7 +152,7 @@ export function JoinGameSheet({
|
|
|
123
152
|
|
|
124
153
|
const poolAfterJoin = totalPool + buyIn;
|
|
125
154
|
const selectedOdds = selectedTeam === 'home' ? homeOdds : selectedTeam === 'away' ? awayOdds : '—';
|
|
126
|
-
const potentialWinnings = selectedOdds !== '—' ? (parseFloat(selectedOdds) * buyIn)
|
|
155
|
+
const potentialWinnings = selectedOdds !== '—' ? formatSol(parseFloat(selectedOdds) * buyIn) : '—';
|
|
127
156
|
|
|
128
157
|
const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
|
|
129
158
|
const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
|
|
@@ -166,6 +195,19 @@ export function JoinGameSheet({
|
|
|
166
195
|
<TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
|
|
167
196
|
</Animated.View>
|
|
168
197
|
|
|
198
|
+
{/* Success overlay */}
|
|
199
|
+
{showSuccess && (
|
|
200
|
+
<View style={styles.successOverlay}>
|
|
201
|
+
<Animated.View style={[styles.successContent, { opacity: successOpacity, transform: [{ scale: successScale }] }]}>
|
|
202
|
+
<Text style={styles.successEmoji}>🎉</Text>
|
|
203
|
+
<Text style={styles.successTitle}>You're in!</Text>
|
|
204
|
+
<Text style={styles.successSub}>
|
|
205
|
+
{formatSol(buyIn)} SOL on {selectedName}
|
|
206
|
+
</Text>
|
|
207
|
+
</Animated.View>
|
|
208
|
+
</View>
|
|
209
|
+
)}
|
|
210
|
+
|
|
169
211
|
<KeyboardAvoidingView
|
|
170
212
|
style={styles.keyboardView}
|
|
171
213
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
@@ -185,6 +227,34 @@ export function JoinGameSheet({
|
|
|
185
227
|
</TouchableOpacity>
|
|
186
228
|
</View>
|
|
187
229
|
|
|
230
|
+
{/* Bettors row */}
|
|
231
|
+
{bettors.length > 0 && (
|
|
232
|
+
<View style={styles.bettorsSection}>
|
|
233
|
+
<Text style={[styles.bettorsLabel, { color: t.textMuted }]}>
|
|
234
|
+
{bettors.length} {bettors.length === 1 ? 'player' : 'players'} in this game
|
|
235
|
+
</Text>
|
|
236
|
+
<View style={styles.bettorsRow}>
|
|
237
|
+
{bettors.slice(0, 6).map((b, i) => {
|
|
238
|
+
const pngUrl = toPng(b.avatar);
|
|
239
|
+
return (
|
|
240
|
+
<View key={b.wallet} style={[styles.bettorCircle, { backgroundColor: ['#EF4444','#3B82F6','#22C55E','#F59E0B','#A855F7','#EC4899'][i % 6] }]}>
|
|
241
|
+
{pngUrl ? (
|
|
242
|
+
<Image source={{ uri: pngUrl }} style={styles.bettorImg} />
|
|
243
|
+
) : (
|
|
244
|
+
<Text style={styles.bettorInitial}>{(b.username ?? b.wallet).charAt(0).toUpperCase()}</Text>
|
|
245
|
+
)}
|
|
246
|
+
</View>
|
|
247
|
+
);
|
|
248
|
+
})}
|
|
249
|
+
{bettors.length > 6 && (
|
|
250
|
+
<View style={[styles.bettorCircle, { backgroundColor: '#2C2C2E' }]}>
|
|
251
|
+
<Text style={styles.bettorOverflow}>+{bettors.length - 6}</Text>
|
|
252
|
+
</View>
|
|
253
|
+
)}
|
|
254
|
+
</View>
|
|
255
|
+
</View>
|
|
256
|
+
)}
|
|
257
|
+
|
|
188
258
|
{/* Team Selection — hidden in pool mode and custom games */}
|
|
189
259
|
{!isCustomGame && !isPoolModeEnabled && (
|
|
190
260
|
<View style={styles.section}>
|
|
@@ -220,7 +290,7 @@ export function JoinGameSheet({
|
|
|
220
290
|
<View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
|
|
221
291
|
<View style={styles.summaryRow}>
|
|
222
292
|
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
|
|
223
|
-
<Text style={[styles.summaryValue, { color: t.text }]}>{buyIn} SOL</Text>
|
|
293
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(buyIn)} SOL</Text>
|
|
224
294
|
</View>
|
|
225
295
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
226
296
|
{isPoolModeEnabled ? (
|
|
@@ -232,7 +302,7 @@ export function JoinGameSheet({
|
|
|
232
302
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
233
303
|
<View style={styles.summaryRow}>
|
|
234
304
|
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Current pot</Text>
|
|
235
|
-
<Text style={[styles.summaryValue, { color: t.success }]}>{totalPool} SOL</Text>
|
|
305
|
+
<Text style={[styles.summaryValue, { color: t.success }]}>{formatSol(totalPool)} SOL</Text>
|
|
236
306
|
</View>
|
|
237
307
|
</>
|
|
238
308
|
) : (
|
|
@@ -244,7 +314,7 @@ export function JoinGameSheet({
|
|
|
244
314
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
245
315
|
<View style={styles.summaryRow}>
|
|
246
316
|
<Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
|
|
247
|
-
<Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
|
|
317
|
+
<Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(poolAfterJoin)} SOL</Text>
|
|
248
318
|
</View>
|
|
249
319
|
<View style={[styles.summarySep, { backgroundColor: t.border }]} />
|
|
250
320
|
<View style={styles.summaryRow}>
|
|
@@ -293,9 +363,9 @@ export function JoinGameSheet({
|
|
|
293
363
|
{alreadyJoined
|
|
294
364
|
? 'Already Joined'
|
|
295
365
|
: isPoolModeEnabled
|
|
296
|
-
? `Join Pool \u2014 ${buyIn} SOL`
|
|
366
|
+
? `Join Pool \u2014 ${formatSol(buyIn)} SOL`
|
|
297
367
|
: selectedTeam
|
|
298
|
-
? `Join Game \u2014 ${buyIn} SOL`
|
|
368
|
+
? `Join Game \u2014 ${formatSol(buyIn)} SOL`
|
|
299
369
|
: 'Pick a side to join'}
|
|
300
370
|
</Text>
|
|
301
371
|
)}
|
|
@@ -389,6 +459,70 @@ const styles = StyleSheet.create({
|
|
|
389
459
|
fontSize: 20,
|
|
390
460
|
padding: 4,
|
|
391
461
|
},
|
|
462
|
+
|
|
463
|
+
// Bettors row
|
|
464
|
+
bettorsSection: {
|
|
465
|
+
paddingBottom: 12,
|
|
466
|
+
gap: 8,
|
|
467
|
+
},
|
|
468
|
+
bettorsLabel: {
|
|
469
|
+
fontSize: 12,
|
|
470
|
+
fontWeight: '600',
|
|
471
|
+
},
|
|
472
|
+
bettorsRow: {
|
|
473
|
+
flexDirection: 'row',
|
|
474
|
+
alignItems: 'center',
|
|
475
|
+
gap: 4,
|
|
476
|
+
},
|
|
477
|
+
bettorCircle: {
|
|
478
|
+
width: 28,
|
|
479
|
+
height: 28,
|
|
480
|
+
borderRadius: 14,
|
|
481
|
+
alignItems: 'center',
|
|
482
|
+
justifyContent: 'center',
|
|
483
|
+
overflow: 'hidden',
|
|
484
|
+
},
|
|
485
|
+
bettorImg: {
|
|
486
|
+
width: 28,
|
|
487
|
+
height: 28,
|
|
488
|
+
borderRadius: 14,
|
|
489
|
+
},
|
|
490
|
+
bettorInitial: {
|
|
491
|
+
color: '#FFF',
|
|
492
|
+
fontSize: 11,
|
|
493
|
+
fontWeight: '800',
|
|
494
|
+
},
|
|
495
|
+
bettorOverflow: {
|
|
496
|
+
color: '#8E8E93',
|
|
497
|
+
fontSize: 10,
|
|
498
|
+
fontWeight: '700',
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
// Success overlay
|
|
502
|
+
successOverlay: {
|
|
503
|
+
...StyleSheet.absoluteFillObject,
|
|
504
|
+
zIndex: 100,
|
|
505
|
+
alignItems: 'center',
|
|
506
|
+
justifyContent: 'center',
|
|
507
|
+
backgroundColor: 'rgba(0,0,0,0.85)',
|
|
508
|
+
},
|
|
509
|
+
successContent: {
|
|
510
|
+
alignItems: 'center',
|
|
511
|
+
gap: 12,
|
|
512
|
+
},
|
|
513
|
+
successEmoji: {
|
|
514
|
+
fontSize: 64,
|
|
515
|
+
},
|
|
516
|
+
successTitle: {
|
|
517
|
+
color: '#FFFFFF',
|
|
518
|
+
fontSize: 28,
|
|
519
|
+
fontWeight: '900',
|
|
520
|
+
},
|
|
521
|
+
successSub: {
|
|
522
|
+
color: '#8E8E93',
|
|
523
|
+
fontSize: 16,
|
|
524
|
+
},
|
|
525
|
+
|
|
392
526
|
section: {
|
|
393
527
|
gap: 12,
|
|
394
528
|
paddingTop: 8,
|