@dubsdotapp/expo 0.2.76 → 0.2.78

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.78",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -18,28 +18,10 @@ import { usePushNotifications } from '../hooks/usePushNotifications';
18
18
  import { useDubs } from '../provider';
19
19
  import { AuthContext } from '../auth-context';
20
20
  import { useDubsTheme } from './theme';
21
+ import { AvatarEditor, generateSeed, getAvatarUrl } from './AvatarEditor';
21
22
  import type { AuthStatus } from '../types';
22
23
  import type { DubsClient } from '../client';
23
24
 
24
- // ── Avatar Helpers ──
25
-
26
- const DICEBEAR_STYLES = [
27
- 'adventurer',
28
- 'avataaars',
29
- 'fun-emoji',
30
- 'bottts',
31
- 'big-smile',
32
- 'thumbs',
33
- ];
34
-
35
- function generateSeed(): string {
36
- return Math.random().toString(36).slice(2, 10);
37
- }
38
-
39
- function getAvatarUrl(style: string, seed: string, size = 256): string {
40
- return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&size=${size}`;
41
- }
42
-
43
25
  // ── Public Types ──
44
26
 
45
27
  export interface RegistrationScreenProps {
@@ -295,6 +277,7 @@ function DefaultRegistrationScreen({
295
277
  const [step, setStep] = useState(0);
296
278
  const [avatarSeed, setAvatarSeed] = useState(generateSeed);
297
279
  const [avatarStyle, setAvatarStyle] = useState('adventurer');
280
+ const [avatarBg, setAvatarBg] = useState('1a1a2e');
298
281
  const [showStyles, setShowStyles] = useState(false);
299
282
  const [username, setUsername] = useState('');
300
283
  const [referralCode, setReferralCode] = useState('');
@@ -306,7 +289,7 @@ function DefaultRegistrationScreen({
306
289
  const fadeAnim = useRef(new Animated.Value(1)).current;
307
290
  const slideAnim = useRef(new Animated.Value(0)).current;
308
291
 
309
- const avatarUrl = getAvatarUrl(avatarStyle, avatarSeed);
292
+ const avatarUrl = getAvatarUrl(avatarStyle, avatarSeed, avatarBg);
310
293
 
311
294
  // Debounced username check
312
295
  useEffect(() => {
@@ -383,17 +366,17 @@ function DefaultRegistrationScreen({
383
366
  </View>
384
367
 
385
368
  {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>
369
+ <View style={s.styleScroll}>
370
+ <AvatarEditor
371
+ style={avatarStyle}
372
+ seed={avatarSeed}
373
+ bg={avatarBg}
374
+ onStyleChange={setAvatarStyle}
375
+ onSeedChange={setAvatarSeed}
376
+ onBgChange={setAvatarBg}
377
+ accentColor={accent}
378
+ />
379
+ </View>
397
380
  )}
398
381
  </View>
399
382
 
@@ -0,0 +1,159 @@
1
+ import React, { useState, useCallback } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Image,
7
+ ScrollView,
8
+ StyleSheet,
9
+ } from 'react-native';
10
+ import { useDubsTheme } from './theme';
11
+
12
+ const DICEBEAR_STYLES = [
13
+ 'adventurer',
14
+ 'avataaars',
15
+ 'fun-emoji',
16
+ 'bottts',
17
+ 'big-smile',
18
+ 'thumbs',
19
+ ];
20
+
21
+ const BG_COLORS = [
22
+ '1a1a2e', 'f43f5e', 'f97316', 'eab308', '22c55e',
23
+ '3b82f6', '8b5cf6', 'ec4899', '06b6d4', '64748b',
24
+ ];
25
+
26
+ export function generateSeed(): string {
27
+ return Math.random().toString(36).slice(2, 10);
28
+ }
29
+
30
+ export function getAvatarUrl(style: string, seed: string, bg = '1a1a2e', size = 256): string {
31
+ return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&backgroundColor=${bg}&size=${size}`;
32
+ }
33
+
34
+ export function parseAvatarUrl(url?: string | null): { style: string; seed: string; bg: string } {
35
+ if (!url) return { style: 'adventurer', seed: generateSeed(), bg: '1a1a2e' };
36
+ try {
37
+ const match = url.match(/\/\d+\.x\/([^/]+)\/(?:png|svg)\?seed=([^&]+)/);
38
+ if (match) {
39
+ const bgMatch = url.match(/backgroundColor=([^&]+)/);
40
+ return { style: match[1], seed: match[2], bg: bgMatch?.[1] || '1a1a2e' };
41
+ }
42
+ } catch {}
43
+ return { style: 'adventurer', seed: generateSeed(), bg: '1a1a2e' };
44
+ }
45
+
46
+ export interface AvatarEditorProps {
47
+ style: string;
48
+ seed: string;
49
+ bg: string;
50
+ onStyleChange: (style: string) => void;
51
+ onSeedChange: (seed: string) => void;
52
+ onBgChange: (bg: string) => void;
53
+ disabled?: boolean;
54
+ accentColor?: string;
55
+ }
56
+
57
+ export function AvatarEditor({
58
+ style: avatarStyle,
59
+ seed: avatarSeed,
60
+ bg: bgColor,
61
+ onStyleChange,
62
+ onSeedChange,
63
+ onBgChange,
64
+ disabled = false,
65
+ accentColor,
66
+ }: AvatarEditorProps) {
67
+ const t = useDubsTheme();
68
+ const accent = accentColor || t.accent;
69
+
70
+ return (
71
+ <View style={styles.container}>
72
+ {/* Style picker */}
73
+ <ScrollView
74
+ horizontal
75
+ showsHorizontalScrollIndicator={false}
76
+ contentContainerStyle={styles.row}
77
+ >
78
+ {DICEBEAR_STYLES.map((st) => {
79
+ const isSelected = st === avatarStyle;
80
+ return (
81
+ <TouchableOpacity
82
+ key={st}
83
+ onPress={() => onStyleChange(st)}
84
+ activeOpacity={0.7}
85
+ disabled={disabled}
86
+ style={[
87
+ styles.styleTile,
88
+ {
89
+ borderColor: isSelected ? accent : t.border,
90
+ borderWidth: isSelected ? 2 : 1,
91
+ },
92
+ ]}
93
+ >
94
+ <Image
95
+ source={{ uri: getAvatarUrl(st, avatarSeed, bgColor, 80) }}
96
+ style={styles.styleTileImage}
97
+ />
98
+ </TouchableOpacity>
99
+ );
100
+ })}
101
+ </ScrollView>
102
+
103
+ {/* Background color picker */}
104
+ <Text style={[styles.label, { color: t.textSecondary }]}>Background Color</Text>
105
+ <ScrollView
106
+ horizontal
107
+ showsHorizontalScrollIndicator={false}
108
+ contentContainerStyle={styles.row}
109
+ >
110
+ {BG_COLORS.map((color) => {
111
+ const isSelected = color === bgColor;
112
+ return (
113
+ <TouchableOpacity
114
+ key={color}
115
+ onPress={() => onBgChange(color)}
116
+ activeOpacity={0.7}
117
+ disabled={disabled}
118
+ style={[
119
+ styles.colorSwatch,
120
+ { backgroundColor: `#${color}` },
121
+ isSelected && { borderColor: accent, borderWidth: 2.5 },
122
+ ]}
123
+ />
124
+ );
125
+ })}
126
+ </ScrollView>
127
+ </View>
128
+ );
129
+ }
130
+
131
+ const styles = StyleSheet.create({
132
+ container: {
133
+ gap: 10,
134
+ },
135
+ row: {
136
+ gap: 10,
137
+ },
138
+ label: {
139
+ fontSize: 14,
140
+ fontWeight: '600',
141
+ },
142
+ styleTile: {
143
+ width: 72,
144
+ height: 72,
145
+ borderRadius: 16,
146
+ overflow: 'hidden',
147
+ },
148
+ styleTileImage: {
149
+ width: '100%',
150
+ height: '100%',
151
+ },
152
+ colorSwatch: {
153
+ width: 32,
154
+ height: 32,
155
+ borderRadius: 16,
156
+ borderWidth: 1.5,
157
+ borderColor: 'rgba(255,255,255,0.15)',
158
+ },
159
+ });
@@ -16,37 +16,7 @@ import {
16
16
  import { useDubsTheme } from './theme';
17
17
  import { useDubs } from '../provider';
18
18
  import { usePushNotifications } from '../hooks/usePushNotifications';
19
- import { ensurePngAvatar } from '../utils/avatarUrl';
20
-
21
- // ── Avatar Helpers ──
22
-
23
- const DICEBEAR_STYLES = [
24
- 'adventurer',
25
- 'avataaars',
26
- 'fun-emoji',
27
- 'bottts',
28
- 'big-smile',
29
- 'thumbs',
30
- ];
31
-
32
- function generateSeed(): string {
33
- return Math.random().toString(36).slice(2, 10);
34
- }
35
-
36
- function getAvatarUrl(style: string, seed: string, size = 256): string {
37
- return `https://api.dicebear.com/9.x/${style}/png?seed=${seed}&size=${size}`;
38
- }
39
-
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() };
43
- try {
44
- // Match both png and svg formats, any API version (7.x, 9.x, etc.)
45
- const match = url.match(/\/\d+\.x\/([^/]+)\/(?:png|svg)\?seed=([^&]+)/);
46
- if (match) return { style: match[1], seed: match[2] };
47
- } catch {}
48
- return { style: 'adventurer', seed: generateSeed() };
49
- }
19
+ import { AvatarEditor, getAvatarUrl, generateSeed, parseAvatarUrl } from './AvatarEditor';
50
20
 
51
21
  function truncateAddress(address: string, chars = 4): string {
52
22
  if (address.length <= chars * 2 + 3) return address;
@@ -84,6 +54,7 @@ export function UserProfileSheet({
84
54
  const parsed = useMemo(() => parseAvatarUrl(user.avatar), [user.avatar]);
85
55
  const [avatarStyle, setAvatarStyle] = useState(parsed.style);
86
56
  const [avatarSeed, setAvatarSeed] = useState(parsed.seed);
57
+ const [bgColor, setBgColor] = useState(parsed.bg);
87
58
  const [saving, setSaving] = useState(false);
88
59
  const [error, setError] = useState<string | null>(null);
89
60
 
@@ -92,6 +63,7 @@ export function UserProfileSheet({
92
63
  const p = parseAvatarUrl(user.avatar);
93
64
  setAvatarStyle(p.style);
94
65
  setAvatarSeed(p.seed);
66
+ setBgColor(p.bg);
95
67
  }, [user.avatar]);
96
68
 
97
69
  // Animate overlay
@@ -108,30 +80,9 @@ export function UserProfileSheet({
108
80
  if (visible) setError(null);
109
81
  }, [visible]);
110
82
 
111
- const currentAvatarUrl = getAvatarUrl(avatarStyle, avatarSeed);
112
-
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
- );
83
+ const currentAvatarUrl = getAvatarUrl(avatarStyle, avatarSeed, bgColor);
130
84
 
131
- const handleShuffle = useCallback(async () => {
132
- const newSeed = generateSeed();
133
- const newUrl = getAvatarUrl(avatarStyle, newSeed);
134
- setAvatarSeed(newSeed);
85
+ const saveAvatar = useCallback(async (newUrl: string) => {
135
86
  setSaving(true);
136
87
  setError(null);
137
88
  try {
@@ -142,7 +93,23 @@ export function UserProfileSheet({
142
93
  } finally {
143
94
  setSaving(false);
144
95
  }
145
- }, [avatarStyle, client, onAvatarUpdated]);
96
+ }, [client, onAvatarUpdated]);
97
+
98
+ const handleStyleChange = useCallback((style: string) => {
99
+ setAvatarStyle(style);
100
+ saveAvatar(getAvatarUrl(style, avatarSeed, bgColor));
101
+ }, [avatarSeed, bgColor, saveAvatar]);
102
+
103
+ const handleShuffle = useCallback(() => {
104
+ const newSeed = generateSeed();
105
+ setAvatarSeed(newSeed);
106
+ saveAvatar(getAvatarUrl(avatarStyle, newSeed, bgColor));
107
+ }, [avatarStyle, bgColor, saveAvatar]);
108
+
109
+ const handleBgChange = useCallback((color: string) => {
110
+ setBgColor(color);
111
+ saveAvatar(getAvatarUrl(avatarStyle, avatarSeed, color));
112
+ }, [avatarStyle, avatarSeed, saveAvatar]);
146
113
 
147
114
  return (
148
115
  <Modal
@@ -216,35 +183,15 @@ export function UserProfileSheet({
216
183
  <Text style={[styles.shuffleText, { color: t.accent }]}>Shuffle</Text>
217
184
  </TouchableOpacity>
218
185
  </View>
219
- <ScrollView
220
- horizontal
221
- showsHorizontalScrollIndicator={false}
222
- contentContainerStyle={styles.stylePickerContent}
223
- >
224
- {DICEBEAR_STYLES.map((style) => {
225
- const isSelected = style === avatarStyle;
226
- return (
227
- <TouchableOpacity
228
- key={style}
229
- onPress={() => handleSelectStyle(style)}
230
- activeOpacity={0.7}
231
- disabled={saving}
232
- style={[
233
- styles.styleTile,
234
- {
235
- borderColor: isSelected ? t.accent : t.border,
236
- borderWidth: isSelected ? 2 : 1,
237
- },
238
- ]}
239
- >
240
- <Image
241
- source={{ uri: getAvatarUrl(style, avatarSeed, 80) }}
242
- style={styles.styleTileImage}
243
- />
244
- </TouchableOpacity>
245
- );
246
- })}
247
- </ScrollView>
186
+ <AvatarEditor
187
+ style={avatarStyle}
188
+ seed={avatarSeed}
189
+ bg={bgColor}
190
+ onStyleChange={handleStyleChange}
191
+ onSeedChange={setAvatarSeed}
192
+ onBgChange={handleBgChange}
193
+ disabled={saving}
194
+ />
248
195
  </View>
249
196
 
250
197
  {/* Error */}
@@ -363,7 +310,6 @@ const styles = StyleSheet.create({
363
310
  scrollContentInner: {
364
311
  paddingBottom: 8,
365
312
  },
366
- // Avatar
367
313
  avatarSection: {
368
314
  alignItems: 'center',
369
315
  paddingTop: 8,
@@ -380,7 +326,6 @@ const styles = StyleSheet.create({
380
326
  avatar: {
381
327
  width: '100%',
382
328
  height: '100%',
383
- backgroundColor: '#1a1a2e',
384
329
  },
385
330
  avatarLoading: {
386
331
  ...StyleSheet.absoluteFillObject,
@@ -396,7 +341,6 @@ const styles = StyleSheet.create({
396
341
  fontSize: 13,
397
342
  fontFamily: 'monospace',
398
343
  },
399
- // Change Avatar
400
344
  section: {
401
345
  marginBottom: 20,
402
346
  gap: 12,
@@ -420,21 +364,6 @@ const styles = StyleSheet.create({
420
364
  fontSize: 13,
421
365
  fontWeight: '600',
422
366
  },
423
- stylePickerContent: {
424
- gap: 10,
425
- },
426
- styleTile: {
427
- width: 72,
428
- height: 72,
429
- borderRadius: 16,
430
- overflow: 'hidden',
431
- },
432
- styleTileImage: {
433
- width: '100%',
434
- height: '100%',
435
- backgroundColor: '#1a1a2e',
436
- },
437
- // Error
438
367
  errorBox: {
439
368
  marginBottom: 16,
440
369
  borderRadius: 12,
@@ -445,7 +374,6 @@ const styles = StyleSheet.create({
445
374
  fontSize: 13,
446
375
  fontWeight: '500',
447
376
  },
448
- // Push Notifications
449
377
  notifRow: {
450
378
  flexDirection: 'row',
451
379
  alignItems: 'center',
@@ -476,7 +404,6 @@ const styles = StyleSheet.create({
476
404
  fontSize: 14,
477
405
  fontWeight: '700',
478
406
  },
479
- // Disconnect
480
407
  disconnectButton: {
481
408
  height: 52,
482
409
  borderRadius: 16,