@dubsdotapp/expo 0.2.77 → 0.2.79

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.77",
3
+ "version": "0.2.79",
4
4
  "description": "React Native SDK for the Dubs betting platform",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -18,33 +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
- const BG_COLORS = [
36
- '1a1a2e', 'f43f5e', 'f97316', 'eab308', '22c55e',
37
- '3b82f6', '8b5cf6', 'ec4899', '06b6d4', '64748b',
38
- ];
39
-
40
- function generateSeed(): string {
41
- return Math.random().toString(36).slice(2, 10);
42
- }
43
-
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}`;
46
- }
47
-
48
25
  // ── Public Types ──
49
26
 
50
27
  export interface RegistrationScreenProps {
@@ -389,33 +366,17 @@ function DefaultRegistrationScreen({
389
366
  </View>
390
367
 
391
368
  {showStyles && (
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
- </>
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>
419
380
  )}
420
381
  </View>
421
382
 
@@ -777,7 +738,6 @@ const s = StyleSheet.create({
777
738
  styleScroll: { paddingHorizontal: 24, marginTop: 4 },
778
739
  styleThumbWrap: { borderWidth: 2, borderRadius: 12, padding: 3, marginRight: 10 },
779
740
  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 },
781
741
  // Input
782
742
  inputGroup: { paddingHorizontal: 24, gap: 6 },
783
743
  inputLabel: { fontSize: 15, fontWeight: '600' },
@@ -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
+ });
@@ -15,45 +15,9 @@ import {
15
15
  } from 'react-native';
16
16
  import { useDubsTheme } from './theme';
17
17
  import { useDubs } from '../provider';
18
+ import { useAuth } from '../hooks/useAuth';
18
19
  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
- const BG_COLORS = [
33
- '1a1a2e', 'f43f5e', 'f97316', 'eab308', '22c55e',
34
- '3b82f6', '8b5cf6', 'ec4899', '06b6d4', '64748b',
35
- ];
36
-
37
- function generateSeed(): string {
38
- return Math.random().toString(36).slice(2, 10);
39
- }
40
-
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}`;
43
- }
44
-
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' };
48
- try {
49
- const match = url.match(/\/\d+\.x\/([^/]+)\/(?:png|svg)\?seed=([^&]+)/);
50
- if (match) {
51
- const bgMatch = url.match(/backgroundColor=([^&]+)/);
52
- return { style: match[1], seed: match[2], bg: bgMatch?.[1] || '1a1a2e' };
53
- }
54
- } catch {}
55
- return { style: 'adventurer', seed: generateSeed(), bg: '1a1a2e' };
56
- }
20
+ import { AvatarEditor, getAvatarUrl, generateSeed, parseAvatarUrl } from './AvatarEditor';
57
21
 
58
22
  function truncateAddress(address: string, chars = 4): string {
59
23
  if (address.length <= chars * 2 + 3) return address;
@@ -84,6 +48,7 @@ export function UserProfileSheet({
84
48
  }: UserProfileSheetProps) {
85
49
  const t = useDubsTheme();
86
50
  const { client } = useDubs();
51
+ const { refreshUser } = useAuth();
87
52
  const push = usePushNotifications();
88
53
 
89
54
  const overlayOpacity = useRef(new Animated.Value(0)).current;
@@ -124,21 +89,19 @@ export function UserProfileSheet({
124
89
  setError(null);
125
90
  try {
126
91
  await client.updateProfile({ avatar: newUrl });
92
+ await refreshUser();
127
93
  onAvatarUpdated?.(newUrl);
128
94
  } catch (err) {
129
95
  setError(err instanceof Error ? err.message : 'Failed to update avatar');
130
96
  } finally {
131
97
  setSaving(false);
132
98
  }
133
- }, [client, onAvatarUpdated]);
99
+ }, [client, refreshUser, onAvatarUpdated]);
134
100
 
135
- const handleSelectStyle = useCallback(
136
- (style: string) => {
137
- setAvatarStyle(style);
138
- saveAvatar(getAvatarUrl(style, avatarSeed, bgColor));
139
- },
140
- [avatarSeed, bgColor, saveAvatar],
141
- );
101
+ const handleStyleChange = useCallback((style: string) => {
102
+ setAvatarStyle(style);
103
+ saveAvatar(getAvatarUrl(style, avatarSeed, bgColor));
104
+ }, [avatarSeed, bgColor, saveAvatar]);
142
105
 
143
106
  const handleShuffle = useCallback(() => {
144
107
  const newSeed = generateSeed();
@@ -146,13 +109,10 @@ export function UserProfileSheet({
146
109
  saveAvatar(getAvatarUrl(avatarStyle, newSeed, bgColor));
147
110
  }, [avatarStyle, bgColor, saveAvatar]);
148
111
 
149
- const handleSelectBgColor = useCallback(
150
- (color: string) => {
151
- setBgColor(color);
152
- saveAvatar(getAvatarUrl(avatarStyle, avatarSeed, color));
153
- },
154
- [avatarStyle, avatarSeed, saveAvatar],
155
- );
112
+ const handleBgChange = useCallback((color: string) => {
113
+ setBgColor(color);
114
+ saveAvatar(getAvatarUrl(avatarStyle, avatarSeed, color));
115
+ }, [avatarStyle, avatarSeed, saveAvatar]);
156
116
 
157
117
  return (
158
118
  <Modal
@@ -226,62 +186,15 @@ export function UserProfileSheet({
226
186
  <Text style={[styles.shuffleText, { color: t.accent }]}>Shuffle</Text>
227
187
  </TouchableOpacity>
228
188
  </View>
229
- <ScrollView
230
- horizontal
231
- showsHorizontalScrollIndicator={false}
232
- contentContainerStyle={styles.stylePickerContent}
233
- >
234
- {DICEBEAR_STYLES.map((style) => {
235
- const isSelected = style === avatarStyle;
236
- return (
237
- <TouchableOpacity
238
- key={style}
239
- onPress={() => handleSelectStyle(style)}
240
- activeOpacity={0.7}
241
- disabled={saving}
242
- style={[
243
- styles.styleTile,
244
- {
245
- borderColor: isSelected ? t.accent : t.border,
246
- borderWidth: isSelected ? 2 : 1,
247
- },
248
- ]}
249
- >
250
- <Image
251
- source={{ uri: getAvatarUrl(style, avatarSeed, bgColor, 80) }}
252
- style={styles.styleTileImage}
253
- />
254
- </TouchableOpacity>
255
- );
256
- })}
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>
189
+ <AvatarEditor
190
+ style={avatarStyle}
191
+ seed={avatarSeed}
192
+ bg={bgColor}
193
+ onStyleChange={handleStyleChange}
194
+ onSeedChange={setAvatarSeed}
195
+ onBgChange={handleBgChange}
196
+ disabled={saving}
197
+ />
285
198
  </View>
286
199
 
287
200
  {/* Error */}
@@ -400,7 +313,6 @@ const styles = StyleSheet.create({
400
313
  scrollContentInner: {
401
314
  paddingBottom: 8,
402
315
  },
403
- // Avatar
404
316
  avatarSection: {
405
317
  alignItems: 'center',
406
318
  paddingTop: 8,
@@ -417,7 +329,6 @@ const styles = StyleSheet.create({
417
329
  avatar: {
418
330
  width: '100%',
419
331
  height: '100%',
420
- backgroundColor: '#1a1a2e',
421
332
  },
422
333
  avatarLoading: {
423
334
  ...StyleSheet.absoluteFillObject,
@@ -433,7 +344,6 @@ const styles = StyleSheet.create({
433
344
  fontSize: 13,
434
345
  fontFamily: 'monospace',
435
346
  },
436
- // Change Avatar
437
347
  section: {
438
348
  marginBottom: 20,
439
349
  gap: 12,
@@ -457,31 +367,6 @@ const styles = StyleSheet.create({
457
367
  fontSize: 13,
458
368
  fontWeight: '600',
459
369
  },
460
- stylePickerContent: {
461
- gap: 10,
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
- },
473
- styleTile: {
474
- width: 72,
475
- height: 72,
476
- borderRadius: 16,
477
- overflow: 'hidden',
478
- },
479
- styleTileImage: {
480
- width: '100%',
481
- height: '100%',
482
- backgroundColor: '#1a1a2e',
483
- },
484
- // Error
485
370
  errorBox: {
486
371
  marginBottom: 16,
487
372
  borderRadius: 12,
@@ -492,7 +377,6 @@ const styles = StyleSheet.create({
492
377
  fontSize: 13,
493
378
  fontWeight: '500',
494
379
  },
495
- // Push Notifications
496
380
  notifRow: {
497
381
  flexDirection: 'row',
498
382
  alignItems: 'center',
@@ -523,7 +407,6 @@ const styles = StyleSheet.create({
523
407
  fontSize: 14,
524
408
  fontWeight: '700',
525
409
  },
526
- // Disconnect
527
410
  disconnectButton: {
528
411
  height: 52,
529
412
  borderRadius: 16,