@dubsdotapp/expo 0.1.2 → 0.2.0
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 +191 -57
- package/dist/index.d.ts +191 -57
- package/dist/index.js +1430 -507
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1484 -556
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -2
- package/src/auth-context.ts +9 -0
- package/src/client.ts +21 -1
- package/src/constants.ts +15 -0
- package/src/hooks/useAuth.ts +16 -4
- 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 +20 -2
- package/src/managed-wallet.tsx +148 -0
- package/src/provider.tsx +228 -9
- package/src/storage.ts +57 -0
- package/src/types.ts +39 -4
- package/src/ui/AuthGate.tsx +407 -308
- 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 +10 -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.
|
|
3
|
+
"version": "0.2.0",
|
|
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,7 @@ import type {
|
|
|
29
29
|
DubsPublicUser,
|
|
30
30
|
DubsAppUser,
|
|
31
31
|
CheckUsernameResult,
|
|
32
|
+
LiveScore,
|
|
32
33
|
} from './types';
|
|
33
34
|
|
|
34
35
|
export interface DubsClientConfig {
|
|
@@ -69,13 +70,23 @@ export class DubsClient {
|
|
|
69
70
|
headers['Authorization'] = `Bearer ${this._token}`;
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
console.log(`[DubsClient] ${method} ${url}`, body ? JSON.stringify(body).slice(0, 200) : '');
|
|
74
|
+
|
|
72
75
|
const res = await fetch(url, {
|
|
73
76
|
method,
|
|
74
77
|
headers,
|
|
75
78
|
body: body ? JSON.stringify(body) : undefined,
|
|
76
79
|
});
|
|
77
80
|
|
|
78
|
-
const
|
|
81
|
+
const text = await res.text();
|
|
82
|
+
console.log(`[DubsClient] ${method} ${path} → ${res.status}`, text.slice(0, 300));
|
|
83
|
+
|
|
84
|
+
let json: any;
|
|
85
|
+
try {
|
|
86
|
+
json = JSON.parse(text);
|
|
87
|
+
} catch {
|
|
88
|
+
throw new DubsApiError('parse_error', `Invalid JSON response: ${text.slice(0, 100)}`, res.status);
|
|
89
|
+
}
|
|
79
90
|
|
|
80
91
|
if (!json.success) {
|
|
81
92
|
const err = json.error;
|
|
@@ -211,6 +222,14 @@ export class DubsClient {
|
|
|
211
222
|
return res.game;
|
|
212
223
|
}
|
|
213
224
|
|
|
225
|
+
async getLiveScore(gameId: string): Promise<LiveScore | null> {
|
|
226
|
+
const res = await this.request<{ success: true; liveScore: LiveScore | null }>(
|
|
227
|
+
'GET',
|
|
228
|
+
`/games/${encodeURIComponent(gameId)}/live-score`,
|
|
229
|
+
);
|
|
230
|
+
return res.liveScore;
|
|
231
|
+
}
|
|
232
|
+
|
|
214
233
|
async getGames(params?: GetGamesParams): Promise<GameListItem[]> {
|
|
215
234
|
const qs = new URLSearchParams();
|
|
216
235
|
if (params?.wallet) qs.set('wallet', params.wallet);
|
|
@@ -230,6 +249,7 @@ export class DubsClient {
|
|
|
230
249
|
async getNetworkGames(params?: GetNetworkGamesParams): Promise<{ games: GameListItem[]; pagination: Pagination }> {
|
|
231
250
|
const qs = new URLSearchParams();
|
|
232
251
|
if (params?.league) qs.set('league', params.league);
|
|
252
|
+
if (params?.exclude_wallet) qs.set('exclude_wallet', params.exclude_wallet);
|
|
233
253
|
if (params?.limit != null) qs.set('limit', String(params.limit));
|
|
234
254
|
if (params?.offset != null) qs.set('offset', String(params.offset));
|
|
235
255
|
const query = qs.toString();
|
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 {
|
|
@@ -26,7 +27,7 @@ export interface UseAuthResult {
|
|
|
26
27
|
* Register a new user after authenticate() returned needsRegistration.
|
|
27
28
|
* Reuses the nonce+signature from authenticate() — no second signing prompt.
|
|
28
29
|
*/
|
|
29
|
-
register: (username: string, referralCode?: string) => Promise<void>;
|
|
30
|
+
register: (username: string, referralCode?: string, avatarUrl?: string) => Promise<void>;
|
|
30
31
|
|
|
31
32
|
/** Log out and clear state */
|
|
32
33
|
logout: () => Promise<void>;
|
|
@@ -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);
|
|
@@ -107,7 +111,7 @@ export function useAuth(): UseAuthResult {
|
|
|
107
111
|
}
|
|
108
112
|
}, [client, wallet]);
|
|
109
113
|
|
|
110
|
-
const register = useCallback(async (username: string, referralCode?: string) => {
|
|
114
|
+
const register = useCallback(async (username: string, referralCode?: string, avatarUrl?: string) => {
|
|
111
115
|
try {
|
|
112
116
|
const pending = pendingAuth.current;
|
|
113
117
|
if (!pending) {
|
|
@@ -123,10 +127,15 @@ export function useAuth(): UseAuthResult {
|
|
|
123
127
|
nonce: pending.nonce,
|
|
124
128
|
username,
|
|
125
129
|
referralCode,
|
|
130
|
+
avatarUrl,
|
|
126
131
|
});
|
|
127
132
|
|
|
128
133
|
pendingAuth.current = null;
|
|
129
|
-
|
|
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);
|
|
130
139
|
setToken(result.token);
|
|
131
140
|
setStatus('authenticated');
|
|
132
141
|
} catch (err) {
|
|
@@ -165,6 +174,9 @@ export function useAuth(): UseAuthResult {
|
|
|
165
174
|
}
|
|
166
175
|
}, [client]);
|
|
167
176
|
|
|
177
|
+
// If shared context exists (inside AuthGate), prefer it over local state
|
|
178
|
+
if (sharedAuth) return sharedAuth;
|
|
179
|
+
|
|
168
180
|
return {
|
|
169
181
|
status,
|
|
170
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,8 @@ export type {
|
|
|
46
52
|
RegisterResult,
|
|
47
53
|
CheckUsernameResult,
|
|
48
54
|
AuthStatus,
|
|
55
|
+
LiveScore,
|
|
56
|
+
LiveScoreCompetitor,
|
|
49
57
|
} from './types';
|
|
50
58
|
|
|
51
59
|
// Provider
|
|
@@ -79,5 +87,15 @@ export type {
|
|
|
79
87
|
export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsTheme } from './ui';
|
|
80
88
|
export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, UserProfileCardProps, SettingsSheetProps, DubsTheme } from './ui';
|
|
81
89
|
|
|
90
|
+
// Game widgets
|
|
91
|
+
export { GamePoster, LivePoolsCard, PickWinnerCard, PlayersCard, JoinGameButton } from './ui';
|
|
92
|
+
export type {
|
|
93
|
+
GamePosterProps,
|
|
94
|
+
LivePoolsCardProps,
|
|
95
|
+
PickWinnerCardProps,
|
|
96
|
+
PlayersCardProps,
|
|
97
|
+
JoinGameButtonProps,
|
|
98
|
+
} from './ui';
|
|
99
|
+
|
|
82
100
|
// Utils
|
|
83
|
-
export { signAndSendBase64Transaction
|
|
101
|
+
export { signAndSendBase64Transaction } from './utils/transaction';
|
|
@@ -0,0 +1,148 @@
|
|
|
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
|
+
children: (adapter: MwaWalletAdapter) => React.ReactNode;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Component ──
|
|
29
|
+
|
|
30
|
+
export function ManagedWalletProvider({
|
|
31
|
+
appName,
|
|
32
|
+
cluster,
|
|
33
|
+
storage,
|
|
34
|
+
renderConnectScreen,
|
|
35
|
+
children,
|
|
36
|
+
}: ManagedWalletProviderProps) {
|
|
37
|
+
const [connected, setConnected] = useState(false);
|
|
38
|
+
const [connecting, setConnecting] = useState(false);
|
|
39
|
+
const [isReady, setIsReady] = useState(false);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const adapterRef = useRef<MwaWalletAdapter | null>(null);
|
|
42
|
+
const transactRef = useRef<any>(null);
|
|
43
|
+
|
|
44
|
+
// Lazily create adapter
|
|
45
|
+
if (!adapterRef.current) {
|
|
46
|
+
adapterRef.current = new MwaWalletAdapter({
|
|
47
|
+
transact: (...args: any[]) => {
|
|
48
|
+
if (!transactRef.current) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
'@dubsdotapp/expo: @solana-mobile/mobile-wallet-adapter-protocol-web3js is required. ' +
|
|
51
|
+
'Install it with: npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return transactRef.current(...args);
|
|
55
|
+
},
|
|
56
|
+
appIdentity: { name: appName },
|
|
57
|
+
cluster,
|
|
58
|
+
onAuthTokenChange: (token) => {
|
|
59
|
+
if (token) {
|
|
60
|
+
storage.setItem(STORAGE_KEYS.MWA_AUTH_TOKEN, token).catch(() => {});
|
|
61
|
+
} else {
|
|
62
|
+
storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const adapter = adapterRef.current;
|
|
68
|
+
|
|
69
|
+
// Dynamic-import transact on mount
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
let cancelled = false;
|
|
72
|
+
|
|
73
|
+
(async () => {
|
|
74
|
+
try {
|
|
75
|
+
const mwa = await import('@solana-mobile/mobile-wallet-adapter-protocol-web3js');
|
|
76
|
+
if (cancelled) return;
|
|
77
|
+
transactRef.current = mwa.transact;
|
|
78
|
+
} catch {
|
|
79
|
+
// MWA not installed — transact calls will throw a clear error
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Attempt silent reconnect from saved token
|
|
83
|
+
try {
|
|
84
|
+
const savedToken = await storage.getItem(STORAGE_KEYS.MWA_AUTH_TOKEN);
|
|
85
|
+
if (savedToken && !cancelled) {
|
|
86
|
+
adapter.setAuthToken(savedToken);
|
|
87
|
+
await adapter.connect();
|
|
88
|
+
if (!cancelled) setConnected(true);
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// Token expired or wallet unavailable — user will tap Connect manually
|
|
92
|
+
} finally {
|
|
93
|
+
if (!cancelled) setIsReady(true);
|
|
94
|
+
}
|
|
95
|
+
})();
|
|
96
|
+
|
|
97
|
+
return () => { cancelled = true; };
|
|
98
|
+
}, [adapter, storage]);
|
|
99
|
+
|
|
100
|
+
const handleConnect = useCallback(async () => {
|
|
101
|
+
setConnecting(true);
|
|
102
|
+
setError(null);
|
|
103
|
+
try {
|
|
104
|
+
await adapter.connect();
|
|
105
|
+
setConnected(true);
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const message = err instanceof Error ? err.message : 'Connection failed';
|
|
108
|
+
setError(message);
|
|
109
|
+
} finally {
|
|
110
|
+
setConnecting(false);
|
|
111
|
+
}
|
|
112
|
+
}, [adapter]);
|
|
113
|
+
|
|
114
|
+
const disconnect = useCallback(async () => {
|
|
115
|
+
adapter.disconnect();
|
|
116
|
+
await storage.deleteItem(STORAGE_KEYS.MWA_AUTH_TOKEN).catch(() => {});
|
|
117
|
+
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
118
|
+
setConnected(false);
|
|
119
|
+
}, [adapter, storage]);
|
|
120
|
+
|
|
121
|
+
// Show nothing until we've attempted silent reconnect
|
|
122
|
+
if (!isReady) return null;
|
|
123
|
+
|
|
124
|
+
// Not connected — show connect screen
|
|
125
|
+
if (!connected) {
|
|
126
|
+
if (renderConnectScreen === false) {
|
|
127
|
+
// Headless mode — render nothing
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
const connectProps: ConnectWalletScreenProps = {
|
|
131
|
+
onConnect: handleConnect,
|
|
132
|
+
connecting,
|
|
133
|
+
error,
|
|
134
|
+
appName,
|
|
135
|
+
};
|
|
136
|
+
if (renderConnectScreen) {
|
|
137
|
+
return <>{renderConnectScreen(connectProps)}</>;
|
|
138
|
+
}
|
|
139
|
+
return <ConnectWalletScreen {...connectProps} />;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Connected — render children with adapter + disconnect
|
|
143
|
+
return (
|
|
144
|
+
<DisconnectContext.Provider value={disconnect}>
|
|
145
|
+
{children(adapter)}
|
|
146
|
+
</DisconnectContext.Provider>
|
|
147
|
+
);
|
|
148
|
+
}
|