@dubsdotapp/expo 0.5.33 → 0.5.35

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.33",
3
+ "version": "0.5.35",
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
@@ -200,6 +200,37 @@ export class DubsClient {
200
200
  }
201
201
 
202
202
  async createGame(params: CreateGameParams): Promise<CreateGameResult> {
203
+ // Sponsored path: spend an active streak credit instead of paying
204
+ // rent + buy-in. Server picks oldest FIFO credit, returns the
205
+ // partial-signed compound tx + promoCode so confirmGame can mark
206
+ // the credit as used after the on-chain confirm.
207
+ if (params.useCredit) {
208
+ const res = await this.request<{
209
+ success: true;
210
+ gameId: string;
211
+ gameAddress: string;
212
+ transaction: string;
213
+ lockTimestamp: number;
214
+ event: UnifiedEvent;
215
+ promoCode: string;
216
+ sponsorWallet?: string;
217
+ wagerAmount: number;
218
+ }>('POST', '/games/create-sponsored', {
219
+ id: params.id,
220
+ teamChoice: params.teamChoice,
221
+ });
222
+ return {
223
+ gameId: res.gameId,
224
+ gameAddress: res.gameAddress,
225
+ transaction: res.transaction,
226
+ lockTimestamp: res.lockTimestamp,
227
+ event: res.event,
228
+ promoCode: res.promoCode,
229
+ sponsorWallet: res.sponsorWallet,
230
+ wagerAmount: res.wagerAmount,
231
+ };
232
+ }
233
+
203
234
  const res = await this.request<{ success: true } & CreateGameResult>(
204
235
  'POST',
205
236
  '/games/create',
@@ -58,14 +58,21 @@ export function useCreateGame() {
58
58
  // 3. Confirm with backend (server handles on-chain verification)
59
59
  setStatus('confirming');
60
60
  console.log('[useCreateGame] Step 3: Confirming with backend...');
61
+ // For sponsored creates, the server forces wager to the credit
62
+ // amount — use that, not whatever was in params, so the saved
63
+ // game record is consistent with the on-chain bet.
64
+ const wagerAmount = createResult.wagerAmount ?? params.wagerAmount;
61
65
  const confirmResult = await client.confirmGame({
62
66
  gameId: createResult.gameId,
63
67
  playerWallet: params.playerWallet,
64
68
  signature,
65
69
  teamChoice: params.teamChoice,
66
- wagerAmount: params.wagerAmount,
70
+ wagerAmount,
67
71
  role: 'creator',
68
72
  gameAddress: createResult.gameAddress,
73
+ // Echo the credit code through so the server marks it as used
74
+ // after the on-chain confirm succeeds.
75
+ promoCode: createResult.promoCode,
69
76
  });
70
77
  console.log('[useCreateGame] Step 3 done.');
71
78
 
@@ -94,14 +101,22 @@ export function useCreateGame() {
94
101
  params.teamChoice === 'away' ? teamNickname(away) :
95
102
  'Draw';
96
103
 
97
- const message = `I just placed a ${params.wagerAmount} SOL bet on ${teamLabel} - Join me!`;
104
+ const message = `I just placed a ${wagerAmount} SOL bet on ${teamLabel} - Join me!`;
105
+ // event.startTime is an ESPN-unified ISO string ("…Z"), but every
106
+ // downstream chat consumer (dubs.app SPA, mobile app) does
107
+ // `new Date(strTimestamp + 'Z')` and crashes on the resulting
108
+ // double-Z. Strip the trailing Z so the on-the-wire shape matches
109
+ // the legacy TheSportsDB format the rest of the system expects.
110
+ const strTimestamp = typeof event?.startTime === 'string'
111
+ ? event.startTime.replace(/Z$/, '')
112
+ : null;
98
113
  const gameInvite = {
99
114
  gameId: createResult.gameId,
100
115
  gameAddress: createResult.gameAddress,
101
116
  title: event?.title,
102
117
  league: event?.league,
103
118
  gameType: 'sports',
104
- buyIn: params.wagerAmount,
119
+ buyIn: wagerAmount,
105
120
  status: 'waiting',
106
121
  homeTeam: home,
107
122
  awayTeam: away,
@@ -110,7 +125,7 @@ export function useCreateGame() {
110
125
  imageUrl: event?.media?.thumbnail ?? null,
111
126
  strThumb: event?.media?.thumbnail ?? null,
112
127
  strPoster: event?.media?.poster ?? null,
113
- strTimestamp: event?.startTime ?? null,
128
+ strTimestamp,
114
129
  creatorTeam: params.teamChoice,
115
130
  creatorWallet: params.playerWallet,
116
131
  } as Record<string, unknown>;
package/src/types.ts CHANGED
@@ -95,6 +95,14 @@ export interface CreateGameParams {
95
95
  playerWallet: string;
96
96
  teamChoice: 'home' | 'away' | 'draw';
97
97
  wagerAmount: number;
98
+ /**
99
+ * When true, fund create+join with one of the user's active 0.01 SOL
100
+ * streak credits instead of paying rent + buy-in from their own
101
+ * wallet. SDK picks the oldest credit (FIFO). Treasury covers rent
102
+ * + buy-in; player only signs to consent. Buy-in is forced to the
103
+ * credit amount.
104
+ */
105
+ useCredit?: boolean;
98
106
  }
99
107
 
100
108
  export interface CreateGameResult {
@@ -103,6 +111,11 @@ export interface CreateGameResult {
103
111
  transaction: string;
104
112
  lockTimestamp: number;
105
113
  event: UnifiedEvent;
114
+ /** Set when sponsored — echoed back to confirmGame for mark-as-used. */
115
+ promoCode?: string;
116
+ sponsorWallet?: string;
117
+ /** Server-confirmed wager amount (== credit amount when sponsored). */
118
+ wagerAmount?: number;
106
119
  }
107
120
 
108
121
  // ── Custom Game (game_mode=6) ──
@@ -15,6 +15,7 @@ import { useDubsTheme } from '../theme';
15
15
  import { useDubs } from '../../provider';
16
16
  import { useCreateGame } from '../../hooks/useCreateGame';
17
17
  import type { CreateGameMutationResult } from '../../hooks/useCreateGame';
18
+ import { useCredits } from '../../hooks/useCredits';
18
19
  import type { UnifiedEvent } from '../../types';
19
20
  import { SolSlider } from './SolSlider';
20
21
  import { TeamButton } from './TeamButton';
@@ -74,10 +75,19 @@ export function CreateGameSheet({
74
75
  const t = useDubsTheme();
75
76
  const { wallet } = useDubs();
76
77
  const mutation = useCreateGame();
78
+ const { credits, refetch: refetchCredits } = useCredits();
77
79
 
78
80
  const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | null>(null);
79
81
  const [wager, setWager] = useState(0.01);
80
82
  const [showSuccess, setShowSuccess] = useState(false);
83
+ const [useCredit, setUseCredit] = useState(false);
84
+
85
+ const oldestCredit = credits.length > 0 ? credits[0] : null;
86
+ // Compare in lamports to avoid float drift between credit.amountSOL and
87
+ // the slider's wager value. The on-chain instruction requires exact
88
+ // match anyway — if either side has a sub-lamport rounding error we'd
89
+ // get a confusing on-chain failure instead of a clean toggle gate.
90
+ const canUseCredit = !!oldestCredit; // Any 0.01 SOL credit covers the 0.01 SOL minimum buy-in
81
91
 
82
92
  const overlayOpacity = useRef(new Animated.Value(0)).current;
83
93
  const successScale = useRef(new Animated.Value(0)).current;
@@ -144,12 +154,17 @@ export function CreateGameSheet({
144
154
  id: event.id,
145
155
  playerWallet: wallet.publicKey.toBase58(),
146
156
  teamChoice: selectedTeam,
147
- wagerAmount: wager,
157
+ // When sponsoring, the on-chain instruction forces buy-in to
158
+ // the credit's amount; pass that so client-side wager state
159
+ // matches what gets recorded.
160
+ wagerAmount: useCredit && oldestCredit ? oldestCredit.amountSOL : wager,
161
+ useCredit: useCredit && canUseCredit ? true : undefined,
148
162
  });
163
+ if (useCredit) refetchCredits();
149
164
  } catch {
150
165
  // Error captured in mutation state
151
166
  }
152
- }, [selectedTeam, wallet.publicKey, mutation.execute, event.id, wager]); // eslint-disable-line react-hooks/exhaustive-deps
167
+ }, [selectedTeam, wallet.publicKey, mutation.execute, event.id, wager, useCredit, oldestCredit, canUseCredit, refetchCredits]); // eslint-disable-line react-hooks/exhaustive-deps
153
168
 
154
169
  const statusLabel = STATUS_LABELS[mutation.status] || '';
155
170
 
@@ -239,8 +254,31 @@ export function CreateGameSheet({
239
254
  </View>
240
255
  </View>
241
256
 
242
- {/* SOL Slider */}
243
- {selectedTeam && (
257
+ {/* Credit toggle — only when user has at least one credit */}
258
+ {selectedTeam && canUseCredit && oldestCredit && (
259
+ <TouchableOpacity
260
+ style={[styles.creditRow, { backgroundColor: useCredit ? '#22C55E18' : t.surface, borderColor: useCredit ? '#22C55E' : t.border }]}
261
+ activeOpacity={0.8}
262
+ onPress={() => setUseCredit(v => !v)}
263
+ >
264
+ <View style={styles.creditRowText}>
265
+ <Text style={[styles.creditTitle, { color: useCredit ? '#22C55E' : t.text }]}>
266
+ 🎁 Use my {formatSol(oldestCredit.amountSOL)} SOL credit
267
+ </Text>
268
+ <Text style={[styles.creditSub, { color: t.textMuted }]}>
269
+ {useCredit
270
+ ? 'Treasury covers rent + buy-in'
271
+ : `${credits.length} credit${credits.length === 1 ? '' : 's'} from your streak`}
272
+ </Text>
273
+ </View>
274
+ <View style={[styles.creditCheckbox, useCredit && { backgroundColor: '#22C55E', borderColor: '#22C55E' }]}>
275
+ {useCredit && <Text style={styles.creditCheck}>✓</Text>}
276
+ </View>
277
+ </TouchableOpacity>
278
+ )}
279
+
280
+ {/* SOL Slider — hidden when sponsoring (wager locked to credit) */}
281
+ {selectedTeam && !useCredit && (
244
282
  <SolSlider
245
283
  value={wager}
246
284
  min={0.01}
@@ -318,6 +356,13 @@ const styles = StyleSheet.create({
318
356
  ctaText: { color: '#FFFFFF', fontSize: 16, fontWeight: '700' },
319
357
  ctaLoading: { flexDirection: 'row', alignItems: 'center', gap: 10 },
320
358
 
359
+ creditRow: { flexDirection: 'row', alignItems: 'center', gap: 12, marginTop: 12, padding: 12, borderRadius: 12, borderWidth: 1.5 },
360
+ creditRowText: { flex: 1, gap: 2 },
361
+ creditTitle: { fontSize: 14, fontWeight: '700' },
362
+ creditSub: { fontSize: 12, fontWeight: '500' },
363
+ creditCheckbox: { width: 22, height: 22, borderRadius: 11, borderWidth: 2, borderColor: '#3A3A3C', alignItems: 'center', justifyContent: 'center' },
364
+ creditCheck: { color: '#FFFFFF', fontSize: 14, fontWeight: '900' },
365
+
321
366
  successOverlay: { ...StyleSheet.absoluteFillObject, zIndex: 100, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.85)' },
322
367
  successContent: { alignItems: 'center', gap: 12 },
323
368
  successEmoji: { fontSize: 64 },
@@ -97,9 +97,14 @@ export function JoinGameSheet({
97
97
  // least one active credit AND the credit's amount covers buy-in. The
98
98
  // sponsored on-chain instruction transfers exactly the credit amount,
99
99
  // so when this is on we lock wager to the credit value.
100
+ //
101
+ // Compare in lamports to dodge float drift between credit.amountSOL
102
+ // (server-rounded to 5dp) and game.buyIn — a 0.01 SOL credit must
103
+ // visibly cover a 0.01 SOL buy-in, which `>=` on raw floats can miss.
100
104
  const [useCredit, setUseCredit] = useState(false);
101
105
  const oldestCredit = credits.length > 0 ? credits[0] : null;
102
- const canUseCredit = !!oldestCredit && oldestCredit.amountSOL >= game.buyIn;
106
+ const canUseCredit = !!oldestCredit
107
+ && Math.round(oldestCredit.amountSOL * 1e9) >= Math.round(game.buyIn * 1e9);
103
108
 
104
109
  const overlayOpacity = useRef(new Animated.Value(0)).current;
105
110
  const successScale = useRef(new Animated.Value(0)).current;