@dubsdotapp/expo 0.5.7 → 0.5.9

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.7",
3
+ "version": "0.5.9",
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/index.ts CHANGED
@@ -134,7 +134,8 @@ export { AuthGate, ConnectWalletScreen, ConnectWalletButton, UserProfileCard, Se
134
134
  export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, ConnectWalletButtonProps, AuthGateConnectWalletProps, UserProfileCardProps, SettingsSheetProps, UserProfileSheetProps, DubsTheme } from './ui';
135
135
 
136
136
  // Game widgets
137
- export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet } from './ui';
137
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './ui';
138
+ export type { SolSliderProps } from './ui';
138
139
  export type {
139
140
  GamePosterProps,
140
141
  LivePoolsCardProps,
@@ -16,6 +16,7 @@ import { useDubs } from '../../provider';
16
16
  import { useJoinGame } from '../../hooks/useJoinGame';
17
17
  import type { JoinGameMutationResult } from '../../hooks/useJoinGame';
18
18
  import type { GameDetail } from '../../types';
19
+ import { SolSlider } from './SolSlider';
19
20
 
20
21
  export interface JoinGameSheetProps {
21
22
  visible: boolean;
@@ -35,6 +36,10 @@ export interface JoinGameSheetProps {
35
36
  onTeamSelect?: (team: 'home' | 'away') => void;
36
37
  /** Called when the join succeeds (useful for success sound/animation) */
37
38
  onJoinSuccess?: (result: JoinGameMutationResult) => void;
39
+ /** Called on each slider tick (useful for haptics/sound) */
40
+ onSliderTick?: (value: number) => void;
41
+ /** Max wager in SOL (default 5) */
42
+ maxWager?: number;
38
43
  /** Pool mode: hides team selection, auto-assigns team, shows "Join Pool" labels */
39
44
  isPoolModeEnabled?: boolean;
40
45
  }
@@ -70,6 +75,8 @@ export function JoinGameSheet({
70
75
  onError,
71
76
  onTeamSelect,
72
77
  onJoinSuccess,
78
+ onSliderTick,
79
+ maxWager = 5,
73
80
  isPoolModeEnabled = false,
74
81
  }: JoinGameSheetProps) {
75
82
  const t = useDubsTheme();
@@ -79,6 +86,7 @@ export function JoinGameSheet({
79
86
  const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
80
87
 
81
88
  const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
89
+ const [wager, setWager] = useState(game.buyIn);
82
90
  const [showSuccess, setShowSuccess] = useState(false);
83
91
 
84
92
  const overlayOpacity = useRef(new Animated.Value(0)).current;
@@ -98,6 +106,7 @@ export function JoinGameSheet({
98
106
  useEffect(() => {
99
107
  if (visible) {
100
108
  setSelectedTeam(isPoolModeEnabled ? 'home' : isCustomGame ? 'away' : null);
109
+ setWager(game.buyIn);
101
110
  setShowSuccess(false);
102
111
  successScale.setValue(0);
103
112
  successOpacity.setValue(0);
@@ -143,20 +152,23 @@ export function JoinGameSheet({
143
152
  const awayPool = game.awayPool || 0;
144
153
  const buyIn = game.buyIn;
145
154
 
146
- const { homeOdds, awayOdds, homeBets, awayBets } = useMemo(() => ({
147
- homeOdds: homePool > 0 ? (totalPool / homePool).toFixed(2) : '—',
148
- awayOdds: awayPool > 0 ? (totalPool / awayPool).toFixed(2) : '—',
149
- homeBets: bettors.filter(b => b.team === 'home').length,
150
- awayBets: bettors.filter(b => b.team === 'away').length,
151
- }), [totalPool, homePool, awayPool, bettors]);
155
+ const poolAfterJoin = totalPool + wager;
156
+
157
+ const { homeOdds, awayOdds, homeBets, awayBets } = useMemo(() => {
158
+ const newPool = totalPool + wager;
159
+ return {
160
+ homeOdds: homePool > 0 ? (newPool / (homePool + (selectedTeam === 'home' ? wager : 0))).toFixed(2) : '—',
161
+ awayOdds: awayPool > 0 ? (newPool / (awayPool + (selectedTeam === 'away' ? wager : 0))).toFixed(2) : '—',
162
+ homeBets: bettors.filter(b => b.team === 'home').length,
163
+ awayBets: bettors.filter(b => b.team === 'away').length,
164
+ };
165
+ }, [totalPool, homePool, awayPool, bettors, wager, selectedTeam]);
152
166
 
153
- const poolAfterJoin = totalPool + buyIn;
154
167
  const selectedOdds = selectedTeam === 'home' ? homeOdds : selectedTeam === 'away' ? awayOdds : '—';
155
- const potentialWinnings = selectedOdds !== '—' ? formatSol(parseFloat(selectedOdds) * buyIn) : '—';
168
+ const potentialWinnings = selectedOdds !== '—' ? formatSol(parseFloat(selectedOdds) * wager) : '—';
156
169
 
157
170
  const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
158
171
  const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
159
- const selectedName = selectedTeam === 'home' ? homeName : selectedTeam === 'away' ? awayName : '—';
160
172
 
161
173
  const alreadyJoined = useMemo(() => {
162
174
  if (!wallet.publicKey) return false;
@@ -175,12 +187,12 @@ export function JoinGameSheet({
175
187
  playerWallet: wallet.publicKey.toBase58(),
176
188
  gameId: game.gameId,
177
189
  teamChoice: selectedTeam,
178
- amount: buyIn,
190
+ amount: wager,
179
191
  });
180
192
  } catch {
181
193
  // Error is already captured in mutation state
182
194
  }
183
- }, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, buyIn]); // eslint-disable-line react-hooks/exhaustive-deps
195
+ }, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, wager]); // eslint-disable-line react-hooks/exhaustive-deps
184
196
 
185
197
  const statusLabel = STATUS_LABELS[mutation.status] || '';
186
198
 
@@ -289,8 +301,8 @@ export function JoinGameSheet({
289
301
  {/* Summary Card */}
290
302
  <View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
291
303
  <View style={styles.summaryRow}>
292
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Buy-in</Text>
293
- <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(buyIn)} SOL</Text>
304
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your wager</Text>
305
+ <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(wager)} SOL</Text>
294
306
  </View>
295
307
  <View style={[styles.summarySep, { backgroundColor: t.border }]} />
296
308
  {isPoolModeEnabled ? (
@@ -307,11 +319,6 @@ export function JoinGameSheet({
307
319
  </>
308
320
  ) : (
309
321
  <>
310
- <View style={styles.summaryRow}>
311
- <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your side</Text>
312
- <Text style={[styles.summaryValue, { color: t.text }]}>{selectedName}</Text>
313
- </View>
314
- <View style={[styles.summarySep, { backgroundColor: t.border }]} />
315
322
  <View style={styles.summaryRow}>
316
323
  <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Total pool</Text>
317
324
  <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(poolAfterJoin)} SOL</Text>
@@ -327,6 +334,19 @@ export function JoinGameSheet({
327
334
  )}
328
335
  </View>
329
336
 
337
+ {/* SOL Slider — sits right below summary card */}
338
+ {selectedTeam && !isPoolModeEnabled && (
339
+ <SolSlider
340
+ value={wager}
341
+ min={game.buyIn}
342
+ max={maxWager}
343
+ step={0.01}
344
+ accentColor={selectedTeam === 'home' ? homeColor : awayColor}
345
+ onValueChange={setWager}
346
+ onTick={onSliderTick}
347
+ />
348
+ )}
349
+
330
350
  {/* Already Joined Notice */}
331
351
  {alreadyJoined && (
332
352
  <View style={[styles.errorBox, { backgroundColor: t.surface, borderColor: t.border }]}>
@@ -363,9 +383,9 @@ export function JoinGameSheet({
363
383
  {alreadyJoined
364
384
  ? 'Already Joined'
365
385
  : isPoolModeEnabled
366
- ? `Join Pool \u2014 ${formatSol(buyIn)} SOL`
386
+ ? `Join Pool \u2014 ${formatSol(wager)} SOL`
367
387
  : selectedTeam
368
- ? `Join Game \u2014 ${formatSol(buyIn)} SOL`
388
+ ? `Join Game \u2014 ${formatSol(wager)} SOL`
369
389
  : 'Pick a side to join'}
370
390
  </Text>
371
391
  )}
@@ -0,0 +1,233 @@
1
+ import React, { useRef, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ PanResponder,
7
+ Dimensions,
8
+ Platform,
9
+ } from 'react-native';
10
+ import { useDubsTheme } from '../theme';
11
+
12
+ const THUMB_SIZE = 32;
13
+ const TRACK_HEIGHT = 6;
14
+ const TICK_INTERVAL = 0.01; // haptic every 0.01 SOL
15
+
16
+ export interface SolSliderProps {
17
+ /** Current value in SOL */
18
+ value: number;
19
+ /** Min SOL (default 0.01) */
20
+ min?: number;
21
+ /** Max SOL (default 5) */
22
+ max?: number;
23
+ /** Step size in SOL (default 0.01) */
24
+ step?: number;
25
+ /** Accent color for filled track + thumb glow */
26
+ accentColor?: string;
27
+ /** Called on every drag frame */
28
+ onValueChange: (value: number) => void;
29
+ /** Called when user lifts finger */
30
+ onSlidingComplete?: (value: number) => void;
31
+ /** Called on each tick for haptics/sound */
32
+ onTick?: (value: number) => void;
33
+ /** Disabled state */
34
+ disabled?: boolean;
35
+ }
36
+
37
+ export function SolSlider({
38
+ value,
39
+ min = 0.01,
40
+ max = 5,
41
+ step = 0.01,
42
+ accentColor,
43
+ onValueChange,
44
+ onSlidingComplete,
45
+ onTick,
46
+ disabled = false,
47
+ }: SolSliderProps) {
48
+ const t = useDubsTheme();
49
+ const accent = accentColor || t.accent;
50
+ const trackRef = useRef<View>(null);
51
+ const trackWidth = useRef(0);
52
+ const lastTickValue = useRef(value);
53
+
54
+ const clamp = (v: number) => {
55
+ const stepped = Math.round(v / step) * step;
56
+ return Math.max(min, Math.min(max, parseFloat(stepped.toFixed(4))));
57
+ };
58
+
59
+ const valueToPosition = (v: number) => {
60
+ const ratio = (v - min) / (max - min);
61
+ return ratio * trackWidth.current;
62
+ };
63
+
64
+ const positionToValue = (x: number) => {
65
+ const ratio = Math.max(0, Math.min(1, x / trackWidth.current));
66
+ return clamp(min + ratio * (max - min));
67
+ };
68
+
69
+ const panResponder = useRef(
70
+ PanResponder.create({
71
+ onStartShouldSetPanResponder: () => !disabled,
72
+ onMoveShouldSetPanResponder: () => !disabled,
73
+ onPanResponderGrant: (_, gestureState) => {
74
+ lastTickValue.current = value;
75
+ },
76
+ onPanResponderMove: (evt, gestureState) => {
77
+ // Calculate position relative to track
78
+ const touchX = gestureState.moveX;
79
+ trackRef.current?.measureInWindow((trackX) => {
80
+ const relX = touchX - trackX;
81
+ const newVal = positionToValue(relX);
82
+
83
+ // Fire tick if we crossed a tick boundary
84
+ const tickDelta = Math.abs(newVal - lastTickValue.current);
85
+ if (tickDelta >= TICK_INTERVAL) {
86
+ lastTickValue.current = newVal;
87
+ onTick?.(newVal);
88
+ }
89
+
90
+ onValueChange(newVal);
91
+ });
92
+ },
93
+ onPanResponderRelease: () => {
94
+ onSlidingComplete?.(value);
95
+ },
96
+ })
97
+ ).current;
98
+
99
+ const ratio = (value - min) / (max - min);
100
+ const filledWidth = `${ratio * 100}%`;
101
+
102
+ // Snap points for visual markers
103
+ const markers = [min, max * 0.25, max * 0.5, max * 0.75, max];
104
+
105
+ return (
106
+ <View style={styles.container}>
107
+ {/* Track */}
108
+ <View
109
+ ref={trackRef}
110
+ style={styles.trackContainer}
111
+ onLayout={(e) => { trackWidth.current = e.nativeEvent.layout.width; }}
112
+ {...panResponder.panHandlers}
113
+ >
114
+ {/* Background track */}
115
+ <View style={[styles.track, { backgroundColor: '#2C2C2E' }]}>
116
+ {/* Filled track */}
117
+ <View style={[styles.trackFilled, { width: filledWidth as any, backgroundColor: accent }]} />
118
+ </View>
119
+
120
+ {/* Tick markers */}
121
+ <View style={styles.markerRow}>
122
+ {markers.map((m, i) => {
123
+ const mRatio = (m - min) / (max - min);
124
+ return (
125
+ <View
126
+ key={i}
127
+ style={[
128
+ styles.marker,
129
+ {
130
+ left: `${mRatio * 100}%`,
131
+ backgroundColor: value >= m ? accent : '#3A3A3C',
132
+ },
133
+ ]}
134
+ />
135
+ );
136
+ })}
137
+ </View>
138
+
139
+ {/* Thumb */}
140
+ <View
141
+ style={[
142
+ styles.thumb,
143
+ {
144
+ left: filledWidth as any,
145
+ backgroundColor: accent,
146
+ shadowColor: accent,
147
+ },
148
+ ]}
149
+ >
150
+ <View style={styles.thumbInner} />
151
+ </View>
152
+ </View>
153
+
154
+ {/* Min/Max labels */}
155
+ <View style={styles.rangeRow}>
156
+ <Text style={styles.rangeText}>{min}</Text>
157
+ <Text style={styles.rangeText}>{max} SOL</Text>
158
+ </View>
159
+ </View>
160
+ );
161
+ }
162
+
163
+ const styles = StyleSheet.create({
164
+ container: {
165
+ paddingVertical: 4,
166
+ },
167
+ trackContainer: {
168
+ height: THUMB_SIZE + 16,
169
+ justifyContent: 'center',
170
+ paddingHorizontal: THUMB_SIZE / 2,
171
+ },
172
+ track: {
173
+ height: TRACK_HEIGHT,
174
+ borderRadius: TRACK_HEIGHT / 2,
175
+ overflow: 'hidden',
176
+ },
177
+ trackFilled: {
178
+ height: '100%',
179
+ borderRadius: TRACK_HEIGHT / 2,
180
+ },
181
+ markerRow: {
182
+ position: 'absolute',
183
+ left: THUMB_SIZE / 2,
184
+ right: THUMB_SIZE / 2,
185
+ height: TRACK_HEIGHT,
186
+ top: (THUMB_SIZE + 16 - TRACK_HEIGHT) / 2,
187
+ },
188
+ marker: {
189
+ position: 'absolute',
190
+ width: 3,
191
+ height: 12,
192
+ borderRadius: 1.5,
193
+ top: -3,
194
+ marginLeft: -1.5,
195
+ },
196
+ thumb: {
197
+ position: 'absolute',
198
+ width: THUMB_SIZE,
199
+ height: THUMB_SIZE,
200
+ borderRadius: THUMB_SIZE / 2,
201
+ top: 8,
202
+ marginLeft: 0,
203
+ alignItems: 'center',
204
+ justifyContent: 'center',
205
+ ...Platform.select({
206
+ ios: {
207
+ shadowOffset: { width: 0, height: 0 },
208
+ shadowOpacity: 0.6,
209
+ shadowRadius: 10,
210
+ },
211
+ android: {
212
+ elevation: 8,
213
+ },
214
+ }),
215
+ },
216
+ thumbInner: {
217
+ width: 12,
218
+ height: 12,
219
+ borderRadius: 6,
220
+ backgroundColor: '#FFFFFF',
221
+ },
222
+ rangeRow: {
223
+ flexDirection: 'row',
224
+ justifyContent: 'space-between',
225
+ paddingHorizontal: THUMB_SIZE / 2,
226
+ marginTop: 4,
227
+ },
228
+ rangeText: {
229
+ color: '#5A5A5E',
230
+ fontSize: 11,
231
+ fontWeight: '600',
232
+ },
233
+ });
@@ -20,3 +20,5 @@ export { EnterArcadePoolSheet } from './EnterArcadePoolSheet';
20
20
  export type { EnterArcadePoolSheetProps } from './EnterArcadePoolSheet';
21
21
  export { ArcadeLeaderboardSheet } from './ArcadeLeaderboardSheet';
22
22
  export type { ArcadeLeaderboardSheetProps } from './ArcadeLeaderboardSheet';
23
+ export { SolSlider } from './SolSlider';
24
+ export type { SolSliderProps } from './SolSlider';
package/src/ui/index.ts CHANGED
@@ -14,7 +14,7 @@ export { useDubsTheme, mergeTheme } from './theme';
14
14
  export type { DubsTheme } from './theme';
15
15
 
16
16
  // Game widgets
17
- export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet } from './game';
17
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './game';
18
18
  export type {
19
19
  GamePosterProps,
20
20
  LivePoolsCardProps,
@@ -27,4 +27,5 @@ export type {
27
27
  ClaimButtonProps,
28
28
  EnterArcadePoolSheetProps,
29
29
  ArcadeLeaderboardSheetProps,
30
+ SolSliderProps,
30
31
  } from './game';