@dubsdotapp/expo 0.5.5 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dubsdotapp/expo",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -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,
@@ -30,6 +31,10 @@ export interface JoinGameSheetProps {
30
31
  /** Callbacks */
31
32
  onSuccess?: (result: JoinGameMutationResult) => void;
32
33
  onError?: (error: Error) => void;
34
+ /** Called when the user taps a team card (useful for sound/haptics) */
35
+ onTeamSelect?: (team: 'home' | 'away') => void;
36
+ /** Called when the join succeeds (useful for success sound/animation) */
37
+ onJoinSuccess?: (result: JoinGameMutationResult) => void;
33
38
  /** Pool mode: hides team selection, auto-assigns team, shows "Join Pool" labels */
34
39
  isPoolModeEnabled?: boolean;
35
40
  }
@@ -43,6 +48,16 @@ const STATUS_LABELS: Record<string, string> = {
43
48
 
44
49
  const CUSTOM_GAME_MODE = 6;
45
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
+
46
61
  export function JoinGameSheet({
47
62
  visible,
48
63
  onDismiss,
@@ -53,6 +68,8 @@ export function JoinGameSheet({
53
68
  awayColor = '#EF4444',
54
69
  onSuccess,
55
70
  onError,
71
+ onTeamSelect,
72
+ onJoinSuccess,
56
73
  isPoolModeEnabled = false,
57
74
  }: JoinGameSheetProps) {
58
75
  const t = useDubsTheme();
@@ -61,11 +78,12 @@ export function JoinGameSheet({
61
78
 
62
79
  const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
63
80
 
64
- // Pool mode and custom games auto-assign team — no team selection needed.
65
- // For sports/esports games the user picks a team.
66
81
  const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
82
+ const [showSuccess, setShowSuccess] = useState(false);
67
83
 
68
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;
69
87
 
70
88
  // Animate overlay on visibility change
71
89
  useEffect(() => {
@@ -80,17 +98,31 @@ export function JoinGameSheet({
80
98
  useEffect(() => {
81
99
  if (visible) {
82
100
  setSelectedTeam(isPoolModeEnabled ? 'home' : isCustomGame ? 'away' : null);
101
+ setShowSuccess(false);
102
+ successScale.setValue(0);
103
+ successOpacity.setValue(0);
83
104
  mutation.reset();
84
105
  }
85
106
  }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
86
107
 
87
- // Auto-dismiss on success
108
+ // Handle success with animation
88
109
  useEffect(() => {
89
110
  if (mutation.status === 'success' && mutation.data) {
111
+ setShowSuccess(true);
90
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
+
91
121
  const timer = setTimeout(() => {
92
- onDismiss();
93
- }, 1500);
122
+ Animated.timing(successOpacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
123
+ onDismiss();
124
+ });
125
+ }, 2500);
94
126
  return () => clearTimeout(timer);
95
127
  }
96
128
  }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -120,7 +152,7 @@ export function JoinGameSheet({
120
152
 
121
153
  const poolAfterJoin = totalPool + buyIn;
122
154
  const selectedOdds = selectedTeam === 'home' ? homeOdds : selectedTeam === 'away' ? awayOdds : '—';
123
- const potentialWinnings = selectedOdds !== '—' ? (parseFloat(selectedOdds) * buyIn).toFixed(4) : '—';
155
+ const potentialWinnings = selectedOdds !== '—' ? formatSol(parseFloat(selectedOdds) * buyIn) : '—';
124
156
 
125
157
  const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
126
158
  const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
@@ -163,6 +195,19 @@ export function JoinGameSheet({
163
195
  <TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
164
196
  </Animated.View>
165
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
+
166
211
  <KeyboardAvoidingView
167
212
  style={styles.keyboardView}
168
213
  behavior={Platform.OS === 'ios' ? 'padding' : undefined}
@@ -182,6 +227,34 @@ export function JoinGameSheet({
182
227
  </TouchableOpacity>
183
228
  </View>
184
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
+
185
258
  {/* Team Selection — hidden in pool mode and custom games */}
186
259
  {!isCustomGame && !isPoolModeEnabled && (
187
260
  <View style={styles.section}>
@@ -194,7 +267,7 @@ export function JoinGameSheet({
194
267
  bets={homeBets}
195
268
  color={homeColor}
196
269
  selected={selectedTeam === 'home'}
197
- onPress={() => setSelectedTeam('home')}
270
+ onPress={() => { setSelectedTeam('home'); onTeamSelect?.('home'); }}
198
271
  ImageComponent={ImageComponent}
199
272
  t={t}
200
273
  />
@@ -205,7 +278,7 @@ export function JoinGameSheet({
205
278
  bets={awayBets}
206
279
  color={awayColor}
207
280
  selected={selectedTeam === 'away'}
208
- onPress={() => setSelectedTeam('away')}
281
+ onPress={() => { setSelectedTeam('away'); onTeamSelect?.('away'); }}
209
282
  ImageComponent={ImageComponent}
210
283
  t={t}
211
284
  />
@@ -217,7 +290,7 @@ export function JoinGameSheet({
217
290
  <View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
218
291
  <View style={styles.summaryRow}>
219
292
  <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
220
- <Text style={[styles.summaryValue, { color: t.text }]}>{buyIn} SOL</Text>
293
+ <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(buyIn)} SOL</Text>
221
294
  </View>
222
295
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
223
296
  {isPoolModeEnabled ? (
@@ -229,7 +302,7 @@ export function JoinGameSheet({
229
302
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
230
303
  <View style={styles.summaryRow}>
231
304
  <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Current pot</Text>
232
- <Text style={[styles.summaryValue, { color: t.success }]}>{totalPool} SOL</Text>
305
+ <Text style={[styles.summaryValue, { color: t.success }]}>{formatSol(totalPool)} SOL</Text>
233
306
  </View>
234
307
  </>
235
308
  ) : (
@@ -241,7 +314,7 @@ export function JoinGameSheet({
241
314
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
242
315
  <View style={styles.summaryRow}>
243
316
  <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
244
- <Text style={[styles.summaryValue, { color: t.text }]}>{poolAfterJoin} SOL</Text>
317
+ <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(poolAfterJoin)} SOL</Text>
245
318
  </View>
246
319
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
247
320
  <View style={styles.summaryRow}>
@@ -290,9 +363,9 @@ export function JoinGameSheet({
290
363
  {alreadyJoined
291
364
  ? 'Already Joined'
292
365
  : isPoolModeEnabled
293
- ? `Join Pool \u2014 ${buyIn} SOL`
366
+ ? `Join Pool \u2014 ${formatSol(buyIn)} SOL`
294
367
  : selectedTeam
295
- ? `Join Game \u2014 ${buyIn} SOL`
368
+ ? `Join Game \u2014 ${formatSol(buyIn)} SOL`
296
369
  : 'Pick a side to join'}
297
370
  </Text>
298
371
  )}
@@ -386,6 +459,70 @@ const styles = StyleSheet.create({
386
459
  fontSize: 20,
387
460
  padding: 4,
388
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
+
389
526
  section: {
390
527
  gap: 12,
391
528
  paddingTop: 8,