@dubsdotapp/expo 0.2.31 → 0.2.33

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.2.31",
3
+ "version": "0.2.33",
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/client.ts CHANGED
@@ -15,6 +15,8 @@ import type {
15
15
  ConfirmGameResult,
16
16
  BuildClaimParams,
17
17
  BuildClaimResult,
18
+ ConfirmClaimParams,
19
+ ConfirmClaimResult,
18
20
  GameDetail,
19
21
  GameListItem,
20
22
  GetGamesParams,
@@ -245,6 +247,20 @@ export class DubsClient {
245
247
  };
246
248
  }
247
249
 
250
+ async confirmClaim(gameId: string, params: ConfirmClaimParams): Promise<ConfirmClaimResult> {
251
+ const res = await this.request<{ success: true } & ConfirmClaimResult>(
252
+ 'POST',
253
+ `/games/${encodeURIComponent(gameId)}/claim/confirm`,
254
+ params,
255
+ );
256
+ return {
257
+ gameId: res.gameId,
258
+ signature: res.signature,
259
+ explorerUrl: res.explorerUrl,
260
+ message: res.message,
261
+ };
262
+ }
263
+
248
264
  // ── Game Queries ──
249
265
 
250
266
  async getGame(gameId: string): Promise<GameDetail> {
@@ -21,7 +21,7 @@ export function useClaim() {
21
21
  setData(null);
22
22
  }, []);
23
23
 
24
- const execute = useCallback(async (params: BuildClaimParams): Promise<ClaimMutationResult> => {
24
+ const execute = useCallback(async (params: BuildClaimParams & { amountClaimed?: number }): Promise<ClaimMutationResult> => {
25
25
  setStatus('building');
26
26
  setError(null);
27
27
  setData(null);
@@ -42,12 +42,20 @@ export function useClaim() {
42
42
  );
43
43
  console.log('[useClaim] Step 2 done. Signature:', signature);
44
44
 
45
- // Claims don't need backend confirmation the on-chain tx is the claim
46
- const explorerUrl = `https://solscan.io/tx/${signature}`;
45
+ // 3. Confirm claim with backend (records signature + amount in DB)
46
+ setStatus('confirming');
47
+ console.log('[useClaim] Step 3: Confirming claim...');
48
+ const confirmResult = await client.confirmClaim(params.gameId, {
49
+ playerWallet: params.playerWallet,
50
+ signature,
51
+ amountClaimed: params.amountClaimed,
52
+ });
53
+ console.log('[useClaim] Step 3 done.');
54
+
47
55
  const result: ClaimMutationResult = {
48
56
  gameId: params.gameId,
49
57
  signature,
50
- explorerUrl,
58
+ explorerUrl: confirmResult.explorerUrl,
51
59
  };
52
60
 
53
61
  setData(result);
package/src/index.ts CHANGED
@@ -31,6 +31,8 @@ export type {
31
31
  ConfirmGameResult,
32
32
  BuildClaimParams,
33
33
  BuildClaimResult,
34
+ ConfirmClaimParams,
35
+ ConfirmClaimResult,
34
36
  Bettor,
35
37
  GameDetail,
36
38
  GameMedia,
@@ -95,7 +97,7 @@ export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsT
95
97
  export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, UserProfileCardProps, SettingsSheetProps, DubsTheme } from './ui';
96
98
 
97
99
  // Game widgets
98
- export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet } from './ui';
100
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet } from './ui';
99
101
  export type {
100
102
  GamePosterProps,
101
103
  LivePoolsCardProps,
@@ -104,6 +106,7 @@ export type {
104
106
  JoinGameButtonProps,
105
107
  CreateCustomGameSheetProps,
106
108
  JoinGameSheetProps,
109
+ ClaimPrizeSheetProps,
107
110
  } from './ui';
108
111
 
109
112
  // Utils
package/src/types.ts CHANGED
@@ -170,6 +170,21 @@ export interface BuildClaimResult {
170
170
  message: string;
171
171
  }
172
172
 
173
+ // ── Confirm Claim ──
174
+
175
+ export interface ConfirmClaimParams {
176
+ playerWallet: string;
177
+ signature: string;
178
+ amountClaimed?: number;
179
+ }
180
+
181
+ export interface ConfirmClaimResult {
182
+ gameId: string;
183
+ signature: string;
184
+ explorerUrl: string;
185
+ message: string;
186
+ }
187
+
173
188
  // ── Game Detail (returned by /games/:gameId) ──
174
189
 
175
190
  export interface GameMedia {
@@ -0,0 +1,367 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ ActivityIndicator,
7
+ Modal,
8
+ Animated,
9
+ StyleSheet,
10
+ KeyboardAvoidingView,
11
+ Platform,
12
+ } from 'react-native';
13
+ import { useDubsTheme } from '../theme';
14
+ import { useDubs } from '../../provider';
15
+ import { useClaim } from '../../hooks/useClaim';
16
+ import type { ClaimMutationResult } from '../../hooks/useClaim';
17
+
18
+ export interface ClaimPrizeSheetProps {
19
+ visible: boolean;
20
+ onDismiss: () => void;
21
+ gameId: string;
22
+ /** Prize amount in SOL */
23
+ prizeAmount: number;
24
+ /** Callbacks */
25
+ onSuccess?: (result: ClaimMutationResult) => void;
26
+ onError?: (error: Error) => void;
27
+ }
28
+
29
+ const STATUS_LABELS: Record<string, string> = {
30
+ building: 'Building transaction...',
31
+ signing: 'Approve in wallet...',
32
+ confirming: 'Confirming...',
33
+ success: 'Claimed!',
34
+ };
35
+
36
+ export function ClaimPrizeSheet({
37
+ visible,
38
+ onDismiss,
39
+ gameId,
40
+ prizeAmount,
41
+ onSuccess,
42
+ onError,
43
+ }: ClaimPrizeSheetProps) {
44
+ const t = useDubsTheme();
45
+ const { wallet } = useDubs();
46
+ const mutation = useClaim();
47
+
48
+ const overlayOpacity = useRef(new Animated.Value(0)).current;
49
+ const celebrationScale = useRef(new Animated.Value(0)).current;
50
+ const celebrationOpacity = useRef(new Animated.Value(0)).current;
51
+ const [showCelebration, setShowCelebration] = useState(false);
52
+
53
+ // Animate overlay on visibility change
54
+ useEffect(() => {
55
+ Animated.timing(overlayOpacity, {
56
+ toValue: visible ? 1 : 0,
57
+ duration: 250,
58
+ useNativeDriver: true,
59
+ }).start();
60
+ }, [visible, overlayOpacity]);
61
+
62
+ // Reset state when sheet opens
63
+ useEffect(() => {
64
+ if (visible) {
65
+ mutation.reset();
66
+ setShowCelebration(false);
67
+ celebrationScale.setValue(0);
68
+ celebrationOpacity.setValue(0);
69
+ }
70
+ }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
71
+
72
+ // Celebration animation + auto-dismiss on success
73
+ useEffect(() => {
74
+ if (mutation.status === 'success' && mutation.data) {
75
+ setShowCelebration(true);
76
+
77
+ Animated.parallel([
78
+ Animated.spring(celebrationScale, {
79
+ toValue: 1,
80
+ tension: 50,
81
+ friction: 6,
82
+ useNativeDriver: true,
83
+ }),
84
+ Animated.timing(celebrationOpacity, {
85
+ toValue: 1,
86
+ duration: 300,
87
+ useNativeDriver: true,
88
+ }),
89
+ ]).start();
90
+
91
+ onSuccess?.(mutation.data);
92
+
93
+ const timer = setTimeout(() => {
94
+ onDismiss();
95
+ }, 2500);
96
+ return () => clearTimeout(timer);
97
+ }
98
+ }, [mutation.status, mutation.data]); // eslint-disable-line react-hooks/exhaustive-deps
99
+
100
+ // Report errors
101
+ useEffect(() => {
102
+ if (mutation.status === 'error' && mutation.error) {
103
+ onError?.(mutation.error);
104
+ }
105
+ }, [mutation.status, mutation.error]); // eslint-disable-line react-hooks/exhaustive-deps
106
+
107
+ const isMutating = mutation.status !== 'idle' && mutation.status !== 'success' && mutation.status !== 'error';
108
+ const canClaim = !isMutating && mutation.status !== 'success' && !!wallet.publicKey;
109
+
110
+ const handleClaim = useCallback(async () => {
111
+ if (!wallet.publicKey) return;
112
+
113
+ try {
114
+ await mutation.execute({
115
+ playerWallet: wallet.publicKey.toBase58(),
116
+ gameId,
117
+ amountClaimed: prizeAmount,
118
+ });
119
+ } catch {
120
+ // Error is already captured in mutation state
121
+ }
122
+ }, [wallet.publicKey, mutation.execute, gameId, prizeAmount]); // eslint-disable-line react-hooks/exhaustive-deps
123
+
124
+ const statusLabel = STATUS_LABELS[mutation.status] || '';
125
+
126
+ return (
127
+ <Modal
128
+ visible={visible}
129
+ animationType="slide"
130
+ transparent
131
+ onRequestClose={onDismiss}
132
+ >
133
+ <Animated.View style={[styles.overlay, { opacity: overlayOpacity }]}>
134
+ <TouchableOpacity style={styles.overlayTap} activeOpacity={1} onPress={onDismiss} />
135
+ </Animated.View>
136
+
137
+ <KeyboardAvoidingView
138
+ style={styles.keyboardView}
139
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
140
+ >
141
+ <View style={styles.sheetPositioner}>
142
+ <View style={[styles.sheet, { backgroundColor: t.background }]}>
143
+ {/* Drag handle */}
144
+ <View style={styles.handleRow}>
145
+ <View style={[styles.handle, { backgroundColor: t.textMuted }]} />
146
+ </View>
147
+
148
+ {/* Header */}
149
+ <View style={styles.header}>
150
+ <Text style={[styles.headerTitle, { color: t.text }]}>
151
+ {showCelebration ? 'Prize Claimed!' : 'Claim Prize'}
152
+ </Text>
153
+ <TouchableOpacity onPress={onDismiss} activeOpacity={0.8}>
154
+ <Text style={[styles.closeButton, { color: t.textMuted }]}>{'\u2715'}</Text>
155
+ </TouchableOpacity>
156
+ </View>
157
+
158
+ {/* Celebration */}
159
+ {showCelebration && (
160
+ <Animated.View
161
+ style={[
162
+ styles.celebrationContainer,
163
+ {
164
+ opacity: celebrationOpacity,
165
+ transform: [{ scale: celebrationScale }],
166
+ },
167
+ ]}
168
+ >
169
+ <Text style={styles.celebrationEmoji}>{'🏆'}</Text>
170
+ <Text style={[styles.celebrationText, { color: t.success }]}>
171
+ +{prizeAmount} SOL
172
+ </Text>
173
+ <Text style={[styles.celebrationSubtext, { color: t.textMuted }]}>
174
+ Winnings sent to your wallet
175
+ </Text>
176
+ </Animated.View>
177
+ )}
178
+
179
+ {/* Summary Card */}
180
+ {!showCelebration && (
181
+ <View style={[styles.summaryCard, { backgroundColor: t.surface, borderColor: t.border }]}>
182
+ <View style={styles.summaryRow}>
183
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Prize</Text>
184
+ <Text style={[styles.summaryValue, { color: t.success }]}>
185
+ {prizeAmount} SOL
186
+ </Text>
187
+ </View>
188
+ <View style={[styles.summarySep, { backgroundColor: t.border }]} />
189
+ <View style={styles.summaryRow}>
190
+ <Text style={[styles.summaryLabel, { color: t.textMuted }]}>Game</Text>
191
+ <Text
192
+ style={[styles.summaryValue, { color: t.text }]}
193
+ numberOfLines={1}
194
+ >
195
+ {gameId.slice(0, 8)}...{gameId.slice(-4)}
196
+ </Text>
197
+ </View>
198
+ </View>
199
+ )}
200
+
201
+ {/* Error Display */}
202
+ {mutation.error && (
203
+ <View style={[styles.errorBox, { backgroundColor: t.errorBg, borderColor: t.errorBorder }]}>
204
+ <Text style={[styles.errorText, { color: t.errorText }]}>{mutation.error.message}</Text>
205
+ </View>
206
+ )}
207
+
208
+ {/* CTA Button */}
209
+ {!showCelebration && (
210
+ <TouchableOpacity
211
+ style={[
212
+ styles.ctaButton,
213
+ { backgroundColor: canClaim ? t.success : t.border },
214
+ ]}
215
+ disabled={!canClaim}
216
+ onPress={handleClaim}
217
+ activeOpacity={0.8}
218
+ >
219
+ {isMutating ? (
220
+ <View style={styles.ctaLoading}>
221
+ <ActivityIndicator size="small" color="#FFFFFF" />
222
+ <Text style={styles.ctaText}>{statusLabel}</Text>
223
+ </View>
224
+ ) : (
225
+ <Text style={[styles.ctaText, !canClaim && { opacity: 0.5 }]}>
226
+ Claim Prize — {prizeAmount} SOL
227
+ </Text>
228
+ )}
229
+ </TouchableOpacity>
230
+ )}
231
+
232
+ {/* Explorer link on success */}
233
+ {mutation.data?.explorerUrl && (
234
+ <Text style={[styles.explorerHint, { color: t.textMuted }]}>
235
+ View on Solscan
236
+ </Text>
237
+ )}
238
+ </View>
239
+ </View>
240
+ </KeyboardAvoidingView>
241
+ </Modal>
242
+ );
243
+ }
244
+
245
+ const styles = StyleSheet.create({
246
+ overlay: {
247
+ ...StyleSheet.absoluteFillObject,
248
+ backgroundColor: 'rgba(0,0,0,0.5)',
249
+ },
250
+ overlayTap: {
251
+ flex: 1,
252
+ },
253
+ keyboardView: {
254
+ flex: 1,
255
+ justifyContent: 'flex-end',
256
+ },
257
+ sheetPositioner: {
258
+ justifyContent: 'flex-end',
259
+ },
260
+ sheet: {
261
+ borderTopLeftRadius: 24,
262
+ borderTopRightRadius: 24,
263
+ paddingHorizontal: 20,
264
+ paddingBottom: 40,
265
+ },
266
+ handleRow: {
267
+ alignItems: 'center',
268
+ paddingTop: 10,
269
+ paddingBottom: 8,
270
+ },
271
+ handle: {
272
+ width: 36,
273
+ height: 4,
274
+ borderRadius: 2,
275
+ opacity: 0.4,
276
+ },
277
+ header: {
278
+ flexDirection: 'row',
279
+ alignItems: 'center',
280
+ justifyContent: 'space-between',
281
+ paddingVertical: 12,
282
+ },
283
+ headerTitle: {
284
+ fontSize: 20,
285
+ fontWeight: '700',
286
+ },
287
+ closeButton: {
288
+ fontSize: 20,
289
+ padding: 4,
290
+ },
291
+ // Celebration
292
+ celebrationContainer: {
293
+ alignItems: 'center',
294
+ paddingVertical: 32,
295
+ gap: 8,
296
+ },
297
+ celebrationEmoji: {
298
+ fontSize: 64,
299
+ },
300
+ celebrationText: {
301
+ fontSize: 32,
302
+ fontWeight: '800',
303
+ },
304
+ celebrationSubtext: {
305
+ fontSize: 14,
306
+ marginTop: 4,
307
+ },
308
+ // Summary
309
+ summaryCard: {
310
+ marginTop: 20,
311
+ borderRadius: 16,
312
+ borderWidth: 1,
313
+ overflow: 'hidden',
314
+ },
315
+ summaryRow: {
316
+ flexDirection: 'row',
317
+ alignItems: 'center',
318
+ justifyContent: 'space-between',
319
+ paddingHorizontal: 16,
320
+ paddingVertical: 14,
321
+ },
322
+ summaryLabel: {
323
+ fontSize: 14,
324
+ },
325
+ summaryValue: {
326
+ fontSize: 15,
327
+ fontWeight: '700',
328
+ },
329
+ summarySep: {
330
+ height: 1,
331
+ marginHorizontal: 16,
332
+ },
333
+ // Error
334
+ errorBox: {
335
+ marginTop: 16,
336
+ borderRadius: 12,
337
+ borderWidth: 1,
338
+ padding: 12,
339
+ },
340
+ errorText: {
341
+ fontSize: 13,
342
+ fontWeight: '500',
343
+ },
344
+ // CTA
345
+ ctaButton: {
346
+ marginTop: 20,
347
+ height: 56,
348
+ borderRadius: 14,
349
+ justifyContent: 'center',
350
+ alignItems: 'center',
351
+ },
352
+ ctaText: {
353
+ color: '#FFFFFF',
354
+ fontSize: 16,
355
+ fontWeight: '700',
356
+ },
357
+ ctaLoading: {
358
+ flexDirection: 'row',
359
+ alignItems: 'center',
360
+ gap: 10,
361
+ },
362
+ explorerHint: {
363
+ textAlign: 'center',
364
+ marginTop: 12,
365
+ fontSize: 13,
366
+ },
367
+ });
@@ -12,3 +12,5 @@ export { CreateCustomGameSheet } from './CreateCustomGameSheet';
12
12
  export type { CreateCustomGameSheetProps } from './CreateCustomGameSheet';
13
13
  export { JoinGameSheet } from './JoinGameSheet';
14
14
  export type { JoinGameSheetProps } from './JoinGameSheet';
15
+ export { ClaimPrizeSheet } from './ClaimPrizeSheet';
16
+ export type { ClaimPrizeSheetProps } from './ClaimPrizeSheet';
package/src/ui/index.ts CHANGED
@@ -10,7 +10,7 @@ export { useDubsTheme, mergeTheme } from './theme';
10
10
  export type { DubsTheme } from './theme';
11
11
 
12
12
  // Game widgets
13
- export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet } from './game';
13
+ export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton, CreateCustomGameSheet, JoinGameSheet, ClaimPrizeSheet } from './game';
14
14
  export type {
15
15
  GamePosterProps,
16
16
  LivePoolsCardProps,
@@ -19,4 +19,5 @@ export type {
19
19
  JoinGameButtonProps,
20
20
  CreateCustomGameSheetProps,
21
21
  JoinGameSheetProps,
22
+ ClaimPrizeSheetProps,
22
23
  } from './game';