@squad-sports/react-native 1.3.1
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/LICENSE +21 -0
- package/README.md +97 -0
- package/package.json +46 -0
- package/src/SquadExperience.tsx +200 -0
- package/src/SquadProvider.tsx +232 -0
- package/src/SquadSportsSDK.ts +286 -0
- package/src/__tests__/DeepLinkHandler.test.ts +101 -0
- package/src/__tests__/ErrorBoundary.test.tsx +161 -0
- package/src/__tests__/EventProcessor.test.ts +241 -0
- package/src/__tests__/PushNotificationHandler.test.ts +91 -0
- package/src/__tests__/SecureStorage.test.ts +62 -0
- package/src/__tests__/SquadSportsSDK.test.ts +278 -0
- package/src/__tests__/VerificationCooldown.test.ts +153 -0
- package/src/components/ErrorBoundary.tsx +129 -0
- package/src/components/SOTD/SOTDComponents.tsx +101 -0
- package/src/components/audio/AudioPlayerRow.tsx +189 -0
- package/src/components/audio/recording/AudioRecording.tsx +232 -0
- package/src/components/communities/CommunityComponents.tsx +78 -0
- package/src/components/dialogs/AllDialogs.tsx +123 -0
- package/src/components/dialogs/ConfirmDialog.tsx +77 -0
- package/src/components/dialogs/PermissionDialog.tsx +132 -0
- package/src/components/dragAndDrop/DragAndDrop.tsx +56 -0
- package/src/components/events/EventComponents.tsx +93 -0
- package/src/components/feed/ChatBannerCard.tsx +94 -0
- package/src/components/feed/FeedLoadingSkeleton.tsx +43 -0
- package/src/components/feed/FreestyleCard.tsx +119 -0
- package/src/components/feed/InterstitialOverlay.tsx +190 -0
- package/src/components/feed/PollCard.tsx +158 -0
- package/src/components/feed/SponsoredContentCard.tsx +118 -0
- package/src/components/freestyle/FreestyleComponents.tsx +148 -0
- package/src/components/index.ts +42 -0
- package/src/components/message/MessageCard.tsx +166 -0
- package/src/components/message/MessageComponents.tsx +143 -0
- package/src/components/poll/PollComponents.tsx +226 -0
- package/src/components/sentinels/Sentinels.tsx +175 -0
- package/src/components/squad/FeedSquad.tsx +54 -0
- package/src/components/toasts/Toasts.tsx +88 -0
- package/src/components/ux/RootUXComponents.tsx +157 -0
- package/src/components/ux/action-sheet/ActionSheet.tsx +67 -0
- package/src/components/ux/bottom-sheet/BottomSheetComponents.tsx +93 -0
- package/src/components/ux/bottom-sheet/SquadBottomSheet.tsx +119 -0
- package/src/components/ux/buttons/Button.tsx +37 -0
- package/src/components/ux/buttons/InfoButton.tsx +24 -0
- package/src/components/ux/buttons/XButton.tsx +27 -0
- package/src/components/ux/carousel/Carousel.tsx +134 -0
- package/src/components/ux/emoji-picker/EmojiReactionPicker.tsx +139 -0
- package/src/components/ux/errors/ErrorHint.tsx +46 -0
- package/src/components/ux/inputs/CodeInput.tsx +121 -0
- package/src/components/ux/inputs/DatePicker.tsx +76 -0
- package/src/components/ux/inputs/MaskedTextInput.tsx +95 -0
- package/src/components/ux/inputs/PhoneNumberInput.tsx +90 -0
- package/src/components/ux/inputs/SearchTextInput.tsx +70 -0
- package/src/components/ux/inputs/TextInput.tsx +58 -0
- package/src/components/ux/layout/AvoidKeyboardScreen.tsx +58 -0
- package/src/components/ux/layout/BlurOverlay.tsx +26 -0
- package/src/components/ux/layout/CrossFade.tsx +30 -0
- package/src/components/ux/layout/DismissKeyboardOnBlur.tsx +14 -0
- package/src/components/ux/layout/LoadingOverlay.tsx +60 -0
- package/src/components/ux/layout/NetworkBanner.tsx +64 -0
- package/src/components/ux/layout/PermissionsCTAContent.tsx +54 -0
- package/src/components/ux/layout/RefreshControl.tsx +7 -0
- package/src/components/ux/layout/Screen.tsx +31 -0
- package/src/components/ux/layout/ScreenHeader.tsx +89 -0
- package/src/components/ux/layout/TabBar.tsx +39 -0
- package/src/components/ux/layout/Toast.tsx +116 -0
- package/src/components/ux/layout/TransparentOverlay.tsx +21 -0
- package/src/components/ux/navigation/BackButton.tsx +29 -0
- package/src/components/ux/navigation/LinkButton.tsx +21 -0
- package/src/components/ux/navigation/UrlButton.tsx +25 -0
- package/src/components/ux/scroll/RefreshableFlatList.tsx +35 -0
- package/src/components/ux/shapes/Shapes.tsx +23 -0
- package/src/components/ux/text/Typography.tsx +28 -0
- package/src/components/ux/user-image/UserImage.tsx +75 -0
- package/src/components/ux/user-image/UserImageVariants.tsx +104 -0
- package/src/components/wallet/WalletComponents.tsx +116 -0
- package/src/contexts/AuthContext.tsx +45 -0
- package/src/contexts/EventProcessorContext.tsx +41 -0
- package/src/contexts/PlayerQueueContext.tsx +95 -0
- package/src/hooks/useAuth.ts +23 -0
- package/src/hooks/useDataRefresh.ts +30 -0
- package/src/hooks/useEventProcessor.ts +6 -0
- package/src/hooks/useImageOptimization.ts +59 -0
- package/src/hooks/useOnboardingStepGuard.ts +36 -0
- package/src/hooks/usePendingNavigation.ts +26 -0
- package/src/hooks/useSquadData.ts +84 -0
- package/src/hooks/useUserCreated.ts +25 -0
- package/src/hooks/useUserUpdate.ts +25 -0
- package/src/hooks/useViewabilityTracker.ts +40 -0
- package/src/index.ts +109 -0
- package/src/navigation/SquadNavigator.tsx +262 -0
- package/src/realtime/DeepLinkHandler.ts +113 -0
- package/src/realtime/EventProcessor.ts +313 -0
- package/src/realtime/NetworkMonitor.ts +84 -0
- package/src/realtime/OfflineQueue.ts +133 -0
- package/src/realtime/PushNotificationHandler.ts +125 -0
- package/src/realtime/useRealtimeSync.ts +84 -0
- package/src/screens/auth/EmailVerificationScreen.tsx +201 -0
- package/src/screens/auth/EnterCodeScreen.tsx +253 -0
- package/src/screens/auth/EnterEmailScreen.tsx +234 -0
- package/src/screens/auth/LandingScreen.tsx +90 -0
- package/src/screens/auth/LoginScreen.tsx +126 -0
- package/src/screens/events/EventScreen.tsx +163 -0
- package/src/screens/freestyle/CommunityFreestyleScreen.tsx +82 -0
- package/src/screens/freestyle/FreestyleCreationScreen.tsx +255 -0
- package/src/screens/freestyle/FreestyleListensScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleReactionsScreen.tsx +44 -0
- package/src/screens/freestyle/FreestyleScreen.tsx +64 -0
- package/src/screens/home/HomeScreen.tsx +365 -0
- package/src/screens/home/slivers/SquadCircle.tsx +77 -0
- package/src/screens/invite/InvitationQrCodeScreen.tsx +114 -0
- package/src/screens/invite/InviteScreen.tsx +175 -0
- package/src/screens/messaging/MessagingScreen.tsx +360 -0
- package/src/screens/onboarding/OnboardingAccountSetupScreen.tsx +221 -0
- package/src/screens/onboarding/OnboardingTeamSelectScreen.tsx +215 -0
- package/src/screens/onboarding/OnboardingWelcomeScreen.tsx +93 -0
- package/src/screens/polls/PollResponseScreen.tsx +229 -0
- package/src/screens/polls/PollSummationScreen.tsx +78 -0
- package/src/screens/profile/ProfileScreen.tsx +234 -0
- package/src/screens/settings/BlockedUsersScreen.tsx +139 -0
- package/src/screens/settings/DeleteAccountScreen.tsx +182 -0
- package/src/screens/settings/EditProfileScreen.tsx +154 -0
- package/src/screens/settings/NetworkStatusScreen.tsx +59 -0
- package/src/screens/settings/SettingsScreen.tsx +194 -0
- package/src/screens/squad-line/AddCallTitleScreen.tsx +293 -0
- package/src/screens/wallet/WalletScreen.tsx +174 -0
- package/src/services/AuthStateManager.ts +93 -0
- package/src/services/NavigationService.ts +40 -0
- package/src/services/UserDataManager.ts +59 -0
- package/src/services/UserUpdateService.ts +31 -0
- package/src/services/VerificationStateManager.ts +41 -0
- package/src/squad-line/CallScreen.tsx +158 -0
- package/src/squad-line/IncomingCallOverlay.tsx +113 -0
- package/src/squad-line/SquadLineClient.ts +327 -0
- package/src/squad-line/useSquadLine.ts +80 -0
- package/src/state/audio.ts +38 -0
- package/src/state/client.ts +26 -0
- package/src/state/communities.ts +45 -0
- package/src/state/contacts.ts +28 -0
- package/src/state/device-info.ts +22 -0
- package/src/state/events.ts +16 -0
- package/src/state/features.ts +57 -0
- package/src/state/index.ts +121 -0
- package/src/state/invitations.ts +16 -0
- package/src/state/modal-keys.ts +63 -0
- package/src/state/modal-queue.ts +104 -0
- package/src/state/navigation.ts +34 -0
- package/src/state/permissions.ts +43 -0
- package/src/state/session.ts +223 -0
- package/src/state/squaddie-of-the-day.ts +21 -0
- package/src/state/sync/crdt.ts +70 -0
- package/src/state/sync/dependable.ts +213 -0
- package/src/state/sync/feed-v2.ts +42 -0
- package/src/state/sync/messages.ts +44 -0
- package/src/state/sync/offline-support.ts +42 -0
- package/src/state/sync/polls.ts +37 -0
- package/src/state/sync/refresh.ts +24 -0
- package/src/state/sync/squad-v2.ts +25 -0
- package/src/state/ui.ts +36 -0
- package/src/state/user.ts +46 -0
- package/src/state/wallet.ts +26 -0
- package/src/storage/SecureStorage.ts +77 -0
- package/src/theme/ThemeContext.tsx +159 -0
- package/src/types/modules.d.ts +165 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Pressable,
|
|
7
|
+
Alert,
|
|
8
|
+
TextInput,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
import { useNavigation } from '@react-navigation/native';
|
|
11
|
+
|
|
12
|
+
import { useSquadSDK, useApiClient } from '../../SquadProvider';
|
|
13
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
14
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
15
|
+
import Button from '../../components/ux/buttons/Button';
|
|
16
|
+
import { TitleMedium, BodyRegular, BodyMedium, BodySmall } from '../../components/ux/text/Typography';
|
|
17
|
+
|
|
18
|
+
const REASONS = [
|
|
19
|
+
{ id: 'not_useful', label: "It's not useful to me" },
|
|
20
|
+
{ id: 'privacy', label: 'Privacy concerns' },
|
|
21
|
+
{ id: 'too_many_notifications', label: 'Too many notifications' },
|
|
22
|
+
{ id: 'found_alternative', label: 'Found an alternative' },
|
|
23
|
+
{ id: 'other', label: 'Other' },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
export function DeleteAccountScreen() {
|
|
27
|
+
const navigation = useNavigation();
|
|
28
|
+
const sdk = useSquadSDK();
|
|
29
|
+
const apiClient = useApiClient();
|
|
30
|
+
const { theme } = useTheme();
|
|
31
|
+
|
|
32
|
+
const [selectedReason, setSelectedReason] = useState<string | null>(null);
|
|
33
|
+
const [otherText, setOtherText] = useState('');
|
|
34
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
35
|
+
const [step, setStep] = useState<'reason' | 'confirm'>('reason');
|
|
36
|
+
|
|
37
|
+
const handleDelete = useCallback(async () => {
|
|
38
|
+
setIsDeleting(true);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const { UserDeletionRequestBody } = await import('@squad-sports/core');
|
|
42
|
+
await apiClient.requestAccountDeletion(
|
|
43
|
+
new UserDeletionRequestBody({
|
|
44
|
+
reason: (selectedReason === 'other' ? otherText : selectedReason ?? '') as any,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
await sdk.sessionManager.logout();
|
|
48
|
+
// Navigator will route to auth
|
|
49
|
+
} catch {
|
|
50
|
+
Alert.alert('Error', 'Failed to delete account. Please try again or contact support.');
|
|
51
|
+
setIsDeleting(false);
|
|
52
|
+
}
|
|
53
|
+
}, [selectedReason, otherText, apiClient, sdk]);
|
|
54
|
+
|
|
55
|
+
const handleContinue = useCallback(() => {
|
|
56
|
+
if (!selectedReason) return;
|
|
57
|
+
|
|
58
|
+
Alert.alert(
|
|
59
|
+
'Delete Account',
|
|
60
|
+
'This will permanently delete your account, all your messages, freestyles, and connections. This cannot be undone.',
|
|
61
|
+
[
|
|
62
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
63
|
+
{ text: 'Delete My Account', style: 'destructive', onPress: handleDelete },
|
|
64
|
+
],
|
|
65
|
+
);
|
|
66
|
+
}, [selectedReason, handleDelete]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
70
|
+
<ScreenHeader title="Delete Account" />
|
|
71
|
+
|
|
72
|
+
<View style={styles.content}>
|
|
73
|
+
<View style={styles.warningBanner}>
|
|
74
|
+
<Text style={styles.warningIcon}>{'!'}</Text>
|
|
75
|
+
<BodyMedium style={styles.warningText}>
|
|
76
|
+
This action is permanent and cannot be undone
|
|
77
|
+
</BodyMedium>
|
|
78
|
+
</View>
|
|
79
|
+
|
|
80
|
+
<TitleMedium style={styles.heading}>
|
|
81
|
+
We're sorry to see you go
|
|
82
|
+
</TitleMedium>
|
|
83
|
+
<BodyRegular style={styles.subtitle}>
|
|
84
|
+
Please tell us why you're leaving so we can improve
|
|
85
|
+
</BodyRegular>
|
|
86
|
+
|
|
87
|
+
<View style={styles.reasons}>
|
|
88
|
+
{REASONS.map(reason => (
|
|
89
|
+
<Pressable
|
|
90
|
+
key={reason.id}
|
|
91
|
+
style={[
|
|
92
|
+
styles.reasonRow,
|
|
93
|
+
selectedReason === reason.id && [styles.reasonRowSelected, { borderColor: Colors.red }],
|
|
94
|
+
]}
|
|
95
|
+
onPress={() => setSelectedReason(reason.id)}
|
|
96
|
+
>
|
|
97
|
+
<View style={[
|
|
98
|
+
styles.radio,
|
|
99
|
+
selectedReason === reason.id && styles.radioSelected,
|
|
100
|
+
]}>
|
|
101
|
+
{selectedReason === reason.id && <View style={styles.radioDot} />}
|
|
102
|
+
</View>
|
|
103
|
+
<BodyRegular style={styles.reasonText}>{reason.label}</BodyRegular>
|
|
104
|
+
</Pressable>
|
|
105
|
+
))}
|
|
106
|
+
</View>
|
|
107
|
+
|
|
108
|
+
{selectedReason === 'other' && (
|
|
109
|
+
<TextInput
|
|
110
|
+
value={otherText}
|
|
111
|
+
onChangeText={setOtherText}
|
|
112
|
+
placeholder="Tell us more..."
|
|
113
|
+
placeholderTextColor={Colors.gray6}
|
|
114
|
+
style={styles.otherInput}
|
|
115
|
+
multiline
|
|
116
|
+
maxLength={200}
|
|
117
|
+
/>
|
|
118
|
+
)}
|
|
119
|
+
</View>
|
|
120
|
+
|
|
121
|
+
<View style={styles.footer}>
|
|
122
|
+
<Button
|
|
123
|
+
style={[styles.deleteButton, { opacity: selectedReason ? 1 : 0.5 }]}
|
|
124
|
+
onPress={handleContinue}
|
|
125
|
+
disabled={!selectedReason || isDeleting}
|
|
126
|
+
>
|
|
127
|
+
<Text style={styles.deleteButtonText}>
|
|
128
|
+
{isDeleting ? 'Deleting...' : 'Delete My Account'}
|
|
129
|
+
</Text>
|
|
130
|
+
</Button>
|
|
131
|
+
|
|
132
|
+
<Button style={styles.cancelButton} onPress={() => navigation.goBack()}>
|
|
133
|
+
<Text style={styles.cancelButtonText}>Keep My Account</Text>
|
|
134
|
+
</Button>
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const styles = StyleSheet.create({
|
|
141
|
+
container: { flex: 1 },
|
|
142
|
+
content: { flex: 1, paddingHorizontal: 24, paddingTop: 16 },
|
|
143
|
+
warningBanner: {
|
|
144
|
+
flexDirection: 'row', alignItems: 'center', gap: 8,
|
|
145
|
+
backgroundColor: 'rgba(255, 68, 120, 0.1)',
|
|
146
|
+
borderRadius: 8, padding: 12, marginBottom: 24,
|
|
147
|
+
},
|
|
148
|
+
warningIcon: { color: Colors.red, fontSize: 18, fontWeight: '700' },
|
|
149
|
+
warningText: { color: Colors.red, flex: 1 },
|
|
150
|
+
heading: { color: Colors.white, marginBottom: 8 },
|
|
151
|
+
subtitle: { color: Colors.gray6, marginBottom: 24 },
|
|
152
|
+
reasons: { gap: 8 },
|
|
153
|
+
reasonRow: {
|
|
154
|
+
flexDirection: 'row', alignItems: 'center', gap: 12,
|
|
155
|
+
padding: 16, backgroundColor: Colors.gray2, borderRadius: 12,
|
|
156
|
+
borderWidth: 2, borderColor: 'transparent',
|
|
157
|
+
},
|
|
158
|
+
reasonRowSelected: { backgroundColor: 'rgba(255, 68, 120, 0.05)' },
|
|
159
|
+
radio: {
|
|
160
|
+
width: 22, height: 22, borderRadius: 11,
|
|
161
|
+
borderWidth: 2, borderColor: Colors.gray5,
|
|
162
|
+
justifyContent: 'center', alignItems: 'center',
|
|
163
|
+
},
|
|
164
|
+
radioSelected: { borderColor: Colors.red },
|
|
165
|
+
radioDot: { width: 10, height: 10, borderRadius: 5, backgroundColor: Colors.red },
|
|
166
|
+
reasonText: { color: Colors.white, flex: 1 },
|
|
167
|
+
otherInput: {
|
|
168
|
+
marginTop: 12, color: Colors.white, fontSize: 14,
|
|
169
|
+
backgroundColor: Colors.gray2, borderRadius: 8,
|
|
170
|
+
padding: 16, minHeight: 80, textAlignVertical: 'top',
|
|
171
|
+
},
|
|
172
|
+
footer: { paddingHorizontal: 24, paddingBottom: 32, gap: 12 },
|
|
173
|
+
deleteButton: {
|
|
174
|
+
height: 56, borderRadius: 28, backgroundColor: Colors.red,
|
|
175
|
+
justifyContent: 'center', alignItems: 'center',
|
|
176
|
+
},
|
|
177
|
+
deleteButtonText: { color: Colors.white, fontSize: 16, fontWeight: '600' },
|
|
178
|
+
cancelButton: {
|
|
179
|
+
height: 48, borderRadius: 24, justifyContent: 'center', alignItems: 'center',
|
|
180
|
+
},
|
|
181
|
+
cancelButtonText: { color: Colors.gray6, fontSize: 15 },
|
|
182
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
TextInput,
|
|
6
|
+
StyleSheet,
|
|
7
|
+
Pressable,
|
|
8
|
+
Alert,
|
|
9
|
+
ScrollView,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useNavigation } from '@react-navigation/native';
|
|
12
|
+
|
|
13
|
+
import { useApiClient } from '../../SquadProvider';
|
|
14
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
15
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
16
|
+
import Button from '../../components/ux/buttons/Button';
|
|
17
|
+
import UserImage from '../../components/ux/user-image/UserImage';
|
|
18
|
+
import { BodyRegular, BodyMedium } from '../../components/ux/text/Typography';
|
|
19
|
+
import type { User } from '@squad-sports/core';
|
|
20
|
+
|
|
21
|
+
export function EditProfileScreen() {
|
|
22
|
+
const navigation = useNavigation();
|
|
23
|
+
const apiClient = useApiClient();
|
|
24
|
+
const { theme } = useTheme();
|
|
25
|
+
|
|
26
|
+
const [user, setUser] = useState<User | null>(null);
|
|
27
|
+
const [displayName, setDisplayName] = useState('');
|
|
28
|
+
const [email, setEmail] = useState('');
|
|
29
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
30
|
+
const [hasChanges, setHasChanges] = useState(false);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const load = async () => {
|
|
34
|
+
const userData = await apiClient.getLoggedInUser();
|
|
35
|
+
if (userData) {
|
|
36
|
+
setUser(userData);
|
|
37
|
+
setDisplayName(userData.displayName ?? '');
|
|
38
|
+
setEmail(userData.email ?? '');
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
load();
|
|
42
|
+
}, [apiClient]);
|
|
43
|
+
|
|
44
|
+
const handleSave = useCallback(async () => {
|
|
45
|
+
if (!user || !hasChanges) return;
|
|
46
|
+
|
|
47
|
+
setIsSaving(true);
|
|
48
|
+
try {
|
|
49
|
+
const updated = user.clone();
|
|
50
|
+
updated.displayName = displayName.trim();
|
|
51
|
+
await apiClient.updateLoggedInUser(updated);
|
|
52
|
+
navigation.goBack();
|
|
53
|
+
} catch {
|
|
54
|
+
Alert.alert('Error', 'Failed to save profile. Please try again.');
|
|
55
|
+
} finally {
|
|
56
|
+
setIsSaving(false);
|
|
57
|
+
}
|
|
58
|
+
}, [user, displayName, hasChanges, apiClient, navigation]);
|
|
59
|
+
|
|
60
|
+
const handlePickPhoto = useCallback(async () => {
|
|
61
|
+
try {
|
|
62
|
+
const ImagePicker = await import('expo-image-picker');
|
|
63
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
64
|
+
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
|
65
|
+
allowsEditing: true,
|
|
66
|
+
aspect: [1, 1],
|
|
67
|
+
quality: 0.8,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!result.canceled && result.assets[0] && user) {
|
|
71
|
+
const response = await fetch(result.assets[0].uri);
|
|
72
|
+
const blob = await response.blob();
|
|
73
|
+
const buffer = new Uint8Array(await (blob as any).arrayBuffer());
|
|
74
|
+
await apiClient.uploadUserImage(user.id ?? '', buffer, 'image/jpeg');
|
|
75
|
+
// Reload user data
|
|
76
|
+
const refreshed = await apiClient.getLoggedInUser();
|
|
77
|
+
if (refreshed) setUser(refreshed);
|
|
78
|
+
}
|
|
79
|
+
} catch {
|
|
80
|
+
Alert.alert('Error', 'Failed to update photo.');
|
|
81
|
+
}
|
|
82
|
+
}, [user, apiClient]);
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
86
|
+
<ScreenHeader
|
|
87
|
+
title="Edit Profile"
|
|
88
|
+
right={
|
|
89
|
+
hasChanges ? (
|
|
90
|
+
<Pressable onPress={handleSave} disabled={isSaving}>
|
|
91
|
+
<Text style={[styles.saveText, { color: theme.buttonColor }]}>
|
|
92
|
+
{isSaving ? '...' : 'Save'}
|
|
93
|
+
</Text>
|
|
94
|
+
</Pressable>
|
|
95
|
+
) : undefined
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
|
|
99
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
100
|
+
<Pressable onPress={handlePickPhoto} style={styles.photoSection}>
|
|
101
|
+
<UserImage
|
|
102
|
+
imageUrl={user?.imageUrl}
|
|
103
|
+
displayName={displayName}
|
|
104
|
+
size={96}
|
|
105
|
+
/>
|
|
106
|
+
<BodyMedium style={[styles.changePhotoText, { color: theme.buttonColor }]}>
|
|
107
|
+
Change Photo
|
|
108
|
+
</BodyMedium>
|
|
109
|
+
</Pressable>
|
|
110
|
+
|
|
111
|
+
<View style={styles.field}>
|
|
112
|
+
<BodyMedium style={styles.label}>Name</BodyMedium>
|
|
113
|
+
<TextInput
|
|
114
|
+
value={displayName}
|
|
115
|
+
onChangeText={(text) => {
|
|
116
|
+
setDisplayName(text);
|
|
117
|
+
setHasChanges(true);
|
|
118
|
+
}}
|
|
119
|
+
style={styles.input}
|
|
120
|
+
placeholderTextColor={Colors.gray6}
|
|
121
|
+
maxLength={30}
|
|
122
|
+
/>
|
|
123
|
+
</View>
|
|
124
|
+
|
|
125
|
+
<View style={styles.field}>
|
|
126
|
+
<BodyMedium style={styles.label}>Email</BodyMedium>
|
|
127
|
+
<BodyRegular style={styles.readOnlyValue}>{email}</BodyRegular>
|
|
128
|
+
</View>
|
|
129
|
+
|
|
130
|
+
<View style={styles.field}>
|
|
131
|
+
<BodyMedium style={styles.label}>Community</BodyMedium>
|
|
132
|
+
<BodyRegular style={styles.readOnlyValue}>
|
|
133
|
+
{user?.community ?? 'Not set'}
|
|
134
|
+
</BodyRegular>
|
|
135
|
+
</View>
|
|
136
|
+
</ScrollView>
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const styles = StyleSheet.create({
|
|
142
|
+
container: { flex: 1 },
|
|
143
|
+
content: { paddingHorizontal: 24, paddingBottom: 48 },
|
|
144
|
+
saveText: { fontSize: 16, fontWeight: '600' },
|
|
145
|
+
photoSection: { alignItems: 'center', paddingVertical: 24 },
|
|
146
|
+
changePhotoText: { marginTop: 8 },
|
|
147
|
+
field: { marginBottom: 24 },
|
|
148
|
+
label: { color: Colors.gray6, marginBottom: 8, textTransform: 'uppercase', fontSize: 12, letterSpacing: 1 },
|
|
149
|
+
input: {
|
|
150
|
+
color: Colors.white, fontSize: 16, paddingVertical: 14, paddingHorizontal: 16,
|
|
151
|
+
backgroundColor: Colors.gray2, borderRadius: 8,
|
|
152
|
+
},
|
|
153
|
+
readOnlyValue: { color: Colors.gray6, fontSize: 16, paddingVertical: 14 },
|
|
154
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Network status debug screen.
|
|
3
|
+
* Ported from squad-demo/src/screens/NetworkStatus.tsx.
|
|
4
|
+
*/
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
7
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
8
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
9
|
+
import { useNetworkStatus } from '../../realtime/NetworkMonitor';
|
|
10
|
+
import { EventProcessor } from '../../realtime/EventProcessor';
|
|
11
|
+
import { BodyRegular, BodyMedium, TitleSmall } from '../../components/ux/text/Typography';
|
|
12
|
+
|
|
13
|
+
export function NetworkStatusScreen() {
|
|
14
|
+
const { theme } = useTheme();
|
|
15
|
+
const isOnline = useNetworkStatus();
|
|
16
|
+
const sseQuality = EventProcessor.shared.getConnectionQuality();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
20
|
+
<ScreenHeader title="Network Status" />
|
|
21
|
+
|
|
22
|
+
<View style={styles.content}>
|
|
23
|
+
<View style={styles.row}>
|
|
24
|
+
<BodyRegular style={styles.label}>Internet</BodyRegular>
|
|
25
|
+
<View style={[styles.statusDot, { backgroundColor: isOnline ? Colors.green : Colors.red }]} />
|
|
26
|
+
<BodyMedium style={styles.value}>{isOnline ? 'Connected' : 'Disconnected'}</BodyMedium>
|
|
27
|
+
</View>
|
|
28
|
+
|
|
29
|
+
<View style={styles.row}>
|
|
30
|
+
<BodyRegular style={styles.label}>Real-time (SSE)</BodyRegular>
|
|
31
|
+
<View style={[styles.statusDot, {
|
|
32
|
+
backgroundColor: sseQuality === 'good' ? Colors.green : sseQuality === 'poor' ? Colors.orange1 : Colors.red,
|
|
33
|
+
}]} />
|
|
34
|
+
<BodyMedium style={styles.value}>{sseQuality}</BodyMedium>
|
|
35
|
+
</View>
|
|
36
|
+
|
|
37
|
+
<View style={styles.infoBox}>
|
|
38
|
+
<TitleSmall style={styles.infoTitle}>About</TitleSmall>
|
|
39
|
+
<BodyMedium style={styles.infoText}>
|
|
40
|
+
The Squad SDK uses Server-Sent Events (SSE) for real-time updates.
|
|
41
|
+
Messages, squad changes, and poll updates arrive automatically when connected.
|
|
42
|
+
</BodyMedium>
|
|
43
|
+
</View>
|
|
44
|
+
</View>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const styles = StyleSheet.create({
|
|
50
|
+
container: { flex: 1 },
|
|
51
|
+
content: { padding: 24 },
|
|
52
|
+
row: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: Colors.gray3 },
|
|
53
|
+
label: { color: Colors.white, flex: 1 },
|
|
54
|
+
statusDot: { width: 10, height: 10, borderRadius: 5, marginRight: 8 },
|
|
55
|
+
value: { color: Colors.gray6 },
|
|
56
|
+
infoBox: { marginTop: 32, backgroundColor: Colors.gray2, borderRadius: 12, padding: 16 },
|
|
57
|
+
infoTitle: { color: Colors.white, marginBottom: 8 },
|
|
58
|
+
infoText: { color: Colors.gray6, lineHeight: 20 },
|
|
59
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Pressable,
|
|
8
|
+
Alert,
|
|
9
|
+
Linking,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
import { useNavigation } from '@react-navigation/native';
|
|
12
|
+
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
|
13
|
+
import type { RootStackParamList } from '../../navigation/SquadNavigator';
|
|
14
|
+
|
|
15
|
+
import { useSquadSDK } from '../../SquadProvider';
|
|
16
|
+
import { useTheme, Colors } from '../../theme/ThemeContext';
|
|
17
|
+
import ScreenHeader from '../../components/ux/layout/ScreenHeader';
|
|
18
|
+
import UserImage from '../../components/ux/user-image/UserImage';
|
|
19
|
+
import { BodyRegular, TitleSmall, BodyMedium } from '../../components/ux/text/Typography';
|
|
20
|
+
import { useCurrentUser } from '../../hooks/useSquadData';
|
|
21
|
+
|
|
22
|
+
interface SettingsRowProps {
|
|
23
|
+
label: string;
|
|
24
|
+
value?: string;
|
|
25
|
+
onPress?: () => void;
|
|
26
|
+
danger?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function SettingsRow({ label, value, onPress, danger }: SettingsRowProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Pressable
|
|
32
|
+
style={styles.row}
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
disabled={!onPress}
|
|
35
|
+
>
|
|
36
|
+
<BodyRegular style={[styles.rowLabel, danger && styles.rowLabelDanger]}>
|
|
37
|
+
{label}
|
|
38
|
+
</BodyRegular>
|
|
39
|
+
{value && <BodyMedium style={styles.rowValue}>{value}</BodyMedium>}
|
|
40
|
+
{onPress && <Text style={styles.rowChevron}>{'>'}</Text>}
|
|
41
|
+
</Pressable>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function SettingsSection({ title, children }: { title: string; children: React.ReactNode }) {
|
|
46
|
+
return (
|
|
47
|
+
<View style={styles.section}>
|
|
48
|
+
<TitleSmall style={styles.sectionTitle}>{title}</TitleSmall>
|
|
49
|
+
<View style={styles.sectionContent}>{children}</View>
|
|
50
|
+
</View>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type Nav = NativeStackNavigationProp<RootStackParamList, 'Settings'>;
|
|
55
|
+
|
|
56
|
+
export function SettingsScreen() {
|
|
57
|
+
const sdk = useSquadSDK();
|
|
58
|
+
const { theme } = useTheme();
|
|
59
|
+
const { user } = useCurrentUser();
|
|
60
|
+
const navigation = useNavigation<Nav>();
|
|
61
|
+
|
|
62
|
+
const handleLogout = useCallback(() => {
|
|
63
|
+
Alert.alert(
|
|
64
|
+
'Log Out',
|
|
65
|
+
'Are you sure you want to log out?',
|
|
66
|
+
[
|
|
67
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
68
|
+
{
|
|
69
|
+
text: 'Log Out',
|
|
70
|
+
style: 'destructive',
|
|
71
|
+
onPress: async () => {
|
|
72
|
+
await sdk.sessionManager.logout();
|
|
73
|
+
// Navigator will automatically route to auth screen
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
);
|
|
78
|
+
}, [sdk]);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<View style={[styles.container, { backgroundColor: theme.screenBackground }]}>
|
|
82
|
+
<ScreenHeader title="Settings" />
|
|
83
|
+
|
|
84
|
+
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
85
|
+
{user && (
|
|
86
|
+
<View style={styles.profileSection}>
|
|
87
|
+
<UserImage
|
|
88
|
+
imageUrl={user.imageUrl}
|
|
89
|
+
displayName={user.displayName}
|
|
90
|
+
size={64}
|
|
91
|
+
/>
|
|
92
|
+
<View style={styles.profileInfo}>
|
|
93
|
+
<TitleSmall style={styles.profileName}>
|
|
94
|
+
{user.displayName}
|
|
95
|
+
</TitleSmall>
|
|
96
|
+
<BodyMedium style={styles.profileEmail}>
|
|
97
|
+
{user.email ?? user.phone ?? ''}
|
|
98
|
+
</BodyMedium>
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<SettingsSection title="Account">
|
|
104
|
+
<SettingsRow label="Edit Profile" onPress={() => navigation.navigate('EditProfile')} />
|
|
105
|
+
<SettingsRow label="Community" value={user?.community ?? ''} onPress={() => {}} />
|
|
106
|
+
<SettingsRow label="Blocked Users" onPress={() => navigation.navigate('BlockedUsers')} />
|
|
107
|
+
</SettingsSection>
|
|
108
|
+
|
|
109
|
+
<SettingsSection title="Notifications">
|
|
110
|
+
<SettingsRow label="Push Notifications" onPress={() => Linking.openSettings()} />
|
|
111
|
+
<SettingsRow label="Email Notifications" onPress={() => {}} />
|
|
112
|
+
</SettingsSection>
|
|
113
|
+
|
|
114
|
+
<SettingsSection title="About">
|
|
115
|
+
<SettingsRow label="Terms & Conditions" onPress={() => Linking.openURL('https://www.withyoursquad.com/terms')} />
|
|
116
|
+
<SettingsRow label="Privacy Policy" onPress={() => Linking.openURL('https://www.withyoursquad.com/privacy')} />
|
|
117
|
+
<SettingsRow label="SDK Version" value="0.1.0" />
|
|
118
|
+
</SettingsSection>
|
|
119
|
+
|
|
120
|
+
<SettingsSection title="">
|
|
121
|
+
<SettingsRow label="Log Out" onPress={handleLogout} danger />
|
|
122
|
+
<SettingsRow label="Delete Account" onPress={() => navigation.navigate('DeleteAccount')} danger />
|
|
123
|
+
</SettingsSection>
|
|
124
|
+
</ScrollView>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const styles = StyleSheet.create({
|
|
130
|
+
container: {
|
|
131
|
+
flex: 1,
|
|
132
|
+
},
|
|
133
|
+
scrollContent: {
|
|
134
|
+
paddingHorizontal: 24,
|
|
135
|
+
paddingBottom: 48,
|
|
136
|
+
},
|
|
137
|
+
profileSection: {
|
|
138
|
+
flexDirection: 'row',
|
|
139
|
+
alignItems: 'center',
|
|
140
|
+
paddingVertical: 20,
|
|
141
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
142
|
+
borderBottomColor: Colors.gray3,
|
|
143
|
+
marginBottom: 24,
|
|
144
|
+
},
|
|
145
|
+
profileInfo: {
|
|
146
|
+
marginLeft: 16,
|
|
147
|
+
flex: 1,
|
|
148
|
+
},
|
|
149
|
+
profileName: {
|
|
150
|
+
color: Colors.white,
|
|
151
|
+
},
|
|
152
|
+
profileEmail: {
|
|
153
|
+
color: Colors.gray6,
|
|
154
|
+
marginTop: 2,
|
|
155
|
+
},
|
|
156
|
+
section: {
|
|
157
|
+
marginBottom: 32,
|
|
158
|
+
},
|
|
159
|
+
sectionTitle: {
|
|
160
|
+
color: Colors.gray6,
|
|
161
|
+
marginBottom: 8,
|
|
162
|
+
textTransform: 'uppercase',
|
|
163
|
+
fontSize: 12,
|
|
164
|
+
letterSpacing: 1,
|
|
165
|
+
},
|
|
166
|
+
sectionContent: {
|
|
167
|
+
backgroundColor: Colors.gray2,
|
|
168
|
+
borderRadius: 12,
|
|
169
|
+
overflow: 'hidden',
|
|
170
|
+
},
|
|
171
|
+
row: {
|
|
172
|
+
flexDirection: 'row',
|
|
173
|
+
alignItems: 'center',
|
|
174
|
+
paddingVertical: 14,
|
|
175
|
+
paddingHorizontal: 16,
|
|
176
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
177
|
+
borderBottomColor: Colors.gray3,
|
|
178
|
+
},
|
|
179
|
+
rowLabel: {
|
|
180
|
+
color: Colors.white,
|
|
181
|
+
flex: 1,
|
|
182
|
+
},
|
|
183
|
+
rowLabelDanger: {
|
|
184
|
+
color: Colors.red,
|
|
185
|
+
},
|
|
186
|
+
rowValue: {
|
|
187
|
+
color: Colors.gray6,
|
|
188
|
+
marginRight: 8,
|
|
189
|
+
},
|
|
190
|
+
rowChevron: {
|
|
191
|
+
color: Colors.gray6,
|
|
192
|
+
fontSize: 14,
|
|
193
|
+
},
|
|
194
|
+
});
|