@dubsdotapp/expo 0.5.11 → 0.5.12

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.11",
3
+ "version": "0.5.12",
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,7 @@ 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, SolSlider } from './ui';
137
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, CreateGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './ui';
138
138
  export type { SolSliderProps } from './ui';
139
139
  export type {
140
140
  GamePosterProps,
@@ -143,6 +143,7 @@ export type {
143
143
  PlayersCardProps,
144
144
  JoinGameButtonProps,
145
145
  CreateCustomGameSheetProps,
146
+ CreateGameSheetProps,
146
147
  JoinGameSheetProps,
147
148
  ClaimPrizeSheetProps,
148
149
  ClaimButtonProps,
@@ -0,0 +1,366 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ Image,
6
+ TouchableOpacity,
7
+ ActivityIndicator,
8
+ Modal,
9
+ Animated,
10
+ StyleSheet,
11
+ KeyboardAvoidingView,
12
+ Platform,
13
+ } from 'react-native';
14
+ import { useDubsTheme } from '../theme';
15
+ import { useDubs } from '../../provider';
16
+ import { useCreateGame } from '../../hooks/useCreateGame';
17
+ import type { CreateGameMutationResult } from '../../hooks/useCreateGame';
18
+ import type { UnifiedEvent } from '../../types';
19
+ import { SolSlider } from './SolSlider';
20
+
21
+ export interface CreateGameSheetProps {
22
+ visible: boolean;
23
+ onDismiss: () => void;
24
+ event: UnifiedEvent;
25
+ /** Override team colors */
26
+ homeColor?: string;
27
+ awayColor?: string;
28
+ /** Callbacks */
29
+ onSuccess?: (result: CreateGameMutationResult) => void;
30
+ onError?: (error: Error) => void;
31
+ /** Called when the user taps a team card */
32
+ onTeamSelect?: (team: 'home' | 'away') => void;
33
+ /** Called on success (for sound/animation) */
34
+ onCreateSuccess?: (result: CreateGameMutationResult) => void;
35
+ /** Called on each slider tick */
36
+ onSliderTick?: (value: number) => void;
37
+ /** Max wager in SOL */
38
+ maxWager?: number;
39
+ /** Custom short-name function for team labels */
40
+ shortName?: (name: string | null) => string;
41
+ }
42
+
43
+ const STATUS_LABELS: Record<string, string> = {
44
+ building: 'Building transaction...',
45
+ signing: 'Approve in wallet...',
46
+ confirming: 'Confirming...',
47
+ success: 'Game Created!',
48
+ };
49
+
50
+ function formatSol(n: number): string {
51
+ return parseFloat(n.toFixed(9)).toString();
52
+ }
53
+
54
+ function toPng(url: string | null | undefined): string | null {
55
+ if (!url) return null;
56
+ return url.replace('/svg?', '/png?');
57
+ }
58
+
59
+ export function CreateGameSheet({
60
+ visible,
61
+ onDismiss,
62
+ event,
63
+ homeColor = '#3B82F6',
64
+ awayColor = '#EF4444',
65
+ onSuccess,
66
+ onError,
67
+ onTeamSelect,
68
+ onCreateSuccess,
69
+ onSliderTick,
70
+ maxWager = 1,
71
+ shortName,
72
+ }: CreateGameSheetProps) {
73
+ const t = useDubsTheme();
74
+ const { wallet } = useDubs();
75
+ const mutation = useCreateGame();
76
+
77
+ const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
78
+ const [wager, setWager] = useState(0.01);
79
+ const [showSuccess, setShowSuccess] = useState(false);
80
+
81
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
82
+ const successScale = useRef(new Animated.Value(0)).current;
83
+ const successOpacity = useRef(new Animated.Value(0)).current;
84
+
85
+ useEffect(() => {
86
+ Animated.timing(overlayOpacity, {
87
+ toValue: visible ? 1 : 0,
88
+ duration: 250,
89
+ useNativeDriver: true,
90
+ }).start();
91
+ }, [visible]);
92
+
93
+ useEffect(() => {
94
+ if (visible) {
95
+ setSelectedTeam(null);
96
+ setWager(0.01);
97
+ setShowSuccess(false);
98
+ successScale.setValue(0);
99
+ successOpacity.setValue(0);
100
+ mutation.reset();
101
+ }
102
+ }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
103
+
104
+ // Handle success
105
+ useEffect(() => {
106
+ if (mutation.status === 'success' && mutation.data) {
107
+ setShowSuccess(true);
108
+ onSuccess?.(mutation.data);
109
+ onCreateSuccess?.(mutation.data);
110
+
111
+ Animated.parallel([
112
+ Animated.spring(successScale, { toValue: 1, friction: 4, tension: 80, useNativeDriver: true }),
113
+ Animated.timing(successOpacity, { toValue: 1, duration: 300, useNativeDriver: true }),
114
+ ]).start();
115
+
116
+ const timer = setTimeout(() => {
117
+ Animated.timing(successOpacity, { toValue: 0, duration: 300, useNativeDriver: true }).start(() => {
118
+ onDismiss();
119
+ });
120
+ }, 2500);
121
+ return () => clearTimeout(timer);
122
+ }
123
+ }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
124
+
125
+ useEffect(() => {
126
+ if (mutation.status === 'error' && mutation.error) {
127
+ onError?.(mutation.error);
128
+ }
129
+ }, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps
130
+
131
+ const opponents = event.opponents || [];
132
+ const homeName = shortName ? shortName(opponents[0]?.name) : (opponents[0]?.name || 'Home');
133
+ const awayName = shortName ? shortName(opponents[1]?.name) : (opponents[1]?.name || 'Away');
134
+
135
+ const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
136
+ const canCreate = selectedTeam !== null && !isMutating && mutation.status !== 'success';
137
+
138
+ const handleCreate = useCallback(async () => {
139
+ if (!selectedTeam || !wallet.publicKey) return;
140
+
141
+ try {
142
+ await mutation.execute({
143
+ id: event.id,
144
+ playerWallet: wallet.publicKey.toBase58(),
145
+ teamChoice: selectedTeam,
146
+ wagerAmount: wager,
147
+ });
148
+ } catch {
149
+ // Error captured in mutation state
150
+ }
151
+ }, [selectedTeam, wallet.publicKey, mutation.execute, event.id, wager]); // eslint-disable-line react-hooks/exhaustive-deps
152
+
153
+ const statusLabel = STATUS_LABELS[mutation.status] || '';
154
+
155
+ // Countdown
156
+ const startTime = event.startTime ? new Date(event.startTime) : null;
157
+ const timeLabel = startTime
158
+ ? startTime.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }) +
159
+ ' at ' +
160
+ startTime.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
161
+ : 'TBD';
162
+
163
+ return (
164
+ <Modal visible={visible} animationType="slide" transparent onRequestClose={onDismiss}>
165
+ <Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
166
+ <TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
167
+ </Animated.View>
168
+
169
+ {/* Success overlay */}
170
+ {showSuccess && (
171
+ <View style={styles.successOverlay}>
172
+ <Animated.View style={[styles.successContent, { opacity: successOpacity, transform: [{ scale: successScale }] }]}>
173
+ <Text style={styles.successEmoji}>🎯</Text>
174
+ <Text style={styles.successTitle}>Game Created!</Text>
175
+ <Text style={styles.successSub}>
176
+ {formatSol(wager)} SOL on {selectedTeam === 'home' ? homeName : awayName}
177
+ </Text>
178
+ </Animated.View>
179
+ </View>
180
+ )}
181
+
182
+ <KeyboardAvoidingView style={styles.keyboardView} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
183
+ <View style={styles.sheetPositioner}>
184
+ <View style={[styles.sheet, { backgroundColor: t.background }]}>
185
+ {/* Drag handle */}
186
+ <View style={styles.handleRow}>
187
+ <View style={[styles.handle, { backgroundColor: t.textMuted }]} />
188
+ </View>
189
+
190
+ {/* Header */}
191
+ <View style={styles.header}>
192
+ <View>
193
+ <Text style={[styles.headerTitle, { color: t.text }]}>Create Game</Text>
194
+ <Text style={[styles.headerSub, { color: t.textMuted }]}>{timeLabel}</Text>
195
+ </View>
196
+ <TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
197
+ <Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
198
+ </TouchableOpacity>
199
+ </View>
200
+
201
+ {/* Matchup banner */}
202
+ <View style={[styles.matchupBanner, { borderColor: t.border }]}>
203
+ <View style={styles.matchupTeam}>
204
+ {opponents[0]?.imageUrl ? (
205
+ <Image source={{ uri: opponents[0].imageUrl }} style={styles.matchupLogo} resizeMode="contain" />
206
+ ) : (
207
+ <View style={[styles.matchupPlaceholder, { backgroundColor: homeColor + '20' }]}>
208
+ <Text style={[styles.matchupInitial, { color: homeColor }]}>{homeName.charAt(0)}</Text>
209
+ </View>
210
+ )}
211
+ <Text style={[styles.matchupName, { color: t.text }]} numberOfLines={1}>{homeName}</Text>
212
+ </View>
213
+ <Text style={[styles.matchupVs, { color: t.textMuted }]}>vs</Text>
214
+ <View style={styles.matchupTeam}>
215
+ {opponents[1]?.imageUrl ? (
216
+ <Image source={{ uri: opponents[1].imageUrl }} style={styles.matchupLogo} resizeMode="contain" />
217
+ ) : (
218
+ <View style={[styles.matchupPlaceholder, { backgroundColor: awayColor + '20' }]}>
219
+ <Text style={[styles.matchupInitial, { color: awayColor }]}>{awayName.charAt(0)}</Text>
220
+ </View>
221
+ )}
222
+ <Text style={[styles.matchupName, { color: t.text }]} numberOfLines={1}>{awayName}</Text>
223
+ </View>
224
+ </View>
225
+
226
+ {/* Team Selection */}
227
+ <View style={styles.section}>
228
+ <Text style={[styles.sectionLabel, { color: t.textSecondary }]}>Pick Your Side</Text>
229
+ <View style={styles.teamsRow}>
230
+ <TouchableOpacity
231
+ style={[styles.teamOption, { borderColor: selectedTeam === 'home' ? homeColor : t.border, backgroundColor: selectedTeam === 'home' ? homeColor + '15' : t.background }]}
232
+ onPress={() => { setSelectedTeam('home'); onTeamSelect?.('home'); }}
233
+ activeOpacity={0.7}
234
+ >
235
+ <Text style={[styles.teamLabel, { color: t.text }]}>{homeName}</Text>
236
+ {selectedTeam === 'home' && (
237
+ <View style={[styles.teamBadge, { backgroundColor: homeColor }]}>
238
+ <Text style={styles.teamBadgeText}>Selected</Text>
239
+ </View>
240
+ )}
241
+ </TouchableOpacity>
242
+ <TouchableOpacity
243
+ style={[styles.teamOption, { borderColor: selectedTeam === 'away' ? awayColor : t.border, backgroundColor: selectedTeam === 'away' ? awayColor + '15' : t.background }]}
244
+ onPress={() => { setSelectedTeam('away'); onTeamSelect?.('away'); }}
245
+ activeOpacity={0.7}
246
+ >
247
+ <Text style={[styles.teamLabel, { color: t.text }]}>{awayName}</Text>
248
+ {selectedTeam === 'away' && (
249
+ <View style={[styles.teamBadge, { backgroundColor: awayColor }]}>
250
+ <Text style={styles.teamBadgeText}>Selected</Text>
251
+ </View>
252
+ )}
253
+ </TouchableOpacity>
254
+ </View>
255
+ </View>
256
+
257
+ {/* Summary */}
258
+ <View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
259
+ <View style={styles.summaryRow}>
260
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Your wager</Text>
261
+ <Text style={[styles.summaryValue, { color: t.text }]}>{formatSol(wager)} SOL</Text>
262
+ </View>
263
+ <View style={[styles.summarySep, { backgroundColor: t.border }]} />
264
+ <View style={styles.summaryRow}>
265
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>You're the first</Text>
266
+ <Text style={[styles.summaryValue, { color: t.success }]}>Set the odds</Text>
267
+ </View>
268
+ </View>
269
+
270
+ {/* SOL Slider */}
271
+ {selectedTeam && (
272
+ <SolSlider
273
+ value={wager}
274
+ min={0.01}
275
+ max={maxWager}
276
+ step={0.01}
277
+ accentColor={selectedTeam === 'home' ? homeColor : awayColor}
278
+ onValueChange={setWager}
279
+ onTick={onSliderTick}
280
+ />
281
+ )}
282
+
283
+ {/* Error */}
284
+ {mutation.error && (
285
+ <View style={[styles.errorBox, { backgroundColor: t.errorBg, borderColor: t.errorBorder }]}>
286
+ <Text style={[styles.errorText, { color: t.errorText }]}>{mutation.error.message}</Text>
287
+ </View>
288
+ )}
289
+
290
+ {/* CTA */}
291
+ <TouchableOpacity
292
+ style={[styles.ctaButton, { backgroundColor: canCreate ? t.accent : t.border }]}
293
+ disabled={!canCreate}
294
+ onPress={handleCreate}
295
+ activeOpacity={0.8}
296
+ >
297
+ {isMutating ? (
298
+ <View style={styles.ctaLoading}>
299
+ <ActivityIndicator size="small" color="#FFFFFF" />
300
+ <Text style={styles.ctaText}>{statusLabel}</Text>
301
+ </View>
302
+ ) : mutation.status === 'success' ? (
303
+ <Text style={styles.ctaText}>Game Created!</Text>
304
+ ) : (
305
+ <Text style={[styles.ctaText, !canCreate && { opacity: 0.5 }]}>
306
+ {selectedTeam
307
+ ? `Create Game \u2014 ${formatSol(wager)} SOL`
308
+ : 'Pick a side to start'}
309
+ </Text>
310
+ )}
311
+ </TouchableOpacity>
312
+ </View>
313
+ </View>
314
+ </KeyboardAvoidingView>
315
+ </Modal>
316
+ );
317
+ }
318
+
319
+ const styles = StyleSheet.create({
320
+ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' },
321
+ overlayTap: { flex: 1 },
322
+ keyboardView: { flex: 1, justifyContent: 'flex-end' },
323
+ sheetPositioner: { justifyContent: 'flex-end' },
324
+ sheet: { borderTopLeftRadius: 24, borderTopRightRadius: 24, paddingHorizontal: 20, paddingBottom: 40 },
325
+ handleRow: { alignItems: 'center', paddingTop: 10, paddingBottom: 8 },
326
+ handle: { width: 36, height: 4, borderRadius: 2, opacity: 0.4 },
327
+ header: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingVertical: 8 },
328
+ headerTitle: { fontSize: 20, fontWeight: '700' },
329
+ headerSub: { fontSize: 13, marginTop: 2 },
330
+ closeButton: { fontSize: 20, padding: 4 },
331
+
332
+ matchupBanner: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 16, borderBottomWidth: 1, gap: 16 },
333
+ matchupTeam: { flex: 1, alignItems: 'center', gap: 6 },
334
+ matchupLogo: { width: 40, height: 40 },
335
+ matchupPlaceholder: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
336
+ matchupInitial: { fontSize: 18, fontWeight: '800' },
337
+ matchupName: { fontSize: 13, fontWeight: '600', textAlign: 'center' },
338
+ matchupVs: { fontSize: 13, fontWeight: '600' },
339
+
340
+ section: { gap: 10, paddingTop: 12 },
341
+ sectionLabel: { fontSize: 14, fontWeight: '600' },
342
+ teamsRow: { flexDirection: 'row', gap: 10 },
343
+ teamOption: { flex: 1, borderWidth: 2, borderRadius: 14, paddingVertical: 14, alignItems: 'center', gap: 6 },
344
+ teamLabel: { fontSize: 15, fontWeight: '700' },
345
+ teamBadge: { borderRadius: 8, paddingHorizontal: 10, paddingVertical: 3 },
346
+ teamBadgeText: { color: '#FFF', fontSize: 11, fontWeight: '700' },
347
+
348
+ summaryCard: { marginTop: 12, borderRadius: 14, borderWidth: 1, overflow: 'hidden' },
349
+ summaryRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 12 },
350
+ summaryLabel: { fontSize: 14 },
351
+ summaryValue: { fontSize: 15, fontWeight: '700' },
352
+ summarySep: { height: 1, marginHorizontal: 16 },
353
+
354
+ errorBox: { marginTop: 12, borderRadius: 12, borderWidth: 1, padding: 12 },
355
+ errorText: { fontSize: 13, fontWeight: '500' },
356
+
357
+ ctaButton: { marginTop: 16, height: 54, borderRadius: 14, justifyContent: 'center', alignItems: 'center' },
358
+ ctaText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
359
+ ctaLoading: { flexDirection: 'row', alignItems: 'center', gap: 10 },
360
+
361
+ successOverlay: { ...StyleSheet.absoluteFillObject, zIndex: 100, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.85)' },
362
+ successContent: { alignItems: 'center', gap: 12 },
363
+ successEmoji: { fontSize: 64 },
364
+ successTitle: { color: '#FFFFFF', fontSize: 28, fontWeight: '900' },
365
+ successSub: { color: '#8E8E93', fontSize: 16 },
366
+ });
@@ -22,3 +22,5 @@ export { ArcadeLeaderboardSheet } from './ArcadeLeaderboardSheet';
22
22
  export type { ArcadeLeaderboardSheetProps } from './ArcadeLeaderboardSheet';
23
23
  export { SolSlider } from './SolSlider';
24
24
  export type { SolSliderProps } from './SolSlider';
25
+ export { CreateGameSheet } from './CreateGameSheet';
26
+ export type { CreateGameSheetProps } from './CreateGameSheet';
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, SolSlider } from './game';
17
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, CreateGameSheet, JoinGameSheet, ClaimPrizeSheet, ClaimButton, EnterArcadePoolSheet, ArcadeLeaderboardSheet, SolSlider } from './game';
18
18
  export type {
19
19
  GamePosterProps,
20
20
  LivePoolsCardProps,
@@ -28,4 +28,5 @@ export type {
28
28
  EnterArcadePoolSheetProps,
29
29
  ArcadeLeaderboardSheetProps,
30
30
  SolSliderProps,
31
+ CreateGameSheetProps,
31
32
  } from './game';