@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/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 { DEFAULT_RPC_URL } from './constants';
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
- export function DubsProvider({ apiKey, baseUrl, rpcUrl, wallet, children }: DubsProviderProps) {
24
- const value = useMemo<DubsContextValue>(() => {
25
- const client = new DubsClient({ apiKey, baseUrl });
26
- const connection = new Connection(rpcUrl || DEFAULT_RPC_URL, { commitment: 'confirmed' });
27
- return { client, wallet, connection };
28
- }, [apiKey, baseUrl, rpcUrl, wallet]);
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
- return <DubsContext.Provider value={value}>{children}</DubsContext.Provider>;
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
- homePlayers: string[];
173
- awayPlayers: string[];
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
+ }
@@ -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') return <>{children}</>;
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
- }: RegistrationScreenProps & { appName: string }) {
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: t.accent }]}>
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}>&#10003;</Text>
@@ -339,11 +348,11 @@ function DefaultRegistrationScreen({
339
348
  <Text style={[s.outlineBtnText, { color: t.text }]}>&#8635; Shuffle</Text>
340
349
  </TouchableOpacity>
341
350
  <TouchableOpacity
342
- style={[s.outlineBtn, { borderColor: t.accent, backgroundColor: t.accent + '15' }]}
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: t.accent }]}>&#9786; Customize</Text>
355
+ <Text style={[s.outlineBtnText, { color: accent }]}>&#9786; 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 ? t.accent : t.border }]}
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: t.accent, flex: 1 }]}
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: t.accent }]}>
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}>&#10003;</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: t.accent }]}
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 }]}>&#8249; Back</Text>
432
441
  </TouchableOpacity>
433
442
  <TouchableOpacity
434
- style={[s.primaryBtn, { backgroundColor: t.accent, flex: 1, opacity: canContinueUsername ? 1 : 0.4 }]}
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 }]}>&#8249; Back</Text>
504
513
  </TouchableOpacity>
505
514
  <TouchableOpacity
506
- style={[s.primaryBtn, { backgroundColor: t.accent, flex: 1, opacity: registering ? 0.7 : 1 }]}
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
- <View style={[styles.logoCircle, { backgroundColor: t.accent }]}>
36
- <Text style={styles.logoText}>D</Text>
37
- </View>
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: t.accent }]}
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',