@dubsdotapp/expo 0.1.3 → 0.2.1
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/README.md +80 -19
- package/dist/index.d.mts +207 -46
- package/dist/index.d.ts +207 -46
- package/dist/index.js +1266 -411
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1299 -444
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
- package/src/auth-context.ts +9 -0
- package/src/client.ts +30 -1
- package/src/constants.ts +15 -0
- package/src/hooks/useAuth.ts +13 -2
- package/src/hooks/useClaim.ts +11 -9
- package/src/hooks/useCreateGame.ts +15 -12
- package/src/hooks/useJoinGame.ts +18 -14
- package/src/index.ts +22 -3
- package/src/managed-wallet.tsx +158 -0
- package/src/provider.tsx +245 -9
- package/src/storage.ts +57 -0
- package/src/types.ts +47 -4
- package/src/ui/AuthGate.tsx +20 -11
- package/src/ui/ConnectWalletScreen.tsx +31 -5
- package/src/ui/game/GamePoster.tsx +182 -0
- package/src/ui/game/JoinGameButton.tsx +73 -0
- package/src/ui/game/LivePoolsCard.tsx +88 -0
- package/src/ui/game/PickWinnerCard.tsx +126 -0
- package/src/ui/game/PlayersCard.tsx +108 -0
- package/src/ui/game/index.ts +10 -0
- package/src/ui/index.ts +11 -1
- package/src/ui/theme.ts +5 -0
- package/src/utils/transaction.ts +8 -49
- package/src/wallet/mwa-adapter.ts +9 -6
- package/src/wallet/types.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dubsdotapp/expo",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "React Native SDK for the Dubs betting platform",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -28,7 +28,13 @@
|
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@solana/web3.js": "^1.90.0",
|
|
30
30
|
"react": ">=18.0.0",
|
|
31
|
-
"react-native": ">=0.72.0"
|
|
31
|
+
"react-native": ">=0.72.0",
|
|
32
|
+
"expo-secure-store": ">=13.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"expo-secure-store": {
|
|
36
|
+
"optional": true
|
|
37
|
+
}
|
|
32
38
|
},
|
|
33
39
|
"optionalDependencies": {
|
|
34
40
|
"@solana-mobile/mobile-wallet-adapter-protocol-web3js": "^2.0.0"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import type { UseAuthResult } from './hooks/useAuth';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared auth context provided by AuthGate.
|
|
6
|
+
* When useAuth() is called inside an AuthGate, it reads from this context
|
|
7
|
+
* so all components share the same auth state.
|
|
8
|
+
*/
|
|
9
|
+
export const AuthContext = createContext<UseAuthResult | null>(null);
|
package/src/client.ts
CHANGED
|
@@ -29,6 +29,8 @@ import type {
|
|
|
29
29
|
DubsPublicUser,
|
|
30
30
|
DubsAppUser,
|
|
31
31
|
CheckUsernameResult,
|
|
32
|
+
LiveScore,
|
|
33
|
+
UiConfig,
|
|
32
34
|
} from './types';
|
|
33
35
|
|
|
34
36
|
export interface DubsClientConfig {
|
|
@@ -69,13 +71,23 @@ export class DubsClient {
|
|
|
69
71
|
headers['Authorization'] = `Bearer ${this._token}`;
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
console.log(`[DubsClient] ${method} ${url}`, body ? JSON.stringify(body).slice(0, 200) : '');
|
|
75
|
+
|
|
72
76
|
const res = await fetch(url, {
|
|
73
77
|
method,
|
|
74
78
|
headers,
|
|
75
79
|
body: body ? JSON.stringify(body) : undefined,
|
|
76
80
|
});
|
|
77
81
|
|
|
78
|
-
const
|
|
82
|
+
const text = await res.text();
|
|
83
|
+
console.log(`[DubsClient] ${method} ${path} → ${res.status}`, text.slice(0, 300));
|
|
84
|
+
|
|
85
|
+
let json: any;
|
|
86
|
+
try {
|
|
87
|
+
json = JSON.parse(text);
|
|
88
|
+
} catch {
|
|
89
|
+
throw new DubsApiError('parse_error', `Invalid JSON response: ${text.slice(0, 100)}`, res.status);
|
|
90
|
+
}
|
|
79
91
|
|
|
80
92
|
if (!json.success) {
|
|
81
93
|
const err = json.error;
|
|
@@ -211,6 +223,14 @@ export class DubsClient {
|
|
|
211
223
|
return res.game;
|
|
212
224
|
}
|
|
213
225
|
|
|
226
|
+
async getLiveScore(gameId: string): Promise<LiveScore | null> {
|
|
227
|
+
const res = await this.request<{ success: true; liveScore: LiveScore | null }>(
|
|
228
|
+
'GET',
|
|
229
|
+
`/games/${encodeURIComponent(gameId)}/live-score`,
|
|
230
|
+
);
|
|
231
|
+
return res.liveScore;
|
|
232
|
+
}
|
|
233
|
+
|
|
214
234
|
async getGames(params?: GetGamesParams): Promise<GameListItem[]> {
|
|
215
235
|
const qs = new URLSearchParams();
|
|
216
236
|
if (params?.wallet) qs.set('wallet', params.wallet);
|
|
@@ -230,6 +250,7 @@ export class DubsClient {
|
|
|
230
250
|
async getNetworkGames(params?: GetNetworkGamesParams): Promise<{ games: GameListItem[]; pagination: Pagination }> {
|
|
231
251
|
const qs = new URLSearchParams();
|
|
232
252
|
if (params?.league) qs.set('league', params.league);
|
|
253
|
+
if (params?.exclude_wallet) qs.set('exclude_wallet', params.exclude_wallet);
|
|
233
254
|
if (params?.limit != null) qs.set('limit', String(params.limit));
|
|
234
255
|
if (params?.offset != null) qs.set('offset', String(params.offset));
|
|
235
256
|
const query = qs.toString();
|
|
@@ -352,4 +373,12 @@ export class DubsClient {
|
|
|
352
373
|
getErrorCodesLocal(): Record<number, SolanaErrorCode> {
|
|
353
374
|
return { ...SOLANA_PROGRAM_ERRORS };
|
|
354
375
|
}
|
|
376
|
+
|
|
377
|
+
// ── App Config ──
|
|
378
|
+
|
|
379
|
+
/** Fetch the app's UI customization config (accent color, icon, tagline) */
|
|
380
|
+
async getAppConfig(): Promise<UiConfig> {
|
|
381
|
+
const res = await this.request<{ uiConfig: UiConfig }>('GET', '/apps/config');
|
|
382
|
+
return res.uiConfig || {};
|
|
383
|
+
}
|
|
355
384
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
export const DEFAULT_BASE_URL = 'https://dubs-server-prod-9c91d3f01199.herokuapp.com/api/developer/v1';
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_RPC_URL = 'https://api.mainnet-beta.solana.com';
|
|
4
|
+
|
|
5
|
+
export type DubsNetwork = 'devnet' | 'mainnet-beta';
|
|
6
|
+
|
|
7
|
+
export const NETWORK_CONFIG: Record<DubsNetwork, { baseUrl: string; rpcUrl: string; cluster: string }> = {
|
|
8
|
+
'mainnet-beta': {
|
|
9
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
10
|
+
rpcUrl: DEFAULT_RPC_URL,
|
|
11
|
+
cluster: 'mainnet-beta',
|
|
12
|
+
},
|
|
13
|
+
devnet: {
|
|
14
|
+
baseUrl: 'https://dubs-server-dev-55d1fba09a97.herokuapp.com/api/developer/v1',
|
|
15
|
+
rpcUrl: 'https://api.devnet.solana.com',
|
|
16
|
+
cluster: 'devnet',
|
|
17
|
+
},
|
|
18
|
+
};
|
package/src/hooks/useAuth.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from 'react';
|
|
1
|
+
import { useState, useCallback, useRef, useContext } from 'react';
|
|
2
2
|
import bs58 from 'bs58';
|
|
3
3
|
import { useDubs } from '../provider';
|
|
4
|
+
import { AuthContext } from '../auth-context';
|
|
4
5
|
import type { AuthStatus, DubsUser } from '../types';
|
|
5
6
|
|
|
6
7
|
export interface UseAuthResult {
|
|
@@ -42,6 +43,9 @@ export interface UseAuthResult {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
export function useAuth(): UseAuthResult {
|
|
46
|
+
// If inside AuthGate, return the shared auth state
|
|
47
|
+
const sharedAuth = useContext(AuthContext);
|
|
48
|
+
|
|
45
49
|
const { client, wallet } = useDubs();
|
|
46
50
|
const [status, setStatus] = useState<AuthStatus>('idle');
|
|
47
51
|
const [user, setUser] = useState<DubsUser | null>(null);
|
|
@@ -127,7 +131,11 @@ export function useAuth(): UseAuthResult {
|
|
|
127
131
|
});
|
|
128
132
|
|
|
129
133
|
pendingAuth.current = null;
|
|
130
|
-
|
|
134
|
+
// Use the chosen avatar locally if the backend didn't store it
|
|
135
|
+
const user = avatarUrl && !result.user.avatar
|
|
136
|
+
? { ...result.user, avatar: avatarUrl }
|
|
137
|
+
: result.user;
|
|
138
|
+
setUser(user);
|
|
131
139
|
setToken(result.token);
|
|
132
140
|
setStatus('authenticated');
|
|
133
141
|
} catch (err) {
|
|
@@ -166,6 +174,9 @@ export function useAuth(): UseAuthResult {
|
|
|
166
174
|
}
|
|
167
175
|
}, [client]);
|
|
168
176
|
|
|
177
|
+
// If shared context exists (inside AuthGate), prefer it over local state
|
|
178
|
+
if (sharedAuth) return sharedAuth;
|
|
179
|
+
|
|
169
180
|
return {
|
|
170
181
|
status,
|
|
171
182
|
user,
|
package/src/hooks/useClaim.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { useDubs } from '../provider';
|
|
3
|
-
import { signAndSendBase64Transaction
|
|
3
|
+
import { signAndSendBase64Transaction } from '../utils/transaction';
|
|
4
4
|
import type { BuildClaimParams, MutationStatus } from '../types';
|
|
5
5
|
|
|
6
6
|
export interface ClaimMutationResult {
|
|
@@ -10,7 +10,7 @@ export interface ClaimMutationResult {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export function useClaim() {
|
|
13
|
-
const { client, wallet
|
|
13
|
+
const { client, wallet } = useDubs();
|
|
14
14
|
const [status, setStatus] = useState<MutationStatus>('idle');
|
|
15
15
|
const [error, setError] = useState<Error | null>(null);
|
|
16
16
|
const [data, setData] = useState<ClaimMutationResult | null>(null);
|
|
@@ -28,20 +28,20 @@ export function useClaim() {
|
|
|
28
28
|
|
|
29
29
|
try {
|
|
30
30
|
// 1. Build unsigned claim transaction
|
|
31
|
+
console.log('[useClaim] Step 1: Building claim transaction...');
|
|
31
32
|
const claimResult = await client.buildClaimTransaction(params);
|
|
33
|
+
console.log('[useClaim] Step 1 done.');
|
|
32
34
|
|
|
33
|
-
// 2. Sign transaction
|
|
35
|
+
// 2. Sign and send transaction
|
|
34
36
|
setStatus('signing');
|
|
37
|
+
console.log('[useClaim] Step 2: Signing and sending...');
|
|
35
38
|
const signature = await signAndSendBase64Transaction(
|
|
36
39
|
claimResult.transaction,
|
|
37
40
|
wallet,
|
|
38
|
-
connection,
|
|
39
41
|
);
|
|
42
|
+
console.log('[useClaim] Step 2 done. Signature:', signature);
|
|
40
43
|
|
|
41
|
-
//
|
|
42
|
-
setStatus('confirming');
|
|
43
|
-
await pollTransactionConfirmation(signature, connection);
|
|
44
|
-
|
|
44
|
+
// Claims don't need backend confirmation — the on-chain tx is the claim
|
|
45
45
|
const explorerUrl = `https://solscan.io/tx/${signature}`;
|
|
46
46
|
const result: ClaimMutationResult = {
|
|
47
47
|
gameId: params.gameId,
|
|
@@ -51,14 +51,16 @@ export function useClaim() {
|
|
|
51
51
|
|
|
52
52
|
setData(result);
|
|
53
53
|
setStatus('success');
|
|
54
|
+
console.log('[useClaim] Complete!');
|
|
54
55
|
return result;
|
|
55
56
|
} catch (err) {
|
|
57
|
+
console.error('[useClaim] FAILED:', err);
|
|
56
58
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
57
59
|
setError(error);
|
|
58
60
|
setStatus('error');
|
|
59
61
|
throw error;
|
|
60
62
|
}
|
|
61
|
-
}, [client, wallet
|
|
63
|
+
}, [client, wallet]);
|
|
62
64
|
|
|
63
65
|
return { execute, status, error, data, reset };
|
|
64
66
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { useDubs } from '../provider';
|
|
3
|
-
import { signAndSendBase64Transaction
|
|
4
|
-
import type { CreateGameParams,
|
|
3
|
+
import { signAndSendBase64Transaction } from '../utils/transaction';
|
|
4
|
+
import type { CreateGameParams, MutationStatus } from '../types';
|
|
5
5
|
|
|
6
6
|
export interface CreateGameMutationResult {
|
|
7
7
|
gameId: string;
|
|
@@ -11,7 +11,7 @@ export interface CreateGameMutationResult {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useCreateGame() {
|
|
14
|
-
const { client, wallet
|
|
14
|
+
const { client, wallet } = useDubs();
|
|
15
15
|
const [status, setStatus] = useState<MutationStatus>('idle');
|
|
16
16
|
const [error, setError] = useState<Error | null>(null);
|
|
17
17
|
const [data, setData] = useState<CreateGameMutationResult | null>(null);
|
|
@@ -29,23 +29,22 @@ export function useCreateGame() {
|
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
// 1. Build unsigned transaction
|
|
32
|
+
console.log('[useCreateGame] Step 1: Building transaction...');
|
|
32
33
|
const createResult = await client.createGame(params);
|
|
34
|
+
console.log('[useCreateGame] Step 1 done:', { gameId: createResult.gameId, gameAddress: createResult.gameAddress });
|
|
33
35
|
|
|
34
|
-
// 2. Sign transaction
|
|
36
|
+
// 2. Sign and send transaction
|
|
35
37
|
setStatus('signing');
|
|
38
|
+
console.log('[useCreateGame] Step 2: Signing and sending...');
|
|
36
39
|
const signature = await signAndSendBase64Transaction(
|
|
37
40
|
createResult.transaction,
|
|
38
41
|
wallet,
|
|
39
|
-
connection,
|
|
40
42
|
);
|
|
43
|
+
console.log('[useCreateGame] Step 2 done. Signature:', signature);
|
|
41
44
|
|
|
42
|
-
// 3.
|
|
45
|
+
// 3. Confirm with backend (server handles on-chain verification)
|
|
43
46
|
setStatus('confirming');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// 4. Confirm with backend
|
|
47
|
-
setStatus('saving');
|
|
48
|
-
const explorerUrl = `https://solscan.io/tx/${signature}`;
|
|
47
|
+
console.log('[useCreateGame] Step 3: Confirming with backend...');
|
|
49
48
|
await client.confirmGame({
|
|
50
49
|
gameId: createResult.gameId,
|
|
51
50
|
playerWallet: params.playerWallet,
|
|
@@ -55,7 +54,9 @@ export function useCreateGame() {
|
|
|
55
54
|
role: 'creator',
|
|
56
55
|
gameAddress: createResult.gameAddress,
|
|
57
56
|
});
|
|
57
|
+
console.log('[useCreateGame] Step 3 done.');
|
|
58
58
|
|
|
59
|
+
const explorerUrl = `https://solscan.io/tx/${signature}`;
|
|
59
60
|
const result: CreateGameMutationResult = {
|
|
60
61
|
gameId: createResult.gameId,
|
|
61
62
|
gameAddress: createResult.gameAddress,
|
|
@@ -65,14 +66,16 @@ export function useCreateGame() {
|
|
|
65
66
|
|
|
66
67
|
setData(result);
|
|
67
68
|
setStatus('success');
|
|
69
|
+
console.log('[useCreateGame] Complete!');
|
|
68
70
|
return result;
|
|
69
71
|
} catch (err) {
|
|
72
|
+
console.error('[useCreateGame] FAILED:', err);
|
|
70
73
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
71
74
|
setError(error);
|
|
72
75
|
setStatus('error');
|
|
73
76
|
throw error;
|
|
74
77
|
}
|
|
75
|
-
}, [client, wallet
|
|
78
|
+
}, [client, wallet]);
|
|
76
79
|
|
|
77
80
|
return { execute, status, error, data, reset };
|
|
78
81
|
}
|
package/src/hooks/useJoinGame.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback } from 'react';
|
|
2
2
|
import { useDubs } from '../provider';
|
|
3
|
-
import { signAndSendBase64Transaction
|
|
3
|
+
import { signAndSendBase64Transaction } from '../utils/transaction';
|
|
4
4
|
import type { JoinGameParams, MutationStatus } from '../types';
|
|
5
5
|
|
|
6
6
|
export interface JoinGameMutationResult {
|
|
@@ -11,7 +11,7 @@ export interface JoinGameMutationResult {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useJoinGame() {
|
|
14
|
-
const { client, wallet
|
|
14
|
+
const { client, wallet } = useDubs();
|
|
15
15
|
const [status, setStatus] = useState<MutationStatus>('idle');
|
|
16
16
|
const [error, setError] = useState<Error | null>(null);
|
|
17
17
|
const [data, setData] = useState<JoinGameMutationResult | null>(null);
|
|
@@ -29,33 +29,35 @@ export function useJoinGame() {
|
|
|
29
29
|
|
|
30
30
|
try {
|
|
31
31
|
// 1. Build unsigned transaction
|
|
32
|
+
console.log('[useJoinGame] Step 1: Building transaction...', { gameId: params.gameId, playerWallet: params.playerWallet, teamChoice: params.teamChoice, amount: params.amount });
|
|
32
33
|
const joinResult = await client.joinGame(params);
|
|
34
|
+
console.log('[useJoinGame] Step 1 done:', { gameId: joinResult.gameId, gameAddress: joinResult.gameAddress, hasTx: !!joinResult.transaction });
|
|
33
35
|
|
|
34
|
-
// 2. Sign transaction
|
|
36
|
+
// 2. Sign and send transaction
|
|
35
37
|
setStatus('signing');
|
|
38
|
+
console.log('[useJoinGame] Step 2: Signing and sending transaction...');
|
|
36
39
|
const signature = await signAndSendBase64Transaction(
|
|
37
40
|
joinResult.transaction,
|
|
38
41
|
wallet,
|
|
39
|
-
connection,
|
|
40
42
|
);
|
|
43
|
+
console.log('[useJoinGame] Step 2 done. Signature:', signature);
|
|
41
44
|
|
|
42
|
-
// 3.
|
|
45
|
+
// 3. Confirm with backend (server handles on-chain verification)
|
|
43
46
|
setStatus('confirming');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// 4. Confirm with backend
|
|
47
|
-
setStatus('saving');
|
|
48
|
-
const explorerUrl = `https://solscan.io/tx/${signature}`;
|
|
49
|
-
await client.confirmGame({
|
|
47
|
+
const confirmParams = {
|
|
50
48
|
gameId: params.gameId,
|
|
51
49
|
playerWallet: params.playerWallet,
|
|
52
50
|
signature,
|
|
53
51
|
teamChoice: params.teamChoice,
|
|
54
52
|
wagerAmount: params.amount,
|
|
55
|
-
role: 'joiner',
|
|
53
|
+
role: 'joiner' as const,
|
|
56
54
|
gameAddress: joinResult.gameAddress,
|
|
57
|
-
}
|
|
55
|
+
};
|
|
56
|
+
console.log('[useJoinGame] Step 3: Confirming with backend...', confirmParams);
|
|
57
|
+
await client.confirmGame(confirmParams);
|
|
58
|
+
console.log('[useJoinGame] Step 3 done. Backend confirmed.');
|
|
58
59
|
|
|
60
|
+
const explorerUrl = `https://solscan.io/tx/${signature}`;
|
|
59
61
|
const result: JoinGameMutationResult = {
|
|
60
62
|
gameId: params.gameId,
|
|
61
63
|
gameAddress: joinResult.gameAddress,
|
|
@@ -65,14 +67,16 @@ export function useJoinGame() {
|
|
|
65
67
|
|
|
66
68
|
setData(result);
|
|
67
69
|
setStatus('success');
|
|
70
|
+
console.log('[useJoinGame] Complete!');
|
|
68
71
|
return result;
|
|
69
72
|
} catch (err) {
|
|
73
|
+
console.error('[useJoinGame] FAILED at status:', status, err);
|
|
70
74
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
71
75
|
setError(error);
|
|
72
76
|
setStatus('error');
|
|
73
77
|
throw error;
|
|
74
78
|
}
|
|
75
|
-
}, [client, wallet
|
|
79
|
+
}, [client, wallet]);
|
|
76
80
|
|
|
77
81
|
return { execute, status, error, data, reset };
|
|
78
82
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
export { DubsClient } from './client';
|
|
3
3
|
export type { DubsClientConfig } from './client';
|
|
4
4
|
export { DubsApiError, parseSolanaError, SOLANA_PROGRAM_ERRORS } from './errors';
|
|
5
|
-
export { DEFAULT_BASE_URL, DEFAULT_RPC_URL } from './constants';
|
|
5
|
+
export { DEFAULT_BASE_URL, DEFAULT_RPC_URL, NETWORK_CONFIG } from './constants';
|
|
6
|
+
export type { DubsNetwork } from './constants';
|
|
7
|
+
|
|
8
|
+
// Storage
|
|
9
|
+
export { createSecureStoreStorage, STORAGE_KEYS } from './storage';
|
|
10
|
+
export type { TokenStorage } from './storage';
|
|
6
11
|
|
|
7
12
|
// Types
|
|
8
13
|
export type {
|
|
@@ -24,6 +29,7 @@ export type {
|
|
|
24
29
|
ConfirmGameResult,
|
|
25
30
|
BuildClaimParams,
|
|
26
31
|
BuildClaimResult,
|
|
32
|
+
Bettor,
|
|
27
33
|
GameDetail,
|
|
28
34
|
GameMedia,
|
|
29
35
|
GameListItem,
|
|
@@ -46,6 +52,9 @@ export type {
|
|
|
46
52
|
RegisterResult,
|
|
47
53
|
CheckUsernameResult,
|
|
48
54
|
AuthStatus,
|
|
55
|
+
LiveScore,
|
|
56
|
+
LiveScoreCompetitor,
|
|
57
|
+
UiConfig,
|
|
49
58
|
} from './types';
|
|
50
59
|
|
|
51
60
|
// Provider
|
|
@@ -76,8 +85,18 @@ export type {
|
|
|
76
85
|
} from './hooks';
|
|
77
86
|
|
|
78
87
|
// UI
|
|
79
|
-
export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsTheme } from './ui';
|
|
88
|
+
export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsTheme, mergeTheme } from './ui';
|
|
80
89
|
export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, UserProfileCardProps, SettingsSheetProps, DubsTheme } from './ui';
|
|
81
90
|
|
|
91
|
+
// Game widgets
|
|
92
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton } from './ui';
|
|
93
|
+
export type {
|
|
94
|
+
GamePosterProps,
|
|
95
|
+
LivePoolsCardProps,
|
|
96
|
+
PickWinnerCardProps,
|
|
97
|
+
PlayersCardProps,
|
|
98
|
+
JoinGameButtonProps,
|
|
99
|
+
} from './ui';
|
|
100
|
+
|
|
82
101
|
// Utils
|
|
83
|
-
export { signAndSendBase64Transaction
|
|
102
|
+
export { signAndSendBase64Transaction } from './utils/transaction';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState, useEffect, useRef, useCallback } from 'react';
|
|
2
|
+
import { MwaWalletAdapter } from './wallet/mwa-adapter';
|
|
3
|
+
import { ConnectWalletScreen } from './ui/ConnectWalletScreen';
|
|
4
|
+
import type { ConnectWalletScreenProps } from './ui/ConnectWalletScreen';
|
|
5
|
+
import type { TokenStorage } from './storage';
|
|
6
|
+
import { STORAGE_KEYS } from './storage';
|
|
7
|
+
|
|
8
|
+
// ── Disconnect Context (internal) ──
|
|
9
|
+
|
|
10
|
+
type DisconnectFn = () => Promise<void>;
|
|
11
|
+
|
|
12
|
+
export const DisconnectContext = createContext<DisconnectFn | null>(null);
|
|
13
|
+
|
|
14
|
+
export function useDisconnect(): DisconnectFn | null {
|
|
15
|
+
return useContext(DisconnectContext);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Props ──
|
|
19
|
+
|
|
20
|
+
interface ManagedWalletProviderProps {
|
|
21
|
+
appName: string;
|
|
22
|
+
cluster: string;
|
|
23
|
+
storage: TokenStorage;
|
|
24
|
+
renderConnectScreen?: ((props: ConnectWalletScreenProps) => React.ReactNode) | false;
|
|
25
|
+
/** Developer UI config overrides for the connect screen */
|
|
26
|
+
accentColor?: string;
|
|
27
|
+
appIcon?: string;
|
|
28
|
+
tagline?: string;
|
|
29
|
+
children: (adapter: MwaWalletAdapter) => React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Component ──
|
|
33
|
+
|
|
34
|
+
export function ManagedWalletProvider({
|
|
35
|
+
appName,
|
|
36
|
+
cluster,
|
|
37
|
+
storage,
|
|
38
|
+
renderConnectScreen,
|
|
39
|
+
accentColor,
|
|
40
|
+
appIcon,
|
|
41
|
+
tagline,
|
|
42
|
+
children,
|
|
43
|
+
}: ManagedWalletProviderProps) {
|
|
44
|
+
const [connected, setConnected] = useState(false);
|
|
45
|
+
const [connecting, setConnecting] = useState(false);
|
|
46
|
+
const [isReady, setIsReady] = useState(false);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const adapterRef = useRef<MwaWalletAdapter | null>(null);
|
|
49
|
+
const transactRef = useRef<any>(null);
|
|
50
|
+
|
|
51
|
+
// Lazily create adapter
|
|
52
|
+
if (!adapterRef.current) {
|
|
53
|
+
adapterRef.current = new MwaWalletAdapter({
|
|
54
|
+
transact: (...args: any[]) => {
|
|
55
|
+
if (!transactRef.current) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'@dubsdotapp/expo: @solana-mobile/mobile-wallet-adapter-protocol-web3js is required. ' +
|
|
58
|
+
'Install it with: npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return transactRef.current(...args);
|
|
62
|
+
},
|
|
63
|
+
appIdentity: { name: appName },
|
|
64
|
+
cluster,
|
|
65
|
+
onAuthTokenChange: (token) => {
|
|
66
|
+
if (token) {
|
|
67
|
+
storage.setItem(STORAGE_KEYS.MWA_AUTH_TOKEN, token).catch(() => {});
|
|
68
|
+
} else {
|
|
69
|
+
storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const adapter = adapterRef.current;
|
|
75
|
+
|
|
76
|
+
// Dynamic-import transact on mount
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
let cancelled = false;
|
|
79
|
+
|
|
80
|
+
(async () => {
|
|
81
|
+
try {
|
|
82
|
+
const mwa = await import('@solana-mobile/mobile-wallet-adapter-protocol-web3js');
|
|
83
|
+
if (cancelled) return;
|
|
84
|
+
transactRef.current = mwa.transact;
|
|
85
|
+
} catch {
|
|
86
|
+
// MWA not installed — transact calls will throw a clear error
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Attempt silent reconnect from saved token
|
|
90
|
+
try {
|
|
91
|
+
const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
|
|
92
|
+
if (savedToken && !cancelled) {
|
|
93
|
+
adapter.setAuthToken(savedToken);
|
|
94
|
+
await adapter.connect();
|
|
95
|
+
if (!cancelled) setConnected(true);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Token expired or wallet unavailable — user will tap Connect manually
|
|
99
|
+
} finally {
|
|
100
|
+
if (!cancelled) setIsReady(true);
|
|
101
|
+
}
|
|
102
|
+
})();
|
|
103
|
+
|
|
104
|
+
return () => { cancelled = true; };
|
|
105
|
+
}, [adapter, storage]);
|
|
106
|
+
|
|
107
|
+
const handleConnect = useCallback(async () => {
|
|
108
|
+
setConnecting(true);
|
|
109
|
+
setError(null);
|
|
110
|
+
try {
|
|
111
|
+
await adapter.connect();
|
|
112
|
+
setConnected(true);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
const message = err instanceof Error ? err.message : 'Connection failed';
|
|
115
|
+
setError(message);
|
|
116
|
+
} finally {
|
|
117
|
+
setConnecting(false);
|
|
118
|
+
}
|
|
119
|
+
}, [adapter]);
|
|
120
|
+
|
|
121
|
+
const disconnect = useCallback(async () => {
|
|
122
|
+
adapter.disconnect();
|
|
123
|
+
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
124
|
+
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
125
|
+
setConnected(false);
|
|
126
|
+
}, [adapter, storage]);
|
|
127
|
+
|
|
128
|
+
// Show nothing until we've attempted silent reconnect
|
|
129
|
+
if (!isReady) return null;
|
|
130
|
+
|
|
131
|
+
// Not connected — show connect screen
|
|
132
|
+
if (!connected) {
|
|
133
|
+
if (renderConnectScreen === false) {
|
|
134
|
+
// Headless mode — render nothing
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const connectProps: ConnectWalletScreenProps = {
|
|
138
|
+
onConnect: handleConnect,
|
|
139
|
+
connecting,
|
|
140
|
+
error,
|
|
141
|
+
appName,
|
|
142
|
+
accentColor,
|
|
143
|
+
appIcon,
|
|
144
|
+
tagline,
|
|
145
|
+
};
|
|
146
|
+
if (renderConnectScreen) {
|
|
147
|
+
return <>{renderConnectScreen(connectProps)}</>;
|
|
148
|
+
}
|
|
149
|
+
return <ConnectWalletScreen {...connectProps} />;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Connected — render children with adapter + disconnect
|
|
153
|
+
return (
|
|
154
|
+
<DisconnectContext.Provider value={disconnect}>
|
|
155
|
+
{children(adapter)}
|
|
156
|
+
</DisconnectContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|