@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/src/provider.tsx
CHANGED
|
@@ -1,35 +1,271 @@
|
|
|
1
|
-
import React, { createContext, useContext, useMemo } from 'react';
|
|
1
|
+
import React, { createContext, useContext, useMemo, useCallback, useState, useEffect } from 'react';
|
|
2
2
|
import { Connection } from '@solana/web3.js';
|
|
3
3
|
import { DubsClient } from './client';
|
|
4
|
-
import {
|
|
4
|
+
import { NETWORK_CONFIG } from './constants';
|
|
5
|
+
import type { DubsNetwork } from './constants';
|
|
5
6
|
import type { WalletAdapter } from './wallet/types';
|
|
7
|
+
import type { TokenStorage } from './storage';
|
|
8
|
+
import { createSecureStoreStorage, STORAGE_KEYS } from './storage';
|
|
9
|
+
import { ManagedWalletProvider, useDisconnect } from './managed-wallet';
|
|
10
|
+
import { AuthGate } from './ui/AuthGate';
|
|
11
|
+
import type { RegistrationScreenProps } from './ui/AuthGate';
|
|
12
|
+
import type { ConnectWalletScreenProps } from './ui/ConnectWalletScreen';
|
|
13
|
+
import type { AuthStatus, UiConfig } from './types';
|
|
14
|
+
|
|
15
|
+
// ── Context ──
|
|
6
16
|
|
|
7
17
|
export interface DubsContextValue {
|
|
8
18
|
client: DubsClient;
|
|
9
19
|
wallet: WalletAdapter;
|
|
10
20
|
connection: Connection;
|
|
21
|
+
appName: string;
|
|
22
|
+
network: DubsNetwork;
|
|
23
|
+
disconnect: () => Promise<void>;
|
|
11
24
|
}
|
|
12
25
|
|
|
13
26
|
const DubsContext = createContext<DubsContextValue | null>(null);
|
|
14
27
|
|
|
28
|
+
// ── Props ──
|
|
29
|
+
|
|
15
30
|
export interface DubsProviderProps {
|
|
16
31
|
apiKey: string;
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
appName?: string;
|
|
34
|
+
network?: DubsNetwork;
|
|
35
|
+
/** Escape hatch: bring your own wallet adapter. Disables managed MWA. */
|
|
36
|
+
wallet?: WalletAdapter;
|
|
37
|
+
/** Escape hatch: custom token persistence. Defaults to expo-secure-store. */
|
|
38
|
+
tokenStorage?: TokenStorage;
|
|
39
|
+
/** Escape hatch: override base URL (takes precedence over network). */
|
|
17
40
|
baseUrl?: string;
|
|
41
|
+
/** Escape hatch: override RPC URL (takes precedence over network). */
|
|
18
42
|
rpcUrl?: string;
|
|
43
|
+
/** Custom connect screen, or false to hide it entirely (headless mode). */
|
|
44
|
+
renderConnectScreen?: ((props: ConnectWalletScreenProps) => React.ReactNode) | false;
|
|
45
|
+
/** Custom loading screen shown during auth. */
|
|
46
|
+
renderLoading?: (status: AuthStatus) => React.ReactNode;
|
|
47
|
+
/** Custom error screen shown on auth failure. */
|
|
48
|
+
renderError?: (error: Error, retry: () => void) => React.ReactNode;
|
|
49
|
+
/** Custom registration screen. */
|
|
50
|
+
renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
|
|
51
|
+
/** Set to false to skip AuthGate and connect screen (headless/BYOA mode). Default: true. */
|
|
52
|
+
managed?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Provider ──
|
|
56
|
+
|
|
57
|
+
export function DubsProvider({
|
|
58
|
+
apiKey,
|
|
59
|
+
children,
|
|
60
|
+
appName = 'Dubs',
|
|
61
|
+
network = 'mainnet-beta',
|
|
62
|
+
wallet: externalWallet,
|
|
63
|
+
tokenStorage,
|
|
64
|
+
baseUrl: baseUrlOverride,
|
|
65
|
+
rpcUrl: rpcUrlOverride,
|
|
66
|
+
renderConnectScreen,
|
|
67
|
+
renderLoading,
|
|
68
|
+
renderError,
|
|
69
|
+
renderRegistration,
|
|
70
|
+
managed = true,
|
|
71
|
+
}: DubsProviderProps) {
|
|
72
|
+
// Resolve network config — explicit props override network defaults
|
|
73
|
+
const config = NETWORK_CONFIG[network];
|
|
74
|
+
const baseUrl = baseUrlOverride || config.baseUrl;
|
|
75
|
+
const rpcUrl = rpcUrlOverride || config.rpcUrl;
|
|
76
|
+
const cluster = config.cluster;
|
|
77
|
+
|
|
78
|
+
// Create stable instances
|
|
79
|
+
const client = useMemo(() => new DubsClient({ apiKey, baseUrl }), [apiKey, baseUrl]);
|
|
80
|
+
const connection = useMemo(() => new Connection(rpcUrl, { commitment: 'confirmed' }), [rpcUrl]);
|
|
81
|
+
const storage = useMemo(() => tokenStorage || createSecureStoreStorage(), [tokenStorage]);
|
|
82
|
+
|
|
83
|
+
// Fetch developer UI config on mount (silent fail = default theme)
|
|
84
|
+
const [uiConfig, setUiConfig] = useState<UiConfig>({});
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
client.getAppConfig().then(setUiConfig).catch(() => {});
|
|
87
|
+
}, [client]);
|
|
88
|
+
|
|
89
|
+
// ── Path A: External wallet provided (BYOA) ──
|
|
90
|
+
if (externalWallet) {
|
|
91
|
+
return (
|
|
92
|
+
<ExternalWalletProvider
|
|
93
|
+
client={client}
|
|
94
|
+
connection={connection}
|
|
95
|
+
wallet={externalWallet}
|
|
96
|
+
appName={uiConfig.appName || appName}
|
|
97
|
+
network={network}
|
|
98
|
+
storage={storage}
|
|
99
|
+
managed={managed}
|
|
100
|
+
renderLoading={renderLoading}
|
|
101
|
+
renderError={renderError}
|
|
102
|
+
renderRegistration={renderRegistration}
|
|
103
|
+
accentColor={uiConfig.accentColor}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</ExternalWalletProvider>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Path B: Managed MWA wallet ──
|
|
111
|
+
return (
|
|
112
|
+
<ManagedWalletProvider
|
|
113
|
+
appName={uiConfig.appName || appName}
|
|
114
|
+
cluster={cluster}
|
|
115
|
+
storage={storage}
|
|
116
|
+
renderConnectScreen={renderConnectScreen}
|
|
117
|
+
accentColor={uiConfig.accentColor}
|
|
118
|
+
appIcon={uiConfig.appIcon}
|
|
119
|
+
tagline={uiConfig.tagline}
|
|
120
|
+
>
|
|
121
|
+
{(adapter) => (
|
|
122
|
+
<ManagedInner
|
|
123
|
+
client={client}
|
|
124
|
+
connection={connection}
|
|
125
|
+
wallet={adapter}
|
|
126
|
+
appName={uiConfig.appName || appName}
|
|
127
|
+
network={network}
|
|
128
|
+
storage={storage}
|
|
129
|
+
renderLoading={renderLoading}
|
|
130
|
+
renderError={renderError}
|
|
131
|
+
renderRegistration={renderRegistration}
|
|
132
|
+
accentColor={uiConfig.accentColor}
|
|
133
|
+
>
|
|
134
|
+
{children}
|
|
135
|
+
</ManagedInner>
|
|
136
|
+
)}
|
|
137
|
+
</ManagedWalletProvider>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── ManagedInner: context + AuthGate for managed wallet path ──
|
|
142
|
+
|
|
143
|
+
function ManagedInner({
|
|
144
|
+
client,
|
|
145
|
+
connection,
|
|
146
|
+
wallet,
|
|
147
|
+
appName,
|
|
148
|
+
network,
|
|
149
|
+
storage,
|
|
150
|
+
renderLoading,
|
|
151
|
+
renderError,
|
|
152
|
+
renderRegistration,
|
|
153
|
+
accentColor,
|
|
154
|
+
children,
|
|
155
|
+
}: {
|
|
156
|
+
client: DubsClient;
|
|
157
|
+
connection: Connection;
|
|
19
158
|
wallet: WalletAdapter;
|
|
159
|
+
appName: string;
|
|
160
|
+
network: DubsNetwork;
|
|
161
|
+
storage: TokenStorage;
|
|
162
|
+
renderLoading?: (status: AuthStatus) => React.ReactNode;
|
|
163
|
+
renderError?: (error: Error, retry: () => void) => React.ReactNode;
|
|
164
|
+
renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
|
|
165
|
+
accentColor?: string;
|
|
20
166
|
children: React.ReactNode;
|
|
167
|
+
}) {
|
|
168
|
+
const managedDisconnect = useDisconnect();
|
|
169
|
+
|
|
170
|
+
const disconnect = useCallback(async () => {
|
|
171
|
+
// Clear client JWT
|
|
172
|
+
client.setToken(null);
|
|
173
|
+
// Delegate to managed wallet (clears adapter + storage)
|
|
174
|
+
await managedDisconnect?.();
|
|
175
|
+
}, [client, managedDisconnect]);
|
|
176
|
+
|
|
177
|
+
const value = useMemo<DubsContextValue>(
|
|
178
|
+
() => ({ client, wallet, connection, appName, network, disconnect }),
|
|
179
|
+
[client, wallet, connection, appName, network, disconnect],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<DubsContext.Provider value={value}>
|
|
184
|
+
<AuthGate
|
|
185
|
+
onSaveToken={(token) => {
|
|
186
|
+
if (token) return storage.setItem(STORAGE_KEYS.JWT_TOKEN, token);
|
|
187
|
+
return storage.deleteItem(STORAGE_KEYS.JWT_TOKEN);
|
|
188
|
+
}}
|
|
189
|
+
onLoadToken={() => storage.getItem(STORAGE_KEYS.JWT_TOKEN)}
|
|
190
|
+
renderLoading={renderLoading}
|
|
191
|
+
renderError={renderError}
|
|
192
|
+
renderRegistration={renderRegistration}
|
|
193
|
+
appName={appName}
|
|
194
|
+
accentColor={accentColor}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</AuthGate>
|
|
198
|
+
</DubsContext.Provider>
|
|
199
|
+
);
|
|
21
200
|
}
|
|
22
201
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
202
|
+
// ── ExternalWalletProvider: for BYOA path ──
|
|
203
|
+
|
|
204
|
+
function ExternalWalletProvider({
|
|
205
|
+
client,
|
|
206
|
+
connection,
|
|
207
|
+
wallet,
|
|
208
|
+
appName,
|
|
209
|
+
network,
|
|
210
|
+
storage,
|
|
211
|
+
managed,
|
|
212
|
+
renderLoading,
|
|
213
|
+
renderError,
|
|
214
|
+
renderRegistration,
|
|
215
|
+
accentColor,
|
|
216
|
+
children,
|
|
217
|
+
}: {
|
|
218
|
+
client: DubsClient;
|
|
219
|
+
connection: Connection;
|
|
220
|
+
wallet: WalletAdapter;
|
|
221
|
+
appName: string;
|
|
222
|
+
network: DubsNetwork;
|
|
223
|
+
storage: TokenStorage;
|
|
224
|
+
managed: boolean;
|
|
225
|
+
renderLoading?: (status: AuthStatus) => React.ReactNode;
|
|
226
|
+
renderError?: (error: Error, retry: () => void) => React.ReactNode;
|
|
227
|
+
renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
|
|
228
|
+
accentColor?: string;
|
|
229
|
+
children: React.ReactNode;
|
|
230
|
+
}) {
|
|
231
|
+
const disconnect = useCallback(async () => {
|
|
232
|
+
client.setToken(null);
|
|
233
|
+
await storage.deleteItem(STORAGE_KEYS.JWT_TOKEN).catch(() => {});
|
|
234
|
+
await wallet.disconnect?.();
|
|
235
|
+
}, [client, storage, wallet]);
|
|
236
|
+
|
|
237
|
+
const value = useMemo<DubsContextValue>(
|
|
238
|
+
() => ({ client, wallet, connection, appName, network, disconnect }),
|
|
239
|
+
[client, wallet, connection, appName, network, disconnect],
|
|
240
|
+
);
|
|
29
241
|
|
|
30
|
-
|
|
242
|
+
if (!managed) {
|
|
243
|
+
// Headless mode — just context, no AuthGate
|
|
244
|
+
return <DubsContext.Provider value={value}>{children}</DubsContext.Provider>;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return (
|
|
248
|
+
<DubsContext.Provider value={value}>
|
|
249
|
+
<AuthGate
|
|
250
|
+
onSaveToken={(token) => {
|
|
251
|
+
if (token) return storage.setItem(STORAGE_KEYS.JWT_TOKEN, token);
|
|
252
|
+
return storage.deleteItem(STORAGE_KEYS.JWT_TOKEN);
|
|
253
|
+
}}
|
|
254
|
+
onLoadToken={() => storage.getItem(STORAGE_KEYS.JWT_TOKEN)}
|
|
255
|
+
renderLoading={renderLoading}
|
|
256
|
+
renderError={renderError}
|
|
257
|
+
renderRegistration={renderRegistration}
|
|
258
|
+
appName={appName}
|
|
259
|
+
accentColor={accentColor}
|
|
260
|
+
>
|
|
261
|
+
{children}
|
|
262
|
+
</AuthGate>
|
|
263
|
+
</DubsContext.Provider>
|
|
264
|
+
);
|
|
31
265
|
}
|
|
32
266
|
|
|
267
|
+
// ── Hook ──
|
|
268
|
+
|
|
33
269
|
export function useDubs(): DubsContextValue {
|
|
34
270
|
const ctx = useContext(DubsContext);
|
|
35
271
|
if (!ctx) {
|
package/src/storage.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface TokenStorage {
|
|
2
|
+
getItem(key: string): Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
deleteItem(key: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const STORAGE_KEYS = {
|
|
8
|
+
MWA_AUTH_TOKEN: 'dubs_mwa_auth_token',
|
|
9
|
+
JWT_TOKEN: 'dubs_jwt_token',
|
|
10
|
+
} as const;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a TokenStorage backed by expo-secure-store.
|
|
14
|
+
* Lazy-imports the module so it's only required at runtime when actually used.
|
|
15
|
+
* Throws a clear error if expo-secure-store is not installed.
|
|
16
|
+
*/
|
|
17
|
+
export function createSecureStoreStorage(): TokenStorage {
|
|
18
|
+
// Use `any` to avoid requiring expo-secure-store types at build time.
|
|
19
|
+
// The module is lazy-imported at runtime only when this storage is actually used.
|
|
20
|
+
let SecureStore: any = null;
|
|
21
|
+
|
|
22
|
+
function getStore(): {
|
|
23
|
+
getItemAsync: (key: string) => Promise<string | null>;
|
|
24
|
+
setItemAsync: (key: string, value: string) => Promise<void>;
|
|
25
|
+
deleteItemAsync: (key: string) => Promise<void>;
|
|
26
|
+
} {
|
|
27
|
+
if (!SecureStore) {
|
|
28
|
+
try {
|
|
29
|
+
// Use require() to keep the import opaque to TypeScript's DTS generation.
|
|
30
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
31
|
+
SecureStore = require('expo-secure-store');
|
|
32
|
+
} catch {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'@dubsdotapp/expo: expo-secure-store is required for default token storage. ' +
|
|
35
|
+
'Install it with: npx expo install expo-secure-store — ' +
|
|
36
|
+
'or pass a custom tokenStorage prop to <DubsProvider>.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return SecureStore;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
async getItem(key: string): Promise<string | null> {
|
|
45
|
+
const store = getStore();
|
|
46
|
+
return store.getItemAsync(key);
|
|
47
|
+
},
|
|
48
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
49
|
+
const store = getStore();
|
|
50
|
+
await store.setItemAsync(key, value);
|
|
51
|
+
},
|
|
52
|
+
async deleteItem(key: string): Promise<void> {
|
|
53
|
+
const store = getStore();
|
|
54
|
+
await store.deleteItemAsync(key);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -159,6 +159,14 @@ export interface GameMedia {
|
|
|
159
159
|
thumbnail: string | null;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
export interface Bettor {
|
|
163
|
+
wallet: string;
|
|
164
|
+
username: string | null;
|
|
165
|
+
avatar: string | null;
|
|
166
|
+
team: 'home' | 'away' | 'draw';
|
|
167
|
+
amount: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
export interface GameDetail {
|
|
163
171
|
gameId: string;
|
|
164
172
|
gameAddress: string;
|
|
@@ -168,15 +176,14 @@ export interface GameDetail {
|
|
|
168
176
|
isLocked: boolean;
|
|
169
177
|
isResolved: boolean;
|
|
170
178
|
status: string;
|
|
179
|
+
league: string | null;
|
|
171
180
|
lockTimestamp: number | null;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
drawPlayers: string[];
|
|
181
|
+
opponents: GameListOpponent[];
|
|
182
|
+
bettors: Bettor[];
|
|
175
183
|
homePool: number;
|
|
176
184
|
awayPool: number;
|
|
177
185
|
drawPool: number;
|
|
178
186
|
totalPool: number;
|
|
179
|
-
sportsEvent: Record<string, unknown> | null;
|
|
180
187
|
media: GameMedia;
|
|
181
188
|
createdAt: string;
|
|
182
189
|
updatedAt: string;
|
|
@@ -216,6 +223,7 @@ export interface GetGamesParams {
|
|
|
216
223
|
|
|
217
224
|
export interface GetNetworkGamesParams {
|
|
218
225
|
league?: string;
|
|
226
|
+
exclude_wallet?: string;
|
|
219
227
|
limit?: number;
|
|
220
228
|
offset?: number;
|
|
221
229
|
}
|
|
@@ -339,3 +347,38 @@ export interface MutationResult<TParams, TResult> {
|
|
|
339
347
|
data: TResult | null;
|
|
340
348
|
reset: () => void;
|
|
341
349
|
}
|
|
350
|
+
|
|
351
|
+
// ── Live Scores ──
|
|
352
|
+
|
|
353
|
+
export interface LiveScoreCompetitor {
|
|
354
|
+
name: string;
|
|
355
|
+
homeAway: 'home' | 'away';
|
|
356
|
+
score: number;
|
|
357
|
+
logo: string | null;
|
|
358
|
+
abbreviation: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export interface LiveScore {
|
|
362
|
+
status: string;
|
|
363
|
+
period: number | null;
|
|
364
|
+
displayClock: string | null;
|
|
365
|
+
detail: string | null;
|
|
366
|
+
shortDetail: string | null;
|
|
367
|
+
competitors: LiveScoreCompetitor[];
|
|
368
|
+
ufcData?: {
|
|
369
|
+
currentRound: number;
|
|
370
|
+
totalRounds: number;
|
|
371
|
+
clock: string;
|
|
372
|
+
fightState: string;
|
|
373
|
+
statusDetail: string;
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── UI Config (developer branding) ──
|
|
378
|
+
|
|
379
|
+
export interface UiConfig {
|
|
380
|
+
accentColor?: string;
|
|
381
|
+
appIcon?: string;
|
|
382
|
+
appName?: string;
|
|
383
|
+
tagline?: string;
|
|
384
|
+
}
|
package/src/ui/AuthGate.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from 'react-native';
|
|
16
16
|
import { useAuth } from '../hooks';
|
|
17
17
|
import { useDubs } from '../provider';
|
|
18
|
+
import { AuthContext } from '../auth-context';
|
|
18
19
|
import { useDubsTheme } from './theme';
|
|
19
20
|
import type { AuthStatus } from '../types';
|
|
20
21
|
import type { DubsClient } from '../client';
|
|
@@ -55,6 +56,8 @@ export interface AuthGateProps {
|
|
|
55
56
|
renderError?: (error: Error, retry: () => void) => React.ReactNode;
|
|
56
57
|
renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
|
|
57
58
|
appName?: string;
|
|
59
|
+
/** Override accent color for registration screens (from developer UI config) */
|
|
60
|
+
accentColor?: string;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
// ── AuthGate Component ──
|
|
@@ -67,6 +70,7 @@ export function AuthGate({
|
|
|
67
70
|
renderError,
|
|
68
71
|
renderRegistration,
|
|
69
72
|
appName = 'Dubs',
|
|
73
|
+
accentColor,
|
|
70
74
|
}: AuthGateProps) {
|
|
71
75
|
const { client } = useDubs();
|
|
72
76
|
const auth = useAuth();
|
|
@@ -125,7 +129,9 @@ export function AuthGate({
|
|
|
125
129
|
return <DefaultLoadingScreen status="authenticating" appName={appName} />;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
if (auth.status === 'authenticated')
|
|
132
|
+
if (auth.status === 'authenticated') {
|
|
133
|
+
return <AuthContext.Provider value={auth}>{children}</AuthContext.Provider>;
|
|
134
|
+
}
|
|
129
135
|
|
|
130
136
|
if (registrationPhase) {
|
|
131
137
|
const isRegistering = auth.status === 'registering';
|
|
@@ -140,6 +146,7 @@ export function AuthGate({
|
|
|
140
146
|
error={regError}
|
|
141
147
|
client={client}
|
|
142
148
|
appName={appName}
|
|
149
|
+
accentColor={accentColor}
|
|
143
150
|
/>
|
|
144
151
|
);
|
|
145
152
|
}
|
|
@@ -253,8 +260,10 @@ function DefaultRegistrationScreen({
|
|
|
253
260
|
error,
|
|
254
261
|
client,
|
|
255
262
|
appName,
|
|
256
|
-
|
|
263
|
+
accentColor,
|
|
264
|
+
}: RegistrationScreenProps & { appName: string; accentColor?: string }) {
|
|
257
265
|
const t = useDubsTheme();
|
|
266
|
+
const accent = accentColor || t.accent;
|
|
258
267
|
|
|
259
268
|
// ── Shared state ──
|
|
260
269
|
const [step, setStep] = useState(0);
|
|
@@ -322,7 +331,7 @@ function DefaultRegistrationScreen({
|
|
|
322
331
|
<StepIndicator currentStep={0} />
|
|
323
332
|
|
|
324
333
|
<View style={s.avatarCenter}>
|
|
325
|
-
<View style={[s.avatarFrame, { borderColor:
|
|
334
|
+
<View style={[s.avatarFrame, { borderColor: accent }]}>
|
|
326
335
|
<Image source={{ uri: avatarUrl }} style={s.avatarLarge} />
|
|
327
336
|
<View style={[s.checkBadge, { backgroundColor: t.success }]}>
|
|
328
337
|
<Text style={s.checkBadgeText}>✓</Text>
|
|
@@ -339,11 +348,11 @@ function DefaultRegistrationScreen({
|
|
|
339
348
|
<Text style={[s.outlineBtnText, { color: t.text }]}>↻ Shuffle</Text>
|
|
340
349
|
</TouchableOpacity>
|
|
341
350
|
<TouchableOpacity
|
|
342
|
-
style={[s.outlineBtn, { borderColor:
|
|
351
|
+
style={[s.outlineBtn, { borderColor: accent, backgroundColor: accent + '15' }]}
|
|
343
352
|
onPress={() => setShowStyles(!showStyles)}
|
|
344
353
|
activeOpacity={0.7}
|
|
345
354
|
>
|
|
346
|
-
<Text style={[s.outlineBtnText, { color:
|
|
355
|
+
<Text style={[s.outlineBtnText, { color: accent }]}>☺ Customize</Text>
|
|
347
356
|
</TouchableOpacity>
|
|
348
357
|
</View>
|
|
349
358
|
|
|
@@ -353,7 +362,7 @@ function DefaultRegistrationScreen({
|
|
|
353
362
|
<TouchableOpacity
|
|
354
363
|
key={st}
|
|
355
364
|
onPress={() => setAvatarStyle(st)}
|
|
356
|
-
style={[s.styleThumbWrap, { borderColor: st === avatarStyle ?
|
|
365
|
+
style={[s.styleThumbWrap, { borderColor: st === avatarStyle ? accent : t.border }]}
|
|
357
366
|
>
|
|
358
367
|
<Image source={{ uri: getAvatarUrl(st, avatarSeed, 80) }} style={s.styleThumb} />
|
|
359
368
|
</TouchableOpacity>
|
|
@@ -364,7 +373,7 @@ function DefaultRegistrationScreen({
|
|
|
364
373
|
|
|
365
374
|
<View style={s.bottomRow}>
|
|
366
375
|
<TouchableOpacity
|
|
367
|
-
style={[s.primaryBtn, { backgroundColor:
|
|
376
|
+
style={[s.primaryBtn, { backgroundColor: accent, flex: 1 }]}
|
|
368
377
|
onPress={() => animateToStep(1)}
|
|
369
378
|
activeOpacity={0.8}
|
|
370
379
|
>
|
|
@@ -388,7 +397,7 @@ function DefaultRegistrationScreen({
|
|
|
388
397
|
<StepIndicator currentStep={1} />
|
|
389
398
|
|
|
390
399
|
<View style={s.avatarCenter}>
|
|
391
|
-
<View style={[s.avatarFrameSmall, { borderColor:
|
|
400
|
+
<View style={[s.avatarFrameSmall, { borderColor: accent }]}>
|
|
392
401
|
<Image source={{ uri: avatarUrl }} style={s.avatarSmall} />
|
|
393
402
|
<View style={[s.checkBadgeSm, { backgroundColor: t.success }]}>
|
|
394
403
|
<Text style={s.checkBadgeTextSm}>✓</Text>
|
|
@@ -401,7 +410,7 @@ function DefaultRegistrationScreen({
|
|
|
401
410
|
Username <Text style={{ color: t.errorText }}>*</Text>
|
|
402
411
|
</Text>
|
|
403
412
|
<TextInput
|
|
404
|
-
style={[s.input, { backgroundColor: t.surface, color: t.text, borderColor:
|
|
413
|
+
style={[s.input, { backgroundColor: t.surface, color: t.text, borderColor: accent }]}
|
|
405
414
|
placeholder="Enter username"
|
|
406
415
|
placeholderTextColor={t.textDim}
|
|
407
416
|
value={username}
|
|
@@ -431,7 +440,7 @@ function DefaultRegistrationScreen({
|
|
|
431
440
|
<Text style={[s.secondaryBtnText, { color: t.text }]}>‹ Back</Text>
|
|
432
441
|
</TouchableOpacity>
|
|
433
442
|
<TouchableOpacity
|
|
434
|
-
style={[s.primaryBtn, { backgroundColor:
|
|
443
|
+
style={[s.primaryBtn, { backgroundColor: accent, flex: 1, opacity: canContinueUsername ? 1 : 0.4 }]}
|
|
435
444
|
onPress={() => animateToStep(2)}
|
|
436
445
|
disabled={!canContinueUsername}
|
|
437
446
|
activeOpacity={0.8}
|
|
@@ -503,7 +512,7 @@ function DefaultRegistrationScreen({
|
|
|
503
512
|
<Text style={[s.secondaryBtnText, { color: t.text }]}>‹ Back</Text>
|
|
504
513
|
</TouchableOpacity>
|
|
505
514
|
<TouchableOpacity
|
|
506
|
-
style={[s.primaryBtn, { backgroundColor:
|
|
515
|
+
style={[s.primaryBtn, { backgroundColor: accent, flex: 1, opacity: registering ? 0.7 : 1 }]}
|
|
507
516
|
onPress={handleSubmit}
|
|
508
517
|
disabled={registering}
|
|
509
518
|
activeOpacity={0.8}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TouchableOpacity,
|
|
6
6
|
ActivityIndicator,
|
|
7
7
|
StyleSheet,
|
|
8
|
+
Image,
|
|
8
9
|
} from 'react-native';
|
|
9
10
|
import { useDubsTheme } from './theme';
|
|
10
11
|
|
|
@@ -17,6 +18,12 @@ export interface ConnectWalletScreenProps {
|
|
|
17
18
|
error?: string | null;
|
|
18
19
|
/** App name shown in the header. Defaults to "Dubs" */
|
|
19
20
|
appName?: string;
|
|
21
|
+
/** Override accent color (e.g. from developer UI config) */
|
|
22
|
+
accentColor?: string;
|
|
23
|
+
/** URL to app icon — renders as Image instead of the default letter circle */
|
|
24
|
+
appIcon?: string;
|
|
25
|
+
/** Override subtitle text below app name */
|
|
26
|
+
tagline?: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export function ConnectWalletScreen({
|
|
@@ -24,20 +31,33 @@ export function ConnectWalletScreen({
|
|
|
24
31
|
connecting = false,
|
|
25
32
|
error = null,
|
|
26
33
|
appName = 'Dubs',
|
|
34
|
+
accentColor,
|
|
35
|
+
appIcon,
|
|
36
|
+
tagline,
|
|
27
37
|
}: ConnectWalletScreenProps) {
|
|
28
38
|
const t = useDubsTheme();
|
|
39
|
+
const accent = accentColor || t.accent;
|
|
29
40
|
|
|
30
41
|
return (
|
|
31
42
|
<View style={[styles.container, { backgroundColor: t.background }]}>
|
|
32
43
|
<View style={styles.content}>
|
|
33
44
|
{/* Branding */}
|
|
34
45
|
<View style={styles.brandingSection}>
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
46
|
+
{appIcon ? (
|
|
47
|
+
<Image
|
|
48
|
+
source={{ uri: appIcon }}
|
|
49
|
+
style={styles.logoImage}
|
|
50
|
+
/>
|
|
51
|
+
) : (
|
|
52
|
+
<View style={[styles.logoCircle, { backgroundColor: accent }]}>
|
|
53
|
+
<Text style={styles.logoText}>
|
|
54
|
+
{appName.charAt(0).toUpperCase()}
|
|
55
|
+
</Text>
|
|
56
|
+
</View>
|
|
57
|
+
)}
|
|
38
58
|
<Text style={[styles.appName, { color: t.text }]}>{appName}</Text>
|
|
39
59
|
<Text style={[styles.subtitle, { color: t.textMuted }]}>
|
|
40
|
-
Connect your Solana wallet to get started
|
|
60
|
+
{tagline || 'Connect your Solana wallet to get started'}
|
|
41
61
|
</Text>
|
|
42
62
|
</View>
|
|
43
63
|
|
|
@@ -55,7 +75,7 @@ export function ConnectWalletScreen({
|
|
|
55
75
|
) : null}
|
|
56
76
|
|
|
57
77
|
<TouchableOpacity
|
|
58
|
-
style={[styles.connectButton, { backgroundColor:
|
|
78
|
+
style={[styles.connectButton, { backgroundColor: accent }]}
|
|
59
79
|
onPress={onConnect}
|
|
60
80
|
disabled={connecting}
|
|
61
81
|
activeOpacity={0.8}
|
|
@@ -100,6 +120,12 @@ const styles = StyleSheet.create({
|
|
|
100
120
|
alignItems: 'center',
|
|
101
121
|
marginBottom: 8,
|
|
102
122
|
},
|
|
123
|
+
logoImage: {
|
|
124
|
+
width: 80,
|
|
125
|
+
height: 80,
|
|
126
|
+
borderRadius: 16,
|
|
127
|
+
marginBottom: 8,
|
|
128
|
+
},
|
|
103
129
|
logoText: {
|
|
104
130
|
fontSize: 36,
|
|
105
131
|
fontWeight: '800',
|