@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 +1 -1
- package/src/client.ts +75 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useArcadeGame.ts +102 -0
- package/src/hooks/useArcadePool.ts +44 -0
- package/src/hooks/useArcadePools.ts +34 -0
- package/src/index.ts +13 -0
- package/src/types.ts +70 -0
package/package.json
CHANGED
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) */
|
package/src/hooks/index.ts
CHANGED
|
@@ -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 {
|