@dubsdotapp/expo 0.2.75 → 0.2.77

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.75",
3
+ "version": "0.2.77",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -5,6 +5,7 @@ import { AuthContext } from '../auth-context';
5
5
  import { useDisconnect } from '../managed-wallet';
6
6
  import { getDeviceInfo } from '../utils/device';
7
7
  import type { AuthStatus, DubsUser } from '../types';
8
+ import { ensurePngAvatar } from '../utils/avatarUrl';
8
9
 
9
10
  export interface UseAuthResult {
10
11
  /** Current auth status */
@@ -66,6 +67,11 @@ export function useAuth(): UseAuthResult {
66
67
  deviceInfo?: import('../utils/device').DeviceInfo;
67
68
  } | null>(null);
68
69
 
70
+ const normalizeUser = (u: DubsUser): DubsUser => ({
71
+ ...u,
72
+ avatar: ensurePngAvatar(u.avatar) ?? u.avatar,
73
+ });
74
+
69
75
  const reset = useCallback(() => {
70
76
  setStatus('idle');
71
77
  setUser(null);
@@ -115,7 +121,7 @@ export function useAuth(): UseAuthResult {
115
121
  }
116
122
 
117
123
  // Existing user — fully authenticated
118
- setUser(result.user!);
124
+ setUser(normalizeUser(result.user!));
119
125
  setToken(result.token!);
120
126
  setStatus('authenticated');
121
127
  } catch (err) {
@@ -158,7 +164,7 @@ export function useAuth(): UseAuthResult {
158
164
  const user = avatarUrl && !result.user.avatar
159
165
  ? { ...result.user, avatar: avatarUrl }
160
166
  : result.user;
161
- setUser(user);
167
+ setUser(normalizeUser(user));
162
168
  setToken(result.token);
163
169
  setStatus('authenticated');
164
170
  } catch (err) {
@@ -184,7 +190,7 @@ export function useAuth(): UseAuthResult {
184
190
  try {
185
191
  client.setToken(savedToken);
186
192
  const me = await client.getMe();
187
- setUser(me);
193
+ setUser(normalizeUser(me));
188
194
  setToken(savedToken);
189
195
  setStatus('authenticated');
190
196
  return true;
@@ -200,7 +206,7 @@ export function useAuth(): UseAuthResult {
200
206
  const refreshUser = useCallback(async () => {
201
207
  try {
202
208
  const me = await client.getMe();
203
- setUser(me);
209
+ setUser(normalizeUser(me));
204
210
  } catch {
205
211
  // Silent failure — keep existing user state
206
212
  }
@@ -32,12 +32,17 @@ const DICEBEAR_STYLES = [
32
32
  'thumbs',
33
33
  ];
34
34
 
35
+ const BG_COLORS = [
36
+ '1a1a2e', 'f43f5e', 'f97316', 'eab308', '22c55e',
37
+ '3b82f6', '8b5cf6', 'ec4899', '06b6d4', '64748b',
38
+ ];
39
+
35
40
  function generateSeed(): string {
36
41
  return Math.random().toString(36).slice(2, 10);
37
42
  }
38
43
 
39
- function getAvatarUrl(style: string, seed: string, size = 256): string {
40
- return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&size=${size}`;
44
+ function getAvatarUrl(style: string, seed: string, bg = '1a1a2e', size = 256): string {
45
+ return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&backgroundColor=${bg}&size=${size}`;
41
46
  }
42
47
 
43
48
  // ── Public Types ──
@@ -295,6 +300,7 @@ function DefaultRegistrationScreen({
295
300
  const [step, setStep] = useState(0);
296
301
  const [avatarSeed, setAvatarSeed] = useState(generateSeed);
297
302
  const [avatarStyle, setAvatarStyle] = useState('adventurer');
303
+ const [avatarBg, setAvatarBg] = useState('1a1a2e');
298
304
  const [showStyles, setShowStyles] = useState(false);
299
305
  const [username, setUsername] = useState('');
300
306
  const [referralCode, setReferralCode] = useState('');
@@ -306,7 +312,7 @@ function DefaultRegistrationScreen({
306
312
  const fadeAnim = useRef(new Animated.Value(1)).current;
307
313
  const slideAnim = useRef(new Animated.Value(0)).current;
308
314
 
309
- const avatarUrl = getAvatarUrl(avatarStyle, avatarSeed);
315
+ const avatarUrl = getAvatarUrl(avatarStyle, avatarSeed, avatarBg);
310
316
 
311
317
  // Debounced username check
312
318
  useEffect(() => {
@@ -383,17 +389,33 @@ function DefaultRegistrationScreen({
383
389
  </View>
384
390
 
385
391
  {showStyles && (
386
- <ScrollView horizontal showsHorizontalScrollIndicator={false} style={s.styleScroll}>
387
- {DICEBEAR_STYLES.map((st) => (
388
- <TouchableOpacity
389
- key={st}
390
- onPress={() => setAvatarStyle(st)}
391
- style={[s.styleThumbWrap, { borderColor: st === avatarStyle ? accent : t.border }]}
392
- >
393
- <Image source={{ uri: getAvatarUrl(st, avatarSeed, 80) }} style={s.styleThumb} />
394
- </TouchableOpacity>
395
- ))}
396
- </ScrollView>
392
+ <>
393
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={s.styleScroll}>
394
+ {DICEBEAR_STYLES.map((st) => (
395
+ <TouchableOpacity
396
+ key={st}
397
+ onPress={() => setAvatarStyle(st)}
398
+ style={[s.styleThumbWrap, { borderColor: st === avatarStyle ? accent : t.border }]}
399
+ >
400
+ <Image source={{ uri: getAvatarUrl(st, avatarSeed, avatarBg, 80) }} style={s.styleThumb} />
401
+ </TouchableOpacity>
402
+ ))}
403
+ </ScrollView>
404
+ <Text style={[s.subtitle, { color: t.textMuted, marginTop: 8 }]}>Background Color</Text>
405
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={s.styleScroll}>
406
+ {BG_COLORS.map((color) => (
407
+ <TouchableOpacity
408
+ key={color}
409
+ onPress={() => setAvatarBg(color)}
410
+ style={[
411
+ s.colorSwatch,
412
+ { backgroundColor: `#${color}` },
413
+ color === avatarBg && { borderColor: accent, borderWidth: 2.5 },
414
+ ]}
415
+ />
416
+ ))}
417
+ </ScrollView>
418
+ </>
397
419
  )}
398
420
  </View>
399
421
 
@@ -755,6 +777,7 @@ const s = StyleSheet.create({
755
777
  styleScroll: { paddingHorizontal: 24, marginTop: 4 },
756
778
  styleThumbWrap: { borderWidth: 2, borderRadius: 12, padding: 3, marginRight: 10 },
757
779
  styleThumb: { width: 52, height: 52, borderRadius: 10, backgroundColor: '#E5E5EA' },
780
+ colorSwatch: { width: 32, height: 32, borderRadius: 16, borderWidth: 1.5, borderColor: 'rgba(255,255,255,0.15)', marginRight: 10 },
758
781
  // Input
759
782
  inputGroup: { paddingHorizontal: 24, gap: 6 },
760
783
  inputLabel: { fontSize: 15, fontWeight: '600' },
@@ -29,23 +29,30 @@ const DICEBEAR_STYLES = [
29
29
  'thumbs',
30
30
  ];
31
31
 
32
+ const BG_COLORS = [
33
+ '1a1a2e', 'f43f5e', 'f97316', 'eab308', '22c55e',
34
+ '3b82f6', '8b5cf6', 'ec4899', '06b6d4', '64748b',
35
+ ];
36
+
32
37
  function generateSeed(): string {
33
38
  return Math.random().toString(36).slice(2, 10);
34
39
  }
35
40
 
36
- function getAvatarUrl(style: string, seed: string, size = 256): string {
37
- return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&size=${size}`;
41
+ function getAvatarUrl(style: string, seed: string, bg = '1a1a2e', size = 256): string {
42
+ return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&backgroundColor=${bg}&size=${size}`;
38
43
  }
39
44
 
40
- /** Parse style and seed from a DiceBear URL, or return defaults. */
41
- function parseAvatarUrl(url?: string | null): { style: string; seed: string } {
42
- if (!url) return { style: 'adventurer', seed: generateSeed() };
45
+ /** Parse style, seed, and backgroundColor from a DiceBear URL, or return defaults. */
46
+ function parseAvatarUrl(url?: string | null): { style: string; seed: string; bg: string } {
47
+ if (!url) return { style: 'adventurer', seed: generateSeed(), bg: '1a1a2e' };
43
48
  try {
44
- // Match both png and svg formats, any API version (7.x, 9.x, etc.)
45
49
  const match = url.match(/\/\d+\.x\/([^/]+)\/(?:png|svg)\?seed=([^&]+)/);
46
- if (match) return { style: match[1], seed: match[2] };
50
+ if (match) {
51
+ const bgMatch = url.match(/backgroundColor=([^&]+)/);
52
+ return { style: match[1], seed: match[2], bg: bgMatch?.[1] || '1a1a2e' };
53
+ }
47
54
  } catch {}
48
- return { style: 'adventurer', seed: generateSeed() };
55
+ return { style: 'adventurer', seed: generateSeed(), bg: '1a1a2e' };
49
56
  }
50
57
 
51
58
  function truncateAddress(address: string, chars = 4): string {
@@ -84,6 +91,7 @@ export function UserProfileSheet({
84
91
  const parsed = useMemo(() => parseAvatarUrl(user.avatar), [user.avatar]);
85
92
  const [avatarStyle, setAvatarStyle] = useState(parsed.style);
86
93
  const [avatarSeed, setAvatarSeed] = useState(parsed.seed);
94
+ const [bgColor, setBgColor] = useState(parsed.bg);
87
95
  const [saving, setSaving] = useState(false);
88
96
  const [error, setError] = useState<string | null>(null);
89
97
 
@@ -92,6 +100,7 @@ export function UserProfileSheet({
92
100
  const p = parseAvatarUrl(user.avatar);
93
101
  setAvatarStyle(p.style);
94
102
  setAvatarSeed(p.seed);
103
+ setBgColor(p.bg);
95
104
  }, [user.avatar]);
96
105
 
97
106
  // Animate overlay
@@ -108,30 +117,9 @@ export function UserProfileSheet({
108
117
  if (visible) setError(null);
109
118
  }, [visible]);
110
119
 
111
- const currentAvatarUrl = getAvatarUrl(avatarStyle, avatarSeed);
120
+ const currentAvatarUrl = getAvatarUrl(avatarStyle, avatarSeed, bgColor);
112
121
 
113
- const handleSelectStyle = useCallback(
114
- async (style: string) => {
115
- const newUrl = getAvatarUrl(style, avatarSeed);
116
- setAvatarStyle(style);
117
- setSaving(true);
118
- setError(null);
119
- try {
120
- await client.updateProfile({ avatar: newUrl });
121
- onAvatarUpdated?.(newUrl);
122
- } catch (err) {
123
- setError(err instanceof Error ? err.message : 'Failed to update avatar');
124
- } finally {
125
- setSaving(false);
126
- }
127
- },
128
- [avatarSeed, client, onAvatarUpdated],
129
- );
130
-
131
- const handleShuffle = useCallback(async () => {
132
- const newSeed = generateSeed();
133
- const newUrl = getAvatarUrl(avatarStyle, newSeed);
134
- setAvatarSeed(newSeed);
122
+ const saveAvatar = useCallback(async (newUrl: string) => {
135
123
  setSaving(true);
136
124
  setError(null);
137
125
  try {
@@ -142,7 +130,29 @@ export function UserProfileSheet({
142
130
  } finally {
143
131
  setSaving(false);
144
132
  }
145
- }, [avatarStyle, client, onAvatarUpdated]);
133
+ }, [client, onAvatarUpdated]);
134
+
135
+ const handleSelectStyle = useCallback(
136
+ (style: string) => {
137
+ setAvatarStyle(style);
138
+ saveAvatar(getAvatarUrl(style, avatarSeed, bgColor));
139
+ },
140
+ [avatarSeed, bgColor, saveAvatar],
141
+ );
142
+
143
+ const handleShuffle = useCallback(() => {
144
+ const newSeed = generateSeed();
145
+ setAvatarSeed(newSeed);
146
+ saveAvatar(getAvatarUrl(avatarStyle, newSeed, bgColor));
147
+ }, [avatarStyle, bgColor, saveAvatar]);
148
+
149
+ const handleSelectBgColor = useCallback(
150
+ (color: string) => {
151
+ setBgColor(color);
152
+ saveAvatar(getAvatarUrl(avatarStyle, avatarSeed, color));
153
+ },
154
+ [avatarStyle, avatarSeed, saveAvatar],
155
+ );
146
156
 
147
157
  return (
148
158
  <Modal
@@ -238,13 +248,40 @@ export function UserProfileSheet({
238
248
  ]}
239
249
  >
240
250
  <Image
241
- source={{ uri: getAvatarUrl(style, avatarSeed, 80) }}
251
+ source={{ uri: getAvatarUrl(style, avatarSeed, bgColor, 80) }}
242
252
  style={styles.styleTileImage}
243
253
  />
244
254
  </TouchableOpacity>
245
255
  );
246
256
  })}
247
257
  </ScrollView>
258
+
259
+ {/* Background color picker */}
260
+ <Text style={[styles.sectionLabel, { color: t.textSecondary, marginTop: 4 }]}>
261
+ Background Color
262
+ </Text>
263
+ <ScrollView
264
+ horizontal
265
+ showsHorizontalScrollIndicator={false}
266
+ contentContainerStyle={styles.colorPickerContent}
267
+ >
268
+ {BG_COLORS.map((color) => {
269
+ const isSelected = color === bgColor;
270
+ return (
271
+ <TouchableOpacity
272
+ key={color}
273
+ onPress={() => handleSelectBgColor(color)}
274
+ activeOpacity={0.7}
275
+ disabled={saving}
276
+ style={[
277
+ styles.colorSwatch,
278
+ { backgroundColor: `#${color}` },
279
+ isSelected && { borderColor: t.accent, borderWidth: 2.5 },
280
+ ]}
281
+ />
282
+ );
283
+ })}
284
+ </ScrollView>
248
285
  </View>
249
286
 
250
287
  {/* Error */}
@@ -423,6 +460,16 @@ const styles = StyleSheet.create({
423
460
  stylePickerContent: {
424
461
  gap: 10,
425
462
  },
463
+ colorPickerContent: {
464
+ gap: 8,
465
+ },
466
+ colorSwatch: {
467
+ width: 32,
468
+ height: 32,
469
+ borderRadius: 16,
470
+ borderWidth: 1.5,
471
+ borderColor: 'rgba(255,255,255,0.15)',
472
+ },
426
473
  styleTile: {
427
474
  width: 72,
428
475
  height: 72,