@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/dist/index.js +136 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +136 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/hooks/useAuth.ts +10 -4
- package/src/ui/AuthGate.tsx +37 -14
- package/src/ui/UserProfileSheet.tsx +80 -33
package/package.json
CHANGED
package/src/hooks/useAuth.ts
CHANGED
|
@@ -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
|
}
|
package/src/ui/AuthGate.tsx
CHANGED
|
@@ -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
|
-
|
|
387
|
-
{
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
}, [
|
|
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,
|