@dubsdotapp/expo 0.3.1 → 0.3.2

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.3.1",
3
+ "version": "0.3.2",
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
@@ -37,6 +37,12 @@ import type {
37
37
  UiConfig,
38
38
  UFCEvent,
39
39
  UFCFighterDetail,
40
+ ArcadePool,
41
+ ArcadeEntry,
42
+ ArcadeLeaderboardEntry,
43
+ ArcadePoolStats,
44
+ StartAttemptResult,
45
+ SubmitScoreResult,
40
46
  } from './types';
41
47
 
42
48
  export interface DubsClientConfig {
@@ -471,6 +477,75 @@ export class DubsClient {
471
477
  return { ...SOLANA_PROGRAM_ERRORS };
472
478
  }
473
479
 
480
+ // ── Arcade Pools ──
481
+
482
+ async getArcadePools(params?: { gameSlug?: string; status?: string }): Promise<ArcadePool[]> {
483
+ const qs = new URLSearchParams();
484
+ if (params?.gameSlug) qs.set('gameSlug', params.gameSlug);
485
+ if (params?.status) qs.set('status', params.status);
486
+ const query = qs.toString();
487
+ const res = await this.request<{ success: true; pools: ArcadePool[] }>(
488
+ 'GET',
489
+ `/arcade/pools${query ? `?${query}` : ''}`,
490
+ );
491
+ return res.pools;
492
+ }
493
+
494
+ async getArcadePool(poolId: number): Promise<{ pool: ArcadePool; stats: ArcadePoolStats }> {
495
+ const res = await this.request<{ success: true; pool: ArcadePool; stats: ArcadePoolStats }>(
496
+ 'GET',
497
+ `/arcade/pools/${poolId}`,
498
+ );
499
+ return { pool: res.pool, stats: res.stats };
500
+ }
501
+
502
+ async enterArcadePool(poolId: number, params: { walletAddress: string; txSignature: string }): Promise<ArcadeEntry> {
503
+ const res = await this.request<{ success: true; entry: ArcadeEntry }>(
504
+ 'POST',
505
+ `/arcade/pools/${poolId}/enter`,
506
+ params,
507
+ );
508
+ return res.entry;
509
+ }
510
+
511
+ async startArcadeAttempt(poolId: number, walletAddress: string): Promise<StartAttemptResult> {
512
+ const res = await this.request<{ success: true } & StartAttemptResult>(
513
+ 'POST',
514
+ `/arcade/pools/${poolId}/start-attempt`,
515
+ { walletAddress },
516
+ );
517
+ return { sessionToken: res.sessionToken, attemptNumber: res.attemptNumber, livesRemaining: res.livesRemaining };
518
+ }
519
+
520
+ async submitArcadeScore(poolId: number, params: { walletAddress: string; sessionToken: string; score: number; durationMs?: number }): Promise<SubmitScoreResult> {
521
+ const res = await this.request<{ success: true } & SubmitScoreResult>(
522
+ 'POST',
523
+ `/arcade/pools/${poolId}/submit-score`,
524
+ params,
525
+ );
526
+ return { score: res.score, bestScore: res.bestScore, livesUsed: res.livesUsed, isNewBest: res.isNewBest };
527
+ }
528
+
529
+ async getArcadeLeaderboard(poolId: number, params?: { limit?: number; offset?: number }): Promise<ArcadeLeaderboardEntry[]> {
530
+ const qs = new URLSearchParams();
531
+ if (params?.limit != null) qs.set('limit', String(params.limit));
532
+ if (params?.offset != null) qs.set('offset', String(params.offset));
533
+ const query = qs.toString();
534
+ const res = await this.request<{ success: true; leaderboard: ArcadeLeaderboardEntry[] }>(
535
+ 'GET',
536
+ `/arcade/pools/${poolId}/leaderboard${query ? `?${query}` : ''}`,
537
+ );
538
+ return res.leaderboard;
539
+ }
540
+
541
+ async getArcadeEntry(poolId: number, walletAddress: string): Promise<ArcadeEntry> {
542
+ const res = await this.request<{ success: true; entry: ArcadeEntry }>(
543
+ 'GET',
544
+ `/arcade/pools/${poolId}/my-entry?walletAddress=${encodeURIComponent(walletAddress)}`,
545
+ );
546
+ return res.entry;
547
+ }
548
+
474
549
  // ── App Config ──
475
550
 
476
551
  /** Fetch the app's UI customization config (accent color, icon, tagline, environment) */
@@ -18,3 +18,9 @@ export { useUFCFightCard } from './useUFCFightCard';
18
18
  export { useUFCFighterDetail } from './useUFCFighterDetail';
19
19
  export { usePushNotifications } from './usePushNotifications';
20
20
  export type { PushNotificationStatus } from './usePushNotifications';
21
+ export { useArcadePools } from './useArcadePools';
22
+ export type { UseArcadePoolsResult } from './useArcadePools';
23
+ export { useArcadePool } from './useArcadePool';
24
+ export type { UseArcadePoolResult } from './useArcadePool';
25
+ export { useArcadeGame } from './useArcadeGame';
26
+ export type { UseArcadeGameResult } from './useArcadeGame';
@@ -0,0 +1,102 @@
1
+ import { useState, useCallback } from 'react';
2
+ import { useDubs } from '../provider';
3
+ import { useAuth } from './useAuth';
4
+ import type { ArcadeEntry, StartAttemptResult, SubmitScoreResult } from '../types';
5
+
6
+ export interface UseArcadeGameResult {
7
+ entry: ArcadeEntry | null;
8
+ livesRemaining: number;
9
+ bestScore: number;
10
+ loading: boolean;
11
+ error: Error | null;
12
+ /** Fetch/refresh the user's entry for this pool */
13
+ refreshEntry: () => Promise<void>;
14
+ /** Start a new life — returns session token to pass to the game */
15
+ startAttempt: () => Promise<StartAttemptResult>;
16
+ /** Submit score after game over */
17
+ submitScore: (sessionToken: string, score: number, durationMs?: number) => Promise<SubmitScoreResult>;
18
+ }
19
+
20
+ export function useArcadeGame(poolId: number | null, maxLives: number = 3): UseArcadeGameResult {
21
+ const { client } = useDubs();
22
+ const { user } = useAuth();
23
+ const [entry, setEntry] = useState<ArcadeEntry | null>(null);
24
+ const [loading, setLoading] = useState(false);
25
+ const [error, setError] = useState<Error | null>(null);
26
+
27
+ const walletAddress = user?.walletAddress || '';
28
+
29
+ const refreshEntry = useCallback(async () => {
30
+ if (!poolId || !walletAddress) return;
31
+ setLoading(true);
32
+ setError(null);
33
+ try {
34
+ const e = await client.getArcadeEntry(poolId, walletAddress);
35
+ setEntry(e);
36
+ } catch (err) {
37
+ // 404 means no entry yet — that's fine
38
+ if (err instanceof Error && err.message.includes('No entry found')) {
39
+ setEntry(null);
40
+ } else {
41
+ setError(err instanceof Error ? err : new Error(String(err)));
42
+ }
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [client, poolId, walletAddress]);
47
+
48
+ const startAttempt = useCallback(async (): Promise<StartAttemptResult> => {
49
+ if (!poolId || !walletAddress) throw new Error('Not ready');
50
+ setError(null);
51
+ try {
52
+ const result = await client.startArcadeAttempt(poolId, walletAddress);
53
+ return result;
54
+ } catch (err) {
55
+ const e = err instanceof Error ? err : new Error(String(err));
56
+ setError(e);
57
+ throw e;
58
+ }
59
+ }, [client, poolId, walletAddress]);
60
+
61
+ const submitScore = useCallback(async (
62
+ sessionToken: string,
63
+ score: number,
64
+ durationMs?: number,
65
+ ): Promise<SubmitScoreResult> => {
66
+ if (!poolId || !walletAddress) throw new Error('Not ready');
67
+ setError(null);
68
+ try {
69
+ const result = await client.submitArcadeScore(poolId, {
70
+ walletAddress,
71
+ sessionToken,
72
+ score,
73
+ durationMs,
74
+ });
75
+ // Update local entry state
76
+ setEntry(prev => prev ? {
77
+ ...prev,
78
+ best_score: result.bestScore,
79
+ lives_used: result.livesUsed,
80
+ } : prev);
81
+ return result;
82
+ } catch (err) {
83
+ const e = err instanceof Error ? err : new Error(String(err));
84
+ setError(e);
85
+ throw e;
86
+ }
87
+ }, [client, poolId, walletAddress]);
88
+
89
+ const livesRemaining = entry ? maxLives - entry.lives_used : maxLives;
90
+ const bestScore = entry?.best_score || 0;
91
+
92
+ return {
93
+ entry,
94
+ livesRemaining,
95
+ bestScore,
96
+ loading,
97
+ error,
98
+ refreshEntry,
99
+ startAttempt,
100
+ submitScore,
101
+ };
102
+ }
@@ -0,0 +1,44 @@
1
+ import { useState, useEffect, useCallback, useMemo } from 'react';
2
+ import { useDubs } from '../provider';
3
+ import type { ArcadePool, ArcadePoolStats, ArcadeLeaderboardEntry } from '../types';
4
+
5
+ export interface UseArcadePoolResult {
6
+ pool: ArcadePool | null;
7
+ stats: ArcadePoolStats | null;
8
+ leaderboard: ArcadeLeaderboardEntry[];
9
+ loading: boolean;
10
+ error: Error | null;
11
+ refetch: () => void;
12
+ }
13
+
14
+ export function useArcadePool(poolId: number | null): UseArcadePoolResult {
15
+ const { client } = useDubs();
16
+ const [pool, setPool] = useState<ArcadePool | null>(null);
17
+ const [stats, setStats] = useState<ArcadePoolStats | null>(null);
18
+ const [leaderboard, setLeaderboard] = useState<ArcadeLeaderboardEntry[]>([]);
19
+ const [loading, setLoading] = useState(false);
20
+ const [error, setError] = useState<Error | null>(null);
21
+
22
+ const fetch = useCallback(async () => {
23
+ if (!poolId) return;
24
+ setLoading(true);
25
+ setError(null);
26
+ try {
27
+ const [poolRes, lb] = await Promise.all([
28
+ client.getArcadePool(poolId),
29
+ client.getArcadeLeaderboard(poolId),
30
+ ]);
31
+ setPool(poolRes.pool);
32
+ setStats(poolRes.stats);
33
+ setLeaderboard(lb);
34
+ } catch (err) {
35
+ setError(err instanceof Error ? err : new Error(String(err)));
36
+ } finally {
37
+ setLoading(false);
38
+ }
39
+ }, [client, poolId]);
40
+
41
+ useEffect(() => { fetch(); }, [fetch]);
42
+
43
+ return { pool, stats, leaderboard, loading, error, refetch: fetch };
44
+ }
@@ -0,0 +1,34 @@
1
+ import { useState, useEffect, useCallback } from 'react';
2
+ import { useDubs } from '../provider';
3
+ import type { ArcadePool } from '../types';
4
+
5
+ export interface UseArcadePoolsResult {
6
+ pools: ArcadePool[];
7
+ loading: boolean;
8
+ error: Error | null;
9
+ refetch: () => void;
10
+ }
11
+
12
+ export function useArcadePools(gameSlug?: string): UseArcadePoolsResult {
13
+ const { client } = useDubs();
14
+ const [pools, setPools] = useState<ArcadePool[]>([]);
15
+ const [loading, setLoading] = useState(false);
16
+ const [error, setError] = useState<Error | null>(null);
17
+
18
+ const fetch = useCallback(async () => {
19
+ setLoading(true);
20
+ setError(null);
21
+ try {
22
+ const result = await client.getArcadePools({ gameSlug });
23
+ setPools(result);
24
+ } catch (err) {
25
+ setError(err instanceof Error ? err : new Error(String(err)));
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ }, [client, gameSlug]);
30
+
31
+ useEffect(() => { fetch(); }, [fetch]);
32
+
33
+ return { pools, loading, error, refetch: fetch };
34
+ }
package/src/index.ts CHANGED
@@ -67,6 +67,13 @@ export type {
67
67
  UFCData,
68
68
  UFCFight,
69
69
  UFCEvent,
70
+ ArcadePool,
71
+ ArcadeEntry,
72
+ ArcadeAttempt,
73
+ ArcadeLeaderboardEntry,
74
+ ArcadePoolStats,
75
+ StartAttemptResult,
76
+ SubmitScoreResult,
70
77
  } from './types';
71
78
 
72
79
  // Provider
@@ -95,6 +102,9 @@ export {
95
102
  useUFCFightCard,
96
103
  useUFCFighterDetail,
97
104
  usePushNotifications,
105
+ useArcadePools,
106
+ useArcadePool,
107
+ useArcadeGame,
98
108
  } from './hooks';
99
109
  export type {
100
110
  CreateGameMutationResult,
@@ -104,6 +114,9 @@ export type {
104
114
  ClaimStatus,
105
115
  UseAuthResult,
106
116
  PushNotificationStatus,
117
+ UseArcadePoolsResult,
118
+ UseArcadePoolResult,
119
+ UseArcadeGameResult,
107
120
  } from './hooks';
108
121
 
109
122
  // UI
package/src/types.ts CHANGED
@@ -481,6 +481,76 @@ export interface UFCEvent {
481
481
  fights: UFCFight[];
482
482
  }
483
483
 
484
+ // ── Arcade Pools ──
485
+
486
+ export interface ArcadePool {
487
+ id: number;
488
+ app_id: number;
489
+ game_slug: string;
490
+ name: string;
491
+ buy_in_lamports: number;
492
+ max_lives: number;
493
+ period_start: string;
494
+ period_end: string;
495
+ schedule: 'weekly' | 'daily' | 'manual';
496
+ status: 'open' | 'active' | 'resolving' | 'complete' | 'cancelled';
497
+ solana_game_id: string | null;
498
+ solana_game_address: string | null;
499
+ total_entries: number;
500
+ created_at: string;
501
+ }
502
+
503
+ export interface ArcadeEntry {
504
+ id: number;
505
+ pool_id: number;
506
+ wallet_address: string;
507
+ best_score: number;
508
+ lives_used: number;
509
+ rank: number | null;
510
+ created_at: string;
511
+ attempts: ArcadeAttempt[] | null;
512
+ }
513
+
514
+ export interface ArcadeAttempt {
515
+ id: number;
516
+ attempt_number: number;
517
+ score: number | null;
518
+ status: 'active' | 'submitted' | 'expired';
519
+ started_at: string;
520
+ submitted_at: string | null;
521
+ duration_ms: number | null;
522
+ }
523
+
524
+ export interface ArcadeLeaderboardEntry {
525
+ id: number;
526
+ wallet_address: string;
527
+ best_score: number;
528
+ lives_used: number;
529
+ username: string;
530
+ avatar: string | null;
531
+ rank: number;
532
+ }
533
+
534
+ export interface ArcadePoolStats {
535
+ total_entries: number;
536
+ top_score: number;
537
+ avg_score: number;
538
+ active_players: number;
539
+ }
540
+
541
+ export interface StartAttemptResult {
542
+ sessionToken: string;
543
+ attemptNumber: number;
544
+ livesRemaining: number;
545
+ }
546
+
547
+ export interface SubmitScoreResult {
548
+ score: number;
549
+ bestScore: number;
550
+ livesUsed: number;
551
+ isNewBest: boolean;
552
+ }
553
+
484
554
  // ── UI Config (developer branding) ──
485
555
 
486
556
  export interface UiConfig {