@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/dist/index.js +678 -612
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +660 -587
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/ui/AuthGate.tsx +14 -31
- package/src/ui/AvatarEditor.tsx +159 -0
- package/src/ui/UserProfileSheet.tsx +31 -104
package/package.json
CHANGED
package/src/ui/AuthGate.tsx
CHANGED
|
@@ -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
|
-
<
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
</
|
|
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 {
|
|
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
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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,
|