@dubsdotapp/expo 0.5.32 → 0.5.34

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.32",
3
+ "version": "0.5.34",
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',
@@ -215,6 +246,31 @@ export class DubsClient {
215
246
  }
216
247
 
217
248
  async joinGame(params: JoinGameParams): Promise<JoinGameResult> {
249
+ // Sponsored path: spend an active streak credit instead of buy-in.
250
+ // Server picks the user's oldest active credit (FIFO); we just flag
251
+ // the intent. Result includes promoCode for the confirm step to
252
+ // mark as used.
253
+ if (params.useCredit) {
254
+ const res = await this.request<{
255
+ success: true;
256
+ gameId: string;
257
+ transaction: string;
258
+ gameAddress: string;
259
+ promoCode: string;
260
+ sponsorWallet?: string;
261
+ }>('POST', '/games/join-sponsored', {
262
+ gameId: params.gameId,
263
+ teamChoice: params.teamChoice,
264
+ });
265
+ return {
266
+ gameId: res.gameId,
267
+ transaction: res.transaction,
268
+ gameAddress: res.gameAddress,
269
+ promoCode: res.promoCode,
270
+ sponsorWallet: res.sponsorWallet,
271
+ };
272
+ }
273
+
218
274
  const res = await this.request<{ success: true } & JoinGameResult>(
219
275
  'POST',
220
276
  '/games/join',
@@ -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,14 @@ 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!`;
98
105
  const gameInvite = {
99
106
  gameId: createResult.gameId,
100
107
  gameAddress: createResult.gameAddress,
101
108
  title: event?.title,
102
109
  league: event?.league,
103
110
  gameType: 'sports',
104
- buyIn: params.wagerAmount,
111
+ buyIn: wagerAmount,
105
112
  status: 'waiting',
106
113
  homeTeam: home,
107
114
  awayTeam: away,
@@ -53,6 +53,9 @@ export function useJoinGame() {
53
53
  wagerAmount: params.amount,
54
54
  role: 'joiner' as const,
55
55
  gameAddress: joinResult.gameAddress,
56
+ // Pass through when this was a sponsored join — server marks
57
+ // the credit as used after the on-chain confirm succeeds.
58
+ promoCode: joinResult.promoCode,
56
59
  };
57
60
  console.log('[useJoinGame] Step 3: Confirming with backend...', confirmParams);
58
61
  const confirmResult = await client.confirmGame(confirmParams);
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) ──
@@ -130,12 +143,26 @@ export interface JoinGameParams {
130
143
  gameId: string;
131
144
  teamChoice: 'home' | 'away' | 'draw';
132
145
  amount: number;
146
+ /**
147
+ * When true, spend one of the user's active 0.01 SOL streak credits
148
+ * instead of paying buy-in from the player's own wallet. The SDK
149
+ * picks the oldest credit (FIFO). The treasury covers buy-in + tx
150
+ * fee; the player only signs to consent. No-op if the user has no
151
+ * active credits — call useCredits() to check before enabling.
152
+ */
153
+ useCredit?: boolean;
133
154
  }
134
155
 
135
156
  export interface JoinGameResult {
136
157
  gameId: string;
137
158
  transaction: string;
138
159
  gameAddress: string;
160
+ /**
161
+ * Set when this join was sponsored. The mutation hook echoes this
162
+ * back to confirmGame so the server can mark the credit as used.
163
+ */
164
+ promoCode?: string;
165
+ sponsorWallet?: string;
139
166
  }
140
167
 
141
168
  // ── Confirm Game ──
@@ -148,6 +175,8 @@ export interface ConfirmGameParams {
148
175
  wagerAmount?: number;
149
176
  role?: 'creator' | 'joiner';
150
177
  gameAddress?: string;
178
+ /** Echoed back from a sponsored joinGame so the server marks the credit as used. */
179
+ promoCode?: string;
151
180
  }
152
181
 
153
182
  export interface ConfirmGameResult {
@@ -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 },
@@ -15,6 +15,7 @@ import { useDubsTheme } from '../theme';
15
15
  import { useDubs } from '../../provider';
16
16
  import { useJoinGame } from '../../hooks/useJoinGame';
17
17
  import type { JoinGameMutationResult } from '../../hooks/useJoinGame';
18
+ import { useCredits } from '../../hooks/useCredits';
18
19
  import type { GameDetail } from '../../types';
19
20
  import { SolSlider } from './SolSlider';
20
21
  import { TeamButton } from './TeamButton';
@@ -85,12 +86,25 @@ export function JoinGameSheet({
85
86
  const t = useDubsTheme();
86
87
  const { wallet } = useDubs();
87
88
  const mutation = useJoinGame();
89
+ const { credits, refetch: refetchCredits } = useCredits();
88
90
 
89
91
  const isCustomGame = game.gameMode === CUSTOM_GAME_MODE;
90
92
 
91
93
  const [selectedTeam, setSelectedTeam] = useState<'home' | 'away' | 'draw' | null>(null);
92
94
  const [wager, setWager] = useState(game.buyIn);
93
95
  const [showSuccess, setShowSuccess] = useState(false);
96
+ // "Use my streak credit" toggle. Only meaningful when the user has at
97
+ // least one active credit AND the credit's amount covers buy-in. The
98
+ // sponsored on-chain instruction transfers exactly the credit amount,
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.
104
+ const [useCredit, setUseCredit] = useState(false);
105
+ const oldestCredit = credits.length > 0 ? credits[0] : null;
106
+ const canUseCredit = !!oldestCredit
107
+ && Math.round(oldestCredit.amountSOL * 1e9) >= Math.round(game.buyIn * 1e9);
94
108
 
95
109
  const overlayOpacity = useRef(new Animated.Value(0)).current;
96
110
  const successScale = useRef(new Animated.Value(0)).current;
@@ -213,12 +227,18 @@ export function JoinGameSheet({
213
227
  playerWallet: wallet.publicKey.toBase58(),
214
228
  gameId: game.gameId,
215
229
  teamChoice: selectedTeam,
216
- amount: wager,
230
+ // When useCredit is on, the on-chain instruction transfers
231
+ // exactly the credit's amount; pass that as the wager.
232
+ amount: useCredit && oldestCredit ? oldestCredit.amountSOL : wager,
233
+ useCredit: useCredit && canUseCredit ? true : undefined,
217
234
  });
235
+ // The credit was just spent — refresh the list so the toggle
236
+ // disappears (or rolls to the next FIFO credit).
237
+ if (useCredit) refetchCredits();
218
238
  } catch {
219
239
  // Error is already captured in mutation state
220
240
  }
221
- }, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, wager]); // eslint-disable-line react-hooks/exhaustive-deps
241
+ }, [selectedTeam, wallet.publicKey, mutation.execute, game.gameId, wager, useCredit, oldestCredit, canUseCredit, refetchCredits]); // eslint-disable-line react-hooks/exhaustive-deps
222
242
 
223
243
  const statusLabel = STATUS_LABELS[mutation.status] || '';
224
244
 
@@ -430,8 +450,31 @@ export function JoinGameSheet({
430
450
  )}
431
451
  </View>
432
452
 
433
- {/* SOL Slidersits right below summary card */}
434
- {selectedTeam && !isPoolModeEnabled && !alreadyJoined && (
453
+ {/* Credit toggleonly when user has a credit ≥ buy-in */}
454
+ {selectedTeam && !alreadyJoined && canUseCredit && oldestCredit && (
455
+ <TouchableOpacity
456
+ style={[styles.creditRow, { backgroundColor: useCredit ? '#22C55E18' : t.surface, borderColor: useCredit ? '#22C55E' : t.border }]}
457
+ activeOpacity={0.8}
458
+ onPress={() => setUseCredit(v => !v)}
459
+ >
460
+ <View style={styles.creditRowText}>
461
+ <Text style={[styles.creditTitle, { color: useCredit ? '#22C55E' : t.text }]}>
462
+ 🎁 Use my {formatSol(oldestCredit.amountSOL)} SOL credit
463
+ </Text>
464
+ <Text style={[styles.creditSub, { color: t.textMuted }]}>
465
+ {useCredit
466
+ ? 'Treasury covers this bet — wager locked to credit amount'
467
+ : `${credits.length} credit${credits.length === 1 ? '' : 's'} available from your streak`}
468
+ </Text>
469
+ </View>
470
+ <View style={[styles.creditCheckbox, useCredit && { backgroundColor: '#22C55E', borderColor: '#22C55E' }]}>
471
+ {useCredit && <Text style={styles.creditCheck}>✓</Text>}
472
+ </View>
473
+ </TouchableOpacity>
474
+ )}
475
+
476
+ {/* SOL Slider — hidden when sponsoring with a credit */}
477
+ {selectedTeam && !isPoolModeEnabled && !alreadyJoined && !useCredit && (
435
478
  <SolSlider
436
479
  value={wager}
437
480
  min={game.buyIn}
@@ -804,4 +847,39 @@ const styles = StyleSheet.create({
804
847
  alignItems: 'center',
805
848
  gap: 10,
806
849
  },
850
+ creditRow: {
851
+ flexDirection: 'row',
852
+ alignItems: 'center',
853
+ gap: 12,
854
+ marginTop: 12,
855
+ padding: 12,
856
+ borderRadius: 12,
857
+ borderWidth: 1.5,
858
+ },
859
+ creditRowText: {
860
+ flex: 1,
861
+ gap: 2,
862
+ },
863
+ creditTitle: {
864
+ fontSize: 14,
865
+ fontWeight: '700',
866
+ },
867
+ creditSub: {
868
+ fontSize: 12,
869
+ fontWeight: '500',
870
+ },
871
+ creditCheckbox: {
872
+ width: 22,
873
+ height: 22,
874
+ borderRadius: 11,
875
+ borderWidth: 2,
876
+ borderColor: '#3A3A3C',
877
+ alignItems: 'center',
878
+ justifyContent: 'center',
879
+ },
880
+ creditCheck: {
881
+ color: '#FFFFFF',
882
+ fontSize: 14,
883
+ fontWeight: '900',
884
+ },
807
885
  });