@oxyhq/services 6.9.43 → 6.9.44
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/lib/commonjs/index.js +9 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/components/ActingAsBanner.js +143 -0
- package/lib/commonjs/ui/components/ActingAsBanner.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +78 -3
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/commonjs/ui/navigation/routes.js +3 -2
- package/lib/commonjs/ui/navigation/routes.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js +21 -1
- package/lib/commonjs/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js +235 -1
- package/lib/commonjs/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js +346 -0
- package/lib/commonjs/ui/screens/CreateManagedAccountScreen.js.map +1 -0
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/components/ActingAsBanner.js +140 -0
- package/lib/module/ui/components/ActingAsBanner.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +78 -3
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/module/ui/navigation/routes.js +3 -2
- package/lib/module/ui/navigation/routes.js.map +1 -1
- package/lib/module/ui/screens/AccountCenterScreen.js +21 -1
- package/lib/module/ui/screens/AccountCenterScreen.js.map +1 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js +235 -1
- package/lib/module/ui/screens/AccountSwitcherScreen.js.map +1 -1
- package/lib/module/ui/screens/CreateManagedAccountScreen.js +342 -0
- package/lib/module/ui/screens/CreateManagedAccountScreen.js.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts +4 -0
- package/lib/typescript/commonjs/ui/components/ActingAsBanner.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts +6 -0
- package/lib/typescript/commonjs/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/commonjs/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts +5 -0
- package/lib/typescript/commonjs/ui/screens/CreateManagedAccountScreen.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ActingAsBanner.d.ts +4 -0
- package/lib/typescript/module/ui/components/ActingAsBanner.d.ts.map +1 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts +6 -0
- package/lib/typescript/module/ui/context/OxyContext.d.ts.map +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts +1 -1
- package/lib/typescript/module/ui/navigation/routes.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountCenterScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/AccountSwitcherScreen.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts +5 -0
- package/lib/typescript/module/ui/screens/CreateManagedAccountScreen.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/ui/components/ActingAsBanner.tsx +135 -0
- package/src/ui/context/OxyContext.tsx +85 -0
- package/src/ui/navigation/routes.ts +3 -1
- package/src/ui/screens/AccountCenterScreen.tsx +21 -1
- package/src/ui/screens/AccountSwitcherScreen.tsx +209 -0
- package/src/ui/screens/CreateManagedAccountScreen.tsx +338 -0
|
@@ -60,11 +60,15 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
60
60
|
refreshSessions,
|
|
61
61
|
isLoading = false,
|
|
62
62
|
isAuthenticated = false,
|
|
63
|
+
actingAs,
|
|
64
|
+
managedAccounts,
|
|
65
|
+
setActingAs,
|
|
63
66
|
} = useOxy();
|
|
64
67
|
|
|
65
68
|
const [sessionsWithUsers, setSessionsWithUsers] = useState<SessionWithUser[]>([]);
|
|
66
69
|
const [switchingToUserId, setSwitchingToUserId] = useState<string | null>(null);
|
|
67
70
|
const [removingUserId, setRemovingUserId] = useState<string | null>(null);
|
|
71
|
+
const [switchingManagedId, setSwitchingManagedId] = useState<string | null>(null);
|
|
68
72
|
|
|
69
73
|
// Device session management state
|
|
70
74
|
const [showDeviceManagement, setShowDeviceManagement] = useState(false);
|
|
@@ -210,6 +214,30 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
210
214
|
);
|
|
211
215
|
}, [logoutAll, onClose, t]);
|
|
212
216
|
|
|
217
|
+
const handleSwitchToManagedAccount = useCallback(async (accountId: string) => {
|
|
218
|
+
if (actingAs === accountId) return; // Already acting as this account
|
|
219
|
+
if (switchingManagedId) return; // Already switching
|
|
220
|
+
|
|
221
|
+
setSwitchingManagedId(accountId);
|
|
222
|
+
try {
|
|
223
|
+
setActingAs(accountId);
|
|
224
|
+
toast.success(t('accountSwitcher.toasts.switchSuccess') || 'Switched identity successfully!');
|
|
225
|
+
onClose?.();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (__DEV__) {
|
|
228
|
+
console.error('Switch managed account failed:', error);
|
|
229
|
+
}
|
|
230
|
+
toast.error('Failed to switch identity. Please try again.');
|
|
231
|
+
} finally {
|
|
232
|
+
setSwitchingManagedId(null);
|
|
233
|
+
}
|
|
234
|
+
}, [actingAs, switchingManagedId, setActingAs, t, onClose]);
|
|
235
|
+
|
|
236
|
+
const handleSwitchBackToPrimary = useCallback(() => {
|
|
237
|
+
setActingAs(null);
|
|
238
|
+
toast.success('Switched back to primary account');
|
|
239
|
+
}, [setActingAs]);
|
|
240
|
+
|
|
213
241
|
// Device session management functions - optimized with useCallback
|
|
214
242
|
const loadAllDeviceSessions = useCallback(async () => {
|
|
215
243
|
const currentActiveSessionId = activeSessionId ?? null;
|
|
@@ -428,6 +456,151 @@ const ModernAccountSwitcherScreen: React.FC<BaseScreenProps> = ({
|
|
|
428
456
|
</View>
|
|
429
457
|
)}
|
|
430
458
|
|
|
459
|
+
{/* Acting as banner - show switch-back when acting as a managed account */}
|
|
460
|
+
{actingAs && (
|
|
461
|
+
<View style={styles.section}>
|
|
462
|
+
<TouchableOpacity
|
|
463
|
+
style={[styles.settingItem, styles.firstSettingItem, styles.lastSettingItem, styles.actingAsBanner]}
|
|
464
|
+
onPress={handleSwitchBackToPrimary}
|
|
465
|
+
activeOpacity={0.7}
|
|
466
|
+
>
|
|
467
|
+
<View style={styles.settingInfo}>
|
|
468
|
+
<Text style={styles.settingLabel}>Switch back to primary account</Text>
|
|
469
|
+
<Text style={styles.settingDescription}>You are currently acting as another identity</Text>
|
|
470
|
+
</View>
|
|
471
|
+
<View style={styles.switchBackButton}>
|
|
472
|
+
<Text style={styles.switchBackButtonText}>Switch Back</Text>
|
|
473
|
+
</View>
|
|
474
|
+
</TouchableOpacity>
|
|
475
|
+
</View>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{/* Managed Accounts */}
|
|
479
|
+
{managedAccounts.length > 0 && (
|
|
480
|
+
<View style={styles.section}>
|
|
481
|
+
<Text style={styles.sectionTitle}>Managed Accounts</Text>
|
|
482
|
+
<Text style={styles.sectionSubtitle}>Identities you manage</Text>
|
|
483
|
+
|
|
484
|
+
{managedAccounts.map((managed, index) => {
|
|
485
|
+
const account = managed.account;
|
|
486
|
+
if (!account) return null;
|
|
487
|
+
|
|
488
|
+
const isActive = actingAs === managed.accountId;
|
|
489
|
+
const isSwitching = switchingManagedId === managed.accountId;
|
|
490
|
+
const isFirst = index === 0;
|
|
491
|
+
const isLast = index === managedAccounts.length - 1;
|
|
492
|
+
|
|
493
|
+
const managedDisplayName = typeof account.name === 'object'
|
|
494
|
+
? account.name.full || account.name.first || account.username
|
|
495
|
+
: account.name || account.username || 'Unknown';
|
|
496
|
+
|
|
497
|
+
// Determine the manager role for badge display
|
|
498
|
+
const myRole = managed.managers?.find(
|
|
499
|
+
(m) => m.userId === user?.id
|
|
500
|
+
)?.role ?? 'owner';
|
|
501
|
+
|
|
502
|
+
return (
|
|
503
|
+
<TouchableOpacity
|
|
504
|
+
key={`managed-${managed.accountId}`}
|
|
505
|
+
style={[
|
|
506
|
+
styles.settingItem,
|
|
507
|
+
isFirst && styles.firstSettingItem,
|
|
508
|
+
isLast && styles.lastSettingItem,
|
|
509
|
+
isActive && styles.currentAccountCard,
|
|
510
|
+
]}
|
|
511
|
+
onPress={() => handleSwitchToManagedAccount(managed.accountId)}
|
|
512
|
+
disabled={isActive || isSwitching}
|
|
513
|
+
activeOpacity={0.7}
|
|
514
|
+
>
|
|
515
|
+
<View style={styles.userIcon}>
|
|
516
|
+
{account.avatar ? (
|
|
517
|
+
<Image source={{ uri: oxyServices.getFileDownloadUrl(account.avatar, 'thumb') }} style={styles.accountAvatarImage} />
|
|
518
|
+
) : (
|
|
519
|
+
<View style={[styles.accountAvatarFallback, styles.managedAvatarFallback]}>
|
|
520
|
+
<Text style={styles.accountAvatarText}>
|
|
521
|
+
{managedDisplayName.charAt(0).toUpperCase()}
|
|
522
|
+
</Text>
|
|
523
|
+
</View>
|
|
524
|
+
)}
|
|
525
|
+
{isActive && (
|
|
526
|
+
<View style={styles.activeBadge}>
|
|
527
|
+
<OxyIcon name="checkmark" size={12} color="#fff" />
|
|
528
|
+
</View>
|
|
529
|
+
)}
|
|
530
|
+
</View>
|
|
531
|
+
<View style={styles.settingInfo}>
|
|
532
|
+
<View>
|
|
533
|
+
<Text style={styles.settingLabel}>{managedDisplayName}</Text>
|
|
534
|
+
<Text style={styles.settingDescription}>@{account.username}</Text>
|
|
535
|
+
</View>
|
|
536
|
+
</View>
|
|
537
|
+
<View style={styles.accountActions}>
|
|
538
|
+
<View style={styles.roleBadge}>
|
|
539
|
+
<Text style={styles.roleBadgeText}>{myRole}</Text>
|
|
540
|
+
</View>
|
|
541
|
+
{isActive ? (
|
|
542
|
+
<View style={styles.currentBadge}>
|
|
543
|
+
<Text style={styles.currentBadgeText}>Current</Text>
|
|
544
|
+
</View>
|
|
545
|
+
) : (
|
|
546
|
+
<TouchableOpacity
|
|
547
|
+
style={styles.switchButton}
|
|
548
|
+
onPress={() => handleSwitchToManagedAccount(managed.accountId)}
|
|
549
|
+
disabled={isSwitching}
|
|
550
|
+
>
|
|
551
|
+
{isSwitching ? (
|
|
552
|
+
<ActivityIndicator size="small" color="#fff" />
|
|
553
|
+
) : (
|
|
554
|
+
<Text style={styles.switchButtonText}>Act As</Text>
|
|
555
|
+
)}
|
|
556
|
+
</TouchableOpacity>
|
|
557
|
+
)}
|
|
558
|
+
</View>
|
|
559
|
+
</TouchableOpacity>
|
|
560
|
+
);
|
|
561
|
+
})}
|
|
562
|
+
|
|
563
|
+
{/* Create New Identity */}
|
|
564
|
+
<TouchableOpacity
|
|
565
|
+
style={[styles.settingItem, styles.firstSettingItem, styles.lastSettingItem, { marginTop: 8 }]}
|
|
566
|
+
onPress={() => navigate?.('CreateManagedAccount')}
|
|
567
|
+
activeOpacity={0.7}
|
|
568
|
+
>
|
|
569
|
+
<View style={styles.userIcon}>
|
|
570
|
+
<View style={[styles.accountAvatarFallback, { backgroundColor: '#007AFF20' }]}>
|
|
571
|
+
<OxyIcon name="add" size={20} color="#007AFF" />
|
|
572
|
+
</View>
|
|
573
|
+
</View>
|
|
574
|
+
<View style={styles.settingInfo}>
|
|
575
|
+
<Text style={[styles.settingLabel, { color: '#007AFF' }]}>Create New Identity</Text>
|
|
576
|
+
<Text style={styles.settingDescription}>Add a managed sub-account</Text>
|
|
577
|
+
</View>
|
|
578
|
+
</TouchableOpacity>
|
|
579
|
+
</View>
|
|
580
|
+
)}
|
|
581
|
+
|
|
582
|
+
{/* Create first managed account (when none exist yet) */}
|
|
583
|
+
{managedAccounts.length === 0 && isAuthenticated && (
|
|
584
|
+
<View style={styles.section}>
|
|
585
|
+
<Text style={styles.sectionTitle}>Managed Accounts</Text>
|
|
586
|
+
<TouchableOpacity
|
|
587
|
+
style={[styles.settingItem, styles.firstSettingItem, styles.lastSettingItem]}
|
|
588
|
+
onPress={() => navigate?.('CreateManagedAccount')}
|
|
589
|
+
activeOpacity={0.7}
|
|
590
|
+
>
|
|
591
|
+
<View style={styles.userIcon}>
|
|
592
|
+
<View style={[styles.accountAvatarFallback, { backgroundColor: '#007AFF20' }]}>
|
|
593
|
+
<OxyIcon name="add" size={20} color="#007AFF" />
|
|
594
|
+
</View>
|
|
595
|
+
</View>
|
|
596
|
+
<View style={styles.settingInfo}>
|
|
597
|
+
<Text style={[styles.settingLabel, { color: '#007AFF' }]}>Create New Identity</Text>
|
|
598
|
+
<Text style={styles.settingDescription}>Create a managed sub-account you control</Text>
|
|
599
|
+
</View>
|
|
600
|
+
</TouchableOpacity>
|
|
601
|
+
</View>
|
|
602
|
+
)}
|
|
603
|
+
|
|
431
604
|
{/* Quick Actions */}
|
|
432
605
|
<View style={styles.section}>
|
|
433
606
|
<Text style={styles.sectionTitle}>Quick Actions</Text>
|
|
@@ -729,6 +902,42 @@ const styles = StyleSheet.create({
|
|
|
729
902
|
fontSize: 16,
|
|
730
903
|
fontWeight: '600',
|
|
731
904
|
},
|
|
905
|
+
sectionSubtitle: {
|
|
906
|
+
fontSize: 13,
|
|
907
|
+
color: '#888',
|
|
908
|
+
marginBottom: 12,
|
|
909
|
+
},
|
|
910
|
+
managedAvatarFallback: {
|
|
911
|
+
backgroundColor: '#5856D6',
|
|
912
|
+
},
|
|
913
|
+
roleBadge: {
|
|
914
|
+
backgroundColor: '#F2F2F7',
|
|
915
|
+
paddingHorizontal: 8,
|
|
916
|
+
paddingVertical: 3,
|
|
917
|
+
borderRadius: 8,
|
|
918
|
+
},
|
|
919
|
+
roleBadgeText: {
|
|
920
|
+
color: '#666',
|
|
921
|
+
fontSize: 11,
|
|
922
|
+
fontWeight: '500',
|
|
923
|
+
textTransform: 'capitalize',
|
|
924
|
+
},
|
|
925
|
+
actingAsBanner: {
|
|
926
|
+
borderWidth: 2,
|
|
927
|
+
borderColor: '#FF9500',
|
|
928
|
+
backgroundColor: '#FF950010',
|
|
929
|
+
},
|
|
930
|
+
switchBackButton: {
|
|
931
|
+
backgroundColor: '#FF9500',
|
|
932
|
+
paddingHorizontal: 14,
|
|
933
|
+
paddingVertical: 8,
|
|
934
|
+
borderRadius: 16,
|
|
935
|
+
},
|
|
936
|
+
switchBackButtonText: {
|
|
937
|
+
color: '#fff',
|
|
938
|
+
fontSize: 13,
|
|
939
|
+
fontWeight: '600',
|
|
940
|
+
},
|
|
732
941
|
});
|
|
733
942
|
|
|
734
943
|
export default ModernAccountSwitcherScreen;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
View,
|
|
5
|
+
Text,
|
|
6
|
+
TextInput,
|
|
7
|
+
TouchableOpacity,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
ActivityIndicator,
|
|
10
|
+
ScrollView,
|
|
11
|
+
Platform,
|
|
12
|
+
KeyboardAvoidingView,
|
|
13
|
+
} from 'react-native';
|
|
14
|
+
import type { BaseScreenProps } from '../types/navigation';
|
|
15
|
+
import { fontFamilies } from '../styles/fonts';
|
|
16
|
+
import { Header } from '../components';
|
|
17
|
+
import { useI18n } from '../hooks/useI18n';
|
|
18
|
+
import { useTheme } from '@oxyhq/bloom/theme';
|
|
19
|
+
import { useOxy } from '../context/OxyContext';
|
|
20
|
+
import { toast } from '../../lib/sonner';
|
|
21
|
+
import { screenContentStyle } from '../constants/spacing';
|
|
22
|
+
|
|
23
|
+
type UsernameStatus = 'idle' | 'checking' | 'available' | 'taken' | 'invalid';
|
|
24
|
+
|
|
25
|
+
const USERNAME_REGEX = /^[a-zA-Z0-9_-]{3,30}$/;
|
|
26
|
+
const DEBOUNCE_MS = 400;
|
|
27
|
+
|
|
28
|
+
const CreateManagedAccountScreen: React.FC<BaseScreenProps> = ({
|
|
29
|
+
onClose,
|
|
30
|
+
goBack,
|
|
31
|
+
}) => {
|
|
32
|
+
const bloomTheme = useTheme();
|
|
33
|
+
const { oxyServices, createManagedAccount, setActingAs } = useOxy();
|
|
34
|
+
const { t } = useI18n();
|
|
35
|
+
|
|
36
|
+
const [username, setUsername] = useState('');
|
|
37
|
+
const [displayName, setDisplayName] = useState('');
|
|
38
|
+
const [bio, setBio] = useState('');
|
|
39
|
+
const [usernameStatus, setUsernameStatus] = useState<UsernameStatus>('idle');
|
|
40
|
+
const [usernameMessage, setUsernameMessage] = useState('');
|
|
41
|
+
const [isCreating, setIsCreating] = useState(false);
|
|
42
|
+
|
|
43
|
+
const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
44
|
+
|
|
45
|
+
// Debounced username availability check
|
|
46
|
+
const checkUsername = useCallback((value: string) => {
|
|
47
|
+
if (debounceTimerRef.current) {
|
|
48
|
+
clearTimeout(debounceTimerRef.current);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!value || value.length < 3) {
|
|
52
|
+
setUsernameStatus(value.length > 0 ? 'invalid' : 'idle');
|
|
53
|
+
setUsernameMessage(value.length > 0 ? 'Username must be at least 3 characters' : '');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!USERNAME_REGEX.test(value)) {
|
|
58
|
+
setUsernameStatus('invalid');
|
|
59
|
+
setUsernameMessage('Only letters, numbers, hyphens, and underscores');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setUsernameStatus('checking');
|
|
64
|
+
setUsernameMessage('');
|
|
65
|
+
|
|
66
|
+
debounceTimerRef.current = setTimeout(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const result = await oxyServices.checkUsernameAvailability(value);
|
|
69
|
+
setUsernameStatus(result.available ? 'available' : 'taken');
|
|
70
|
+
setUsernameMessage(result.message || (result.available ? 'Username is available' : 'Username is taken'));
|
|
71
|
+
} catch {
|
|
72
|
+
setUsernameStatus('idle');
|
|
73
|
+
setUsernameMessage('Could not check availability');
|
|
74
|
+
}
|
|
75
|
+
}, DEBOUNCE_MS);
|
|
76
|
+
}, [oxyServices]);
|
|
77
|
+
|
|
78
|
+
const handleUsernameChange = useCallback((value: string) => {
|
|
79
|
+
const cleaned = value.toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
80
|
+
setUsername(cleaned);
|
|
81
|
+
checkUsername(cleaned);
|
|
82
|
+
}, [checkUsername]);
|
|
83
|
+
|
|
84
|
+
// Cleanup debounce timer
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
return () => {
|
|
87
|
+
if (debounceTimerRef.current) {
|
|
88
|
+
clearTimeout(debounceTimerRef.current);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}, []);
|
|
92
|
+
|
|
93
|
+
const canCreate = usernameStatus === 'available' && displayName.trim().length > 0 && !isCreating;
|
|
94
|
+
|
|
95
|
+
const handleCreate = useCallback(async () => {
|
|
96
|
+
if (!canCreate) return;
|
|
97
|
+
|
|
98
|
+
setIsCreating(true);
|
|
99
|
+
try {
|
|
100
|
+
// Split display name into first/last
|
|
101
|
+
const nameParts = displayName.trim().split(/\s+/);
|
|
102
|
+
const firstName = nameParts[0] || '';
|
|
103
|
+
const lastName = nameParts.length > 1 ? nameParts.slice(1).join(' ') : undefined;
|
|
104
|
+
|
|
105
|
+
const account = await createManagedAccount({
|
|
106
|
+
username,
|
|
107
|
+
name: { first: firstName, last: lastName },
|
|
108
|
+
bio: bio.trim() || undefined,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
toast.success('Identity created successfully');
|
|
112
|
+
|
|
113
|
+
// Switch to the new managed account
|
|
114
|
+
if (account.accountId) {
|
|
115
|
+
setActingAs(account.accountId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onClose?.();
|
|
119
|
+
} catch (error) {
|
|
120
|
+
const message = error instanceof Error ? error.message : 'Failed to create identity';
|
|
121
|
+
toast.error(message);
|
|
122
|
+
} finally {
|
|
123
|
+
setIsCreating(false);
|
|
124
|
+
}
|
|
125
|
+
}, [canCreate, username, displayName, bio, createManagedAccount, setActingAs, onClose]);
|
|
126
|
+
|
|
127
|
+
const getStatusColor = (): string => {
|
|
128
|
+
switch (usernameStatus) {
|
|
129
|
+
case 'available': return '#34C759';
|
|
130
|
+
case 'taken':
|
|
131
|
+
case 'invalid': return bloomTheme.colors.error;
|
|
132
|
+
case 'checking': return bloomTheme.colors.primary;
|
|
133
|
+
default: return 'transparent';
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<View style={[styles.container, { backgroundColor: bloomTheme.colors.background }]}>
|
|
139
|
+
<Header
|
|
140
|
+
title="Create Identity"
|
|
141
|
+
onBack={goBack}
|
|
142
|
+
onClose={onClose}
|
|
143
|
+
showBackButton={true}
|
|
144
|
+
showCloseButton={true}
|
|
145
|
+
elevation="subtle"
|
|
146
|
+
/>
|
|
147
|
+
|
|
148
|
+
<KeyboardAvoidingView
|
|
149
|
+
style={styles.flex}
|
|
150
|
+
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
151
|
+
keyboardVerticalOffset={Platform.OS === 'ios' ? 88 : 0}
|
|
152
|
+
>
|
|
153
|
+
<ScrollView
|
|
154
|
+
style={styles.flex}
|
|
155
|
+
contentContainerStyle={screenContentStyle}
|
|
156
|
+
keyboardShouldPersistTaps="handled"
|
|
157
|
+
>
|
|
158
|
+
<Text style={[styles.description, { color: bloomTheme.colors.text }]}>
|
|
159
|
+
Create a managed identity that you control. It will have its own profile, posts, and interactions.
|
|
160
|
+
</Text>
|
|
161
|
+
|
|
162
|
+
{/* Username */}
|
|
163
|
+
<View style={styles.fieldGroup}>
|
|
164
|
+
<Text style={[styles.label, { color: bloomTheme.colors.text }]}>Username</Text>
|
|
165
|
+
<View style={[
|
|
166
|
+
styles.inputContainer,
|
|
167
|
+
{ borderColor: usernameStatus !== 'idle' ? getStatusColor() : bloomTheme.colors.border },
|
|
168
|
+
]}>
|
|
169
|
+
<Text style={[styles.inputPrefix, { color: bloomTheme.colors.text + '80' }]}>@</Text>
|
|
170
|
+
<TextInput
|
|
171
|
+
style={[styles.input, { color: bloomTheme.colors.text }]}
|
|
172
|
+
value={username}
|
|
173
|
+
onChangeText={handleUsernameChange}
|
|
174
|
+
placeholder="username"
|
|
175
|
+
placeholderTextColor={bloomTheme.colors.text + '40'}
|
|
176
|
+
autoCapitalize="none"
|
|
177
|
+
autoCorrect={false}
|
|
178
|
+
autoComplete="off"
|
|
179
|
+
maxLength={30}
|
|
180
|
+
/>
|
|
181
|
+
{usernameStatus === 'checking' && (
|
|
182
|
+
<ActivityIndicator size="small" color={bloomTheme.colors.primary} />
|
|
183
|
+
)}
|
|
184
|
+
{usernameStatus === 'available' && (
|
|
185
|
+
<Text style={styles.statusIcon}>OK</Text>
|
|
186
|
+
)}
|
|
187
|
+
</View>
|
|
188
|
+
{usernameMessage ? (
|
|
189
|
+
<Text style={[styles.statusMessage, { color: getStatusColor() }]}>
|
|
190
|
+
{usernameMessage}
|
|
191
|
+
</Text>
|
|
192
|
+
) : null}
|
|
193
|
+
</View>
|
|
194
|
+
|
|
195
|
+
{/* Display Name */}
|
|
196
|
+
<View style={styles.fieldGroup}>
|
|
197
|
+
<Text style={[styles.label, { color: bloomTheme.colors.text }]}>Display Name</Text>
|
|
198
|
+
<View style={[styles.inputContainer, { borderColor: bloomTheme.colors.border }]}>
|
|
199
|
+
<TextInput
|
|
200
|
+
style={[styles.input, { color: bloomTheme.colors.text }]}
|
|
201
|
+
value={displayName}
|
|
202
|
+
onChangeText={setDisplayName}
|
|
203
|
+
placeholder="Display name"
|
|
204
|
+
placeholderTextColor={bloomTheme.colors.text + '40'}
|
|
205
|
+
maxLength={50}
|
|
206
|
+
/>
|
|
207
|
+
</View>
|
|
208
|
+
</View>
|
|
209
|
+
|
|
210
|
+
{/* Bio */}
|
|
211
|
+
<View style={styles.fieldGroup}>
|
|
212
|
+
<Text style={[styles.label, { color: bloomTheme.colors.text }]}>Bio (optional)</Text>
|
|
213
|
+
<View style={[styles.inputContainer, styles.bioContainer, { borderColor: bloomTheme.colors.border }]}>
|
|
214
|
+
<TextInput
|
|
215
|
+
style={[styles.input, styles.bioInput, { color: bloomTheme.colors.text }]}
|
|
216
|
+
value={bio}
|
|
217
|
+
onChangeText={setBio}
|
|
218
|
+
placeholder="Tell people about this identity"
|
|
219
|
+
placeholderTextColor={bloomTheme.colors.text + '40'}
|
|
220
|
+
maxLength={160}
|
|
221
|
+
multiline
|
|
222
|
+
numberOfLines={3}
|
|
223
|
+
textAlignVertical="top"
|
|
224
|
+
/>
|
|
225
|
+
</View>
|
|
226
|
+
<Text style={[styles.charCount, { color: bloomTheme.colors.text + '60' }]}>
|
|
227
|
+
{bio.length}/160
|
|
228
|
+
</Text>
|
|
229
|
+
</View>
|
|
230
|
+
|
|
231
|
+
{/* Create Button */}
|
|
232
|
+
<TouchableOpacity
|
|
233
|
+
style={[
|
|
234
|
+
styles.createButton,
|
|
235
|
+
{ backgroundColor: bloomTheme.colors.primary },
|
|
236
|
+
!canCreate && styles.createButtonDisabled,
|
|
237
|
+
]}
|
|
238
|
+
onPress={handleCreate}
|
|
239
|
+
disabled={!canCreate}
|
|
240
|
+
activeOpacity={0.7}
|
|
241
|
+
>
|
|
242
|
+
{isCreating ? (
|
|
243
|
+
<ActivityIndicator size="small" color="#fff" />
|
|
244
|
+
) : (
|
|
245
|
+
<Text style={styles.createButtonText}>Create Identity</Text>
|
|
246
|
+
)}
|
|
247
|
+
</TouchableOpacity>
|
|
248
|
+
</ScrollView>
|
|
249
|
+
</KeyboardAvoidingView>
|
|
250
|
+
</View>
|
|
251
|
+
);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const styles = StyleSheet.create({
|
|
255
|
+
container: {
|
|
256
|
+
flex: 1,
|
|
257
|
+
},
|
|
258
|
+
flex: {
|
|
259
|
+
flex: 1,
|
|
260
|
+
},
|
|
261
|
+
description: {
|
|
262
|
+
fontSize: 15,
|
|
263
|
+
lineHeight: 22,
|
|
264
|
+
marginBottom: 24,
|
|
265
|
+
fontFamily: fontFamilies.inter,
|
|
266
|
+
},
|
|
267
|
+
fieldGroup: {
|
|
268
|
+
marginBottom: 20,
|
|
269
|
+
},
|
|
270
|
+
label: {
|
|
271
|
+
fontSize: 14,
|
|
272
|
+
fontFamily: fontFamilies.interSemiBold,
|
|
273
|
+
fontWeight: Platform.OS === 'web' ? '600' : undefined,
|
|
274
|
+
marginBottom: 8,
|
|
275
|
+
},
|
|
276
|
+
inputContainer: {
|
|
277
|
+
flexDirection: 'row',
|
|
278
|
+
alignItems: 'center',
|
|
279
|
+
borderWidth: 1,
|
|
280
|
+
borderRadius: 12,
|
|
281
|
+
paddingHorizontal: 14,
|
|
282
|
+
height: 48,
|
|
283
|
+
},
|
|
284
|
+
inputPrefix: {
|
|
285
|
+
fontSize: 16,
|
|
286
|
+
fontFamily: fontFamilies.inter,
|
|
287
|
+
marginRight: 2,
|
|
288
|
+
},
|
|
289
|
+
input: {
|
|
290
|
+
flex: 1,
|
|
291
|
+
fontSize: 16,
|
|
292
|
+
fontFamily: fontFamilies.inter,
|
|
293
|
+
paddingVertical: 0,
|
|
294
|
+
},
|
|
295
|
+
bioContainer: {
|
|
296
|
+
height: 88,
|
|
297
|
+
alignItems: 'flex-start',
|
|
298
|
+
paddingVertical: 12,
|
|
299
|
+
},
|
|
300
|
+
bioInput: {
|
|
301
|
+
height: 64,
|
|
302
|
+
},
|
|
303
|
+
statusIcon: {
|
|
304
|
+
fontSize: 13,
|
|
305
|
+
fontFamily: fontFamilies.interSemiBold,
|
|
306
|
+
fontWeight: Platform.OS === 'web' ? '600' : undefined,
|
|
307
|
+
color: '#34C759',
|
|
308
|
+
},
|
|
309
|
+
statusMessage: {
|
|
310
|
+
fontSize: 13,
|
|
311
|
+
fontFamily: fontFamilies.inter,
|
|
312
|
+
marginTop: 6,
|
|
313
|
+
},
|
|
314
|
+
charCount: {
|
|
315
|
+
fontSize: 12,
|
|
316
|
+
fontFamily: fontFamilies.inter,
|
|
317
|
+
textAlign: 'right',
|
|
318
|
+
marginTop: 4,
|
|
319
|
+
},
|
|
320
|
+
createButton: {
|
|
321
|
+
height: 50,
|
|
322
|
+
borderRadius: 25,
|
|
323
|
+
alignItems: 'center',
|
|
324
|
+
justifyContent: 'center',
|
|
325
|
+
marginTop: 12,
|
|
326
|
+
},
|
|
327
|
+
createButtonDisabled: {
|
|
328
|
+
opacity: 0.5,
|
|
329
|
+
},
|
|
330
|
+
createButtonText: {
|
|
331
|
+
color: '#fff',
|
|
332
|
+
fontSize: 16,
|
|
333
|
+
fontFamily: fontFamilies.interSemiBold,
|
|
334
|
+
fontWeight: Platform.OS === 'web' ? '600' : undefined,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
export default CreateManagedAccountScreen;
|