@dubsdotapp/expo 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dubsdotapp/expo",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -8,6 +8,7 @@
8
8
  "exports": {
9
9
  ".": {
10
10
  "types": "./dist/index.d.ts",
11
+ "react-native": "./dist/index.js",
11
12
  "import": "./dist/index.mjs",
12
13
  "require": "./dist/index.js"
13
14
  }
package/src/client.ts CHANGED
@@ -30,6 +30,7 @@ import type {
30
30
  DubsAppUser,
31
31
  CheckUsernameResult,
32
32
  LiveScore,
33
+ UiConfig,
33
34
  } from './types';
34
35
 
35
36
  export interface DubsClientConfig {
@@ -372,4 +373,12 @@ export class DubsClient {
372
373
  getErrorCodesLocal(): Record<number, SolanaErrorCode> {
373
374
  return { ...SOLANA_PROGRAM_ERRORS };
374
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
+ }
375
384
  }
package/src/index.ts CHANGED
@@ -54,6 +54,7 @@ export type {
54
54
  AuthStatus,
55
55
  LiveScore,
56
56
  LiveScoreCompetitor,
57
+ UiConfig,
57
58
  } from './types';
58
59
 
59
60
  // Provider
@@ -84,7 +85,7 @@ export type {
84
85
  } from './hooks';
85
86
 
86
87
  // UI
87
- export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsTheme } from './ui';
88
+ export { AuthGate, ConnectWalletScreen, UserProfileCard, SettingsSheet, useDubsTheme, mergeTheme } from './ui';
88
89
  export type { AuthGateProps, RegistrationScreenProps, ConnectWalletScreenProps, UserProfileCardProps, SettingsSheetProps, DubsTheme } from './ui';
89
90
 
90
91
  // Game widgets
@@ -22,6 +22,10 @@ interface ManagedWalletProviderProps {
22
22
  cluster: string;
23
23
  storage: TokenStorage;
24
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;
25
29
  children: (adapter: MwaWalletAdapter) => React.ReactNode;
26
30
  }
27
31
 
@@ -32,6 +36,9 @@ export function ManagedWalletProvider({
32
36
  cluster,
33
37
  storage,
34
38
  renderConnectScreen,
39
+ accentColor,
40
+ appIcon,
41
+ tagline,
35
42
  children,
36
43
  }: ManagedWalletProviderProps) {
37
44
  const [connected, setConnected] = useState(false);
@@ -132,6 +139,9 @@ export function ManagedWalletProvider({
132
139
  connecting,
133
140
  error,
134
141
  appName,
142
+ accentColor,
143
+ appIcon,
144
+ tagline,
135
145
  };
136
146
  if (renderConnectScreen) {
137
147
  return <>{renderConnectScreen(connectProps)}</>;
package/src/provider.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo, useCallback } 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
4
  import { NETWORK_CONFIG } from './constants';
@@ -10,7 +10,7 @@ import { ManagedWalletProvider, useDisconnect } from './managed-wallet';
10
10
  import { AuthGate } from './ui/AuthGate';
11
11
  import type { RegistrationScreenProps } from './ui/AuthGate';
12
12
  import type { ConnectWalletScreenProps } from './ui/ConnectWalletScreen';
13
- import type { AuthStatus } from './types';
13
+ import type { AuthStatus, UiConfig } from './types';
14
14
 
15
15
  // ── Context ──
16
16
 
@@ -80,6 +80,12 @@ export function DubsProvider({
80
80
  const connection = useMemo(() => new Connection(rpcUrl, { commitment: 'confirmed' }), [rpcUrl]);
81
81
  const storage = useMemo(() => tokenStorage || createSecureStoreStorage(), [tokenStorage]);
82
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
+
83
89
  // ── Path A: External wallet provided (BYOA) ──
84
90
  if (externalWallet) {
85
91
  return (
@@ -87,13 +93,14 @@ export function DubsProvider({
87
93
  client={client}
88
94
  connection={connection}
89
95
  wallet={externalWallet}
90
- appName={appName}
96
+ appName={uiConfig.appName || appName}
91
97
  network={network}
92
98
  storage={storage}
93
99
  managed={managed}
94
100
  renderLoading={renderLoading}
95
101
  renderError={renderError}
96
102
  renderRegistration={renderRegistration}
103
+ accentColor={uiConfig.accentColor}
97
104
  >
98
105
  {children}
99
106
  </ExternalWalletProvider>
@@ -103,22 +110,26 @@ export function DubsProvider({
103
110
  // ── Path B: Managed MWA wallet ──
104
111
  return (
105
112
  <ManagedWalletProvider
106
- appName={appName}
113
+ appName={uiConfig.appName || appName}
107
114
  cluster={cluster}
108
115
  storage={storage}
109
116
  renderConnectScreen={renderConnectScreen}
117
+ accentColor={uiConfig.accentColor}
118
+ appIcon={uiConfig.appIcon}
119
+ tagline={uiConfig.tagline}
110
120
  >
111
121
  {(adapter) => (
112
122
  <ManagedInner
113
123
  client={client}
114
124
  connection={connection}
115
125
  wallet={adapter}
116
- appName={appName}
126
+ appName={uiConfig.appName || appName}
117
127
  network={network}
118
128
  storage={storage}
119
129
  renderLoading={renderLoading}
120
130
  renderError={renderError}
121
131
  renderRegistration={renderRegistration}
132
+ accentColor={uiConfig.accentColor}
122
133
  >
123
134
  {children}
124
135
  </ManagedInner>
@@ -139,6 +150,7 @@ function ManagedInner({
139
150
  renderLoading,
140
151
  renderError,
141
152
  renderRegistration,
153
+ accentColor,
142
154
  children,
143
155
  }: {
144
156
  client: DubsClient;
@@ -150,6 +162,7 @@ function ManagedInner({
150
162
  renderLoading?: (status: AuthStatus) => React.ReactNode;
151
163
  renderError?: (error: Error, retry: () => void) => React.ReactNode;
152
164
  renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
165
+ accentColor?: string;
153
166
  children: React.ReactNode;
154
167
  }) {
155
168
  const managedDisconnect = useDisconnect();
@@ -178,6 +191,7 @@ function ManagedInner({
178
191
  renderError={renderError}
179
192
  renderRegistration={renderRegistration}
180
193
  appName={appName}
194
+ accentColor={accentColor}
181
195
  >
182
196
  {children}
183
197
  </AuthGate>
@@ -198,6 +212,7 @@ function ExternalWalletProvider({
198
212
  renderLoading,
199
213
  renderError,
200
214
  renderRegistration,
215
+ accentColor,
201
216
  children,
202
217
  }: {
203
218
  client: DubsClient;
@@ -210,6 +225,7 @@ function ExternalWalletProvider({
210
225
  renderLoading?: (status: AuthStatus) => React.ReactNode;
211
226
  renderError?: (error: Error, retry: () => void) => React.ReactNode;
212
227
  renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
228
+ accentColor?: string;
213
229
  children: React.ReactNode;
214
230
  }) {
215
231
  const disconnect = useCallback(async () => {
@@ -240,6 +256,7 @@ function ExternalWalletProvider({
240
256
  renderError={renderError}
241
257
  renderRegistration={renderRegistration}
242
258
  appName={appName}
259
+ accentColor={accentColor}
243
260
  >
244
261
  {children}
245
262
  </AuthGate>
package/src/types.ts CHANGED
@@ -373,3 +373,12 @@ export interface LiveScore {
373
373
  statusDetail: string;
374
374
  };
375
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
+ }
@@ -56,6 +56,8 @@ export interface AuthGateProps {
56
56
  renderError?: (error: Error, retry: () => void) => React.ReactNode;
57
57
  renderRegistration?: (props: RegistrationScreenProps) => React.ReactNode;
58
58
  appName?: string;
59
+ /** Override accent color for registration screens (from developer UI config) */
60
+ accentColor?: string;
59
61
  }
60
62
 
61
63
  // ── AuthGate Component ──
@@ -68,6 +70,7 @@ export function AuthGate({
68
70
  renderError,
69
71
  renderRegistration,
70
72
  appName = 'Dubs',
73
+ accentColor,
71
74
  }: AuthGateProps) {
72
75
  const { client } = useDubs();
73
76
  const auth = useAuth();
@@ -143,6 +146,7 @@ export function AuthGate({
143
146
  error={regError}
144
147
  client={client}
145
148
  appName={appName}
149
+ accentColor={accentColor}
146
150
  />
147
151
  );
148
152
  }
@@ -256,8 +260,10 @@ function DefaultRegistrationScreen({
256
260
  error,
257
261
  client,
258
262
  appName,
259
- }: RegistrationScreenProps & { appName: string }) {
263
+ accentColor,
264
+ }: RegistrationScreenProps & { appName: string; accentColor?: string }) {
260
265
  const t = useDubsTheme();
266
+ const accent = accentColor || t.accent;
261
267
 
262
268
  // ── Shared state ──
263
269
  const [step, setStep] = useState(0);
@@ -325,7 +331,7 @@ function DefaultRegistrationScreen({
325
331
  <StepIndicator currentStep={0} />
326
332
 
327
333
  <View style={s.avatarCenter}>
328
- <View style={[s.avatarFrame, { borderColor: t.accent }]}>
334
+ <View style={[s.avatarFrame, { borderColor: accent }]}>
329
335
  <Image source={{ uri: avatarUrl }} style={s.avatarLarge} />
330
336
  <View style={[s.checkBadge, { backgroundColor: t.success }]}>
331
337
  <Text style={s.checkBadgeText}>&#10003;</Text>
@@ -342,11 +348,11 @@ function DefaultRegistrationScreen({
342
348
  <Text style={[s.outlineBtnText, { color: t.text }]}>&#8635; Shuffle</Text>
343
349
  </TouchableOpacity>
344
350
  <TouchableOpacity
345
- style={[s.outlineBtn, { borderColor: t.accent, backgroundColor: t.accent + '15' }]}
351
+ style={[s.outlineBtn, { borderColor: accent, backgroundColor: accent + '15' }]}
346
352
  onPress={() => setShowStyles(!showStyles)}
347
353
  activeOpacity={0.7}
348
354
  >
349
- <Text style={[s.outlineBtnText, { color: t.accent }]}>&#9786; Customize</Text>
355
+ <Text style={[s.outlineBtnText, { color: accent }]}>&#9786; Customize</Text>
350
356
  </TouchableOpacity>
351
357
  </View>
352
358
 
@@ -356,7 +362,7 @@ function DefaultRegistrationScreen({
356
362
  <TouchableOpacity
357
363
  key={st}
358
364
  onPress={() => setAvatarStyle(st)}
359
- style={[s.styleThumbWrap, { borderColor: st === avatarStyle ? t.accent : t.border }]}
365
+ style={[s.styleThumbWrap, { borderColor: st === avatarStyle ? accent : t.border }]}
360
366
  >
361
367
  <Image source={{ uri: getAvatarUrl(st, avatarSeed, 80) }} style={s.styleThumb} />
362
368
  </TouchableOpacity>
@@ -367,7 +373,7 @@ function DefaultRegistrationScreen({
367
373
 
368
374
  <View style={s.bottomRow}>
369
375
  <TouchableOpacity
370
- style={[s.primaryBtn, { backgroundColor: t.accent, flex: 1 }]}
376
+ style={[s.primaryBtn, { backgroundColor: accent, flex: 1 }]}
371
377
  onPress={() => animateToStep(1)}
372
378
  activeOpacity={0.8}
373
379
  >
@@ -391,7 +397,7 @@ function DefaultRegistrationScreen({
391
397
  <StepIndicator currentStep={1} />
392
398
 
393
399
  <View style={s.avatarCenter}>
394
- <View style={[s.avatarFrameSmall, { borderColor: t.accent }]}>
400
+ <View style={[s.avatarFrameSmall, { borderColor: accent }]}>
395
401
  <Image source={{ uri: avatarUrl }} style={s.avatarSmall} />
396
402
  <View style={[s.checkBadgeSm, { backgroundColor: t.success }]}>
397
403
  <Text style={s.checkBadgeTextSm}>&#10003;</Text>
@@ -404,7 +410,7 @@ function DefaultRegistrationScreen({
404
410
  Username <Text style={{ color: t.errorText }}>*</Text>
405
411
  </Text>
406
412
  <TextInput
407
- style={[s.input, { backgroundColor: t.surface, color: t.text, borderColor: t.accent }]}
413
+ style={[s.input, { backgroundColor: t.surface, color: t.text, borderColor: accent }]}
408
414
  placeholder="Enter username"
409
415
  placeholderTextColor={t.textDim}
410
416
  value={username}
@@ -434,7 +440,7 @@ function DefaultRegistrationScreen({
434
440
  <Text style={[s.secondaryBtnText, { color: t.text }]}>&#8249; Back</Text>
435
441
  </TouchableOpacity>
436
442
  <TouchableOpacity
437
- 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 }]}
438
444
  onPress={() => animateToStep(2)}
439
445
  disabled={!canContinueUsername}
440
446
  activeOpacity={0.8}
@@ -506,7 +512,7 @@ function DefaultRegistrationScreen({
506
512
  <Text style={[s.secondaryBtnText, { color: t.text }]}>&#8249; Back</Text>
507
513
  </TouchableOpacity>
508
514
  <TouchableOpacity
509
- 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 }]}
510
516
  onPress={handleSubmit}
511
517
  disabled={registering}
512
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',
package/src/ui/index.ts CHANGED
@@ -6,7 +6,7 @@ export { UserProfileCard } from './UserProfileCard';
6
6
  export type { UserProfileCardProps } from './UserProfileCard';
7
7
  export { SettingsSheet } from './SettingsSheet';
8
8
  export type { SettingsSheetProps } from './SettingsSheet';
9
- export { useDubsTheme } from './theme';
9
+ export { useDubsTheme, mergeTheme } from './theme';
10
10
  export type { DubsTheme } from './theme';
11
11
 
12
12
  // Game widgets
package/src/ui/theme.ts CHANGED
@@ -55,3 +55,8 @@ export function useDubsTheme(): DubsTheme {
55
55
  const scheme = useColorScheme();
56
56
  return scheme === 'light' ? light : dark;
57
57
  }
58
+
59
+ /** Merge overrides into a base theme (e.g. custom accent color from developer config) */
60
+ export function mergeTheme(base: DubsTheme, overrides: Partial<DubsTheme>): DubsTheme {
61
+ return { ...base, ...overrides };
62
+ }