@dubsdotapp/expo 0.2.76 → 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.76",
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",
@@ -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,