From fc1975fd74826cd0a0f499aaff98c405a6544b4c Mon Sep 17 00:00:00 2001 From: gibbyb Date: Sat, 12 Oct 2024 21:55:48 -0500 Subject: [PATCH] Relationship component really close to just working really well --- assets/fonts/SpaceMono-Regular.ttf | Bin components/auth/SignInScreen.tsx | 13 +- components/home/Relationships.tsx | 260 ++++++++++++++---- components/home/RequestRelationship.tsx | 148 ++++++++++ components/home/UserInfo.tsx | 14 +- .../securestorage/RelationshipData.tsx | 57 ++++ .../services/securestorage/UserData.tsx | 18 +- example/assets/fonts/SpaceMono-Regular.ttf | Bin example/scripts/reset-project.js | 0 package-lock.json | 18 +- package.json | 3 +- 11 files changed, 453 insertions(+), 78 deletions(-) mode change 100755 => 100644 assets/fonts/SpaceMono-Regular.ttf create mode 100644 components/home/RequestRelationship.tsx create mode 100644 components/services/securestorage/RelationshipData.tsx mode change 100755 => 100644 example/assets/fonts/SpaceMono-Regular.ttf mode change 100755 => 100644 example/scripts/reset-project.js diff --git a/assets/fonts/SpaceMono-Regular.ttf b/assets/fonts/SpaceMono-Regular.ttf old mode 100755 new mode 100644 diff --git a/components/auth/SignInScreen.tsx b/components/auth/SignInScreen.tsx index 5812d66..a597231 100644 --- a/components/auth/SignInScreen.tsx +++ b/components/auth/SignInScreen.tsx @@ -7,6 +7,16 @@ import * as Notifications from 'expo-notifications'; import Constants from 'expo-constants'; import { saveUserData } from '@/components/services/securestorage/UserData'; +type UserData = { + id: number; + appleId: string | null; + appleEmail: string | null; + fullName: string; + pfpURL: string | null; + pushToken: string; + createdAt: Date; +}; + export default function SignInScreen({ onSignIn }: { onSignIn: () => void }) { const scheme = useColorScheme() ?? 'light'; @@ -38,6 +48,7 @@ export default function SignInScreen({ onSignIn }: { onSignIn: () => void }) { 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', }, }); + console.log('checkUserResponse:', checkUserResponse); if (checkUserResponse.status === 404) { if (!credential.user || !credential.email || !credential.fullName?.givenName @@ -64,7 +75,7 @@ export default function SignInScreen({ onSignIn }: { onSignIn: () => void }) { console.error('API Error:', response.status, errorBody); throw new Error(`Failed to create user: ${response.status} ${errorBody}`); } - const userData = await response.json(); + const userData: UserData = await response.json() as UserData; await saveUserData(userData); } else if (checkUserResponse.ok) { const userData = await checkUserResponse.json(); diff --git a/components/home/Relationships.tsx b/components/home/Relationships.tsx index ed1228b..f5d7efe 100644 --- a/components/home/Relationships.tsx +++ b/components/home/Relationships.tsx @@ -1,18 +1,20 @@ import React, { useEffect, useState } from 'react'; -import { Image, StyleSheet, Alert } from 'react-native'; +import { Image, StyleSheet, AppState } from 'react-native'; import { ThemedView } from '@/components/ThemedView'; import { ThemedText } from '@/components/ThemedText'; import { getUserData } from '@/components/services/securestorage/UserData'; +import { saveRelationshipData, getRelationshipData } from '@/components/services/securestorage/RelationshipData'; import Button from '@/components/buttons/Button'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; +import RequestRelationship from '@/components/home/RequestRelationship'; -type Partner = { +type UserData = { id: number; - appleId: string; - appleEmail: string; + appleId: string | null; + appleEmail: string | null; fullName: string; - pfpURL: string; + pfpURL: string | null; pushToken: string; createdAt: Date; }; @@ -25,16 +27,9 @@ type Relationship = { createdAt: Date; }; -type RelationshipStatus = { - relationship: Relationship | null; - partner: Partner | null; -}; - -type UserData = { - fullName: string; - appleEmail: string; - appleId: string; - pfpURL: string; +type RelationshipData = { + relationship: Relationship; + partner: UserData; }; type RelationshipProps = { @@ -43,12 +38,19 @@ type RelationshipProps = { const Relationships: React.FC = ({ profilePictureUrl }) => { const scheme = useColorScheme() ?? 'light'; - const [status, setStatus] = useState(null); + const [status, setStatus] = useState(null); const [userData, setUserData] = useState(null); + const [showRequestRelationship, setShowRequestRelationship] = useState(false); const [loading, setLoading] = useState(true); + const handleRequestSent = (data: RelationshipData) => { + setStatus(data); + setShowRequestRelationship(false); + }; + useEffect(() => { fetchRelationshipStatus(); + setupPeriodicCheck(); }, []); useEffect(() => { @@ -57,15 +59,73 @@ const Relationships: React.FC = ({ profilePictureUrl }) => { } }, [profilePictureUrl]); + const setupPeriodicCheck = () => { + let intervalId: NodeJS.Timeout | null = null; + + const startChecking = () => { + checkRelationshipStatus(); + intervalId = setInterval(checkRelationshipStatus, 600000); // + }; + + const stopChecking = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + }; + + const handleAppStateChange = (nextAppState: string) => { + if (nextAppState === 'active') { + startChecking(); + } else { + stopChecking(); + } + }; + + const subscription = AppState.addEventListener('change', handleAppStateChange); + + if (AppState.currentState === 'active') { + startChecking(); + } + + return () => { + stopChecking(); + subscription.remove(); + }; + }; + const fetchRelationshipStatus = async () => { + setLoading(true); try { - const userDataFromStorage: UserData = await getUserData(); - if (!userDataFromStorage || !userDataFromStorage.appleId) { + const userDataFromStorage: UserData = await getUserData() as UserData; + if (!userDataFromStorage || !userDataFromStorage.id) { throw new Error('User data not found'); } setUserData(userDataFromStorage); - const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/relationships/checkStatusByAppleId?appleId=${userDataFromStorage.appleId}`, { + // First, try to get relationship data from SecureStore + const storedRelationshipData = await getRelationshipData(); + if (storedRelationshipData) { + setStatus(storedRelationshipData); + } + + // Then, fetch the latest data from the API + await checkRelationshipStatus(); + } catch (error) { + console.log('Error fetching relationship status:', error); + } finally { + setLoading(false); + } + }; + + const checkRelationshipStatus = async () => { + try { + const userDataFromStorage: UserData = await getUserData() as UserData; + if (!userDataFromStorage || !userDataFromStorage.id) { + throw new Error('User data not found'); + } + + const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/relationships/getRelationshipData?userId=${userDataFromStorage.id}`, { headers: { 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', }, @@ -73,62 +133,132 @@ const Relationships: React.FC = ({ profilePictureUrl }) => { if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.message || 'Failed to fetch relationship status'); + throw new Error(errorData.message || `HTTP error! status: ${response.status}`); } - - const data = await response.json(); + const data = await response.json() as RelationshipData; setStatus(data); + await saveRelationshipData(data); } catch (error) { - console.error('Error fetching relationship status:', error); + console.log('No relationship found or error checking status:', error); + setStatus(null); } finally { - setLoading(false); } }; + const handleUpdateRelationshipStatus = async (newStatus: 'accepted' | 'rejected') => { + if (!status || !status.relationship) { + console.error('No relationship to update'); + return; + } + try { + const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/relationships/updateStatus`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', + }, + body: JSON.stringify({ + relationshipId: status.relationship.id, + status: newStatus, + }), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + if (result) { + // Refresh the relationship status + await checkRelationshipStatus(); + } else { + throw new Error('Failed to update relationship status'); + } + } catch (error) { + console.error('Error updating relationship status:', error); + } + }; + + const handleAcceptRequest = () => handleUpdateRelationshipStatus('accepted'); + const handleRejectRequest = () => handleUpdateRelationshipStatus('rejected'); + if (loading) { return Loading...; } - - if (!status || !status.relationship?.id) { - return ( - - - - ); - } + ); + } else if (status.relationship.status === 'pending') { + // Case 2 & 3: Pending relationship (we can't differentiate who requested it) + return ( + <> + Pending Relationship + + + + {status.partner.fullName} + + + + + + + + ); + } else if (status.relationship.status === 'accepted') { + // Case 4: In an accepted relationship + return ( + <> + {status.relationship.title} + + {userData && ( + + + {userData.fullName.split(' ')[0]} + + )} + {status.partner && ( + + + {status.partner.fullName.split(' ')[0]} + + )} + + + ); + } + }; return ( - {status?.relationship?.title} - - {userData && ( - - - {userData.fullName.split(' ')[0]} - - )} - {status?.partner && ( - - - {status.partner.fullName.split(' ')[0]} - - )} - + {renderRelationshipContent()} ); }; @@ -164,9 +294,19 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: 'bold', }, - buttonText: { + buttonLabel: { fontSize: 16, }, + lastChecked: { + fontSize: 12, + marginBottom: 10, + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-around', + width: '100%', + marginTop: 20, + }, }); export default Relationships; diff --git a/components/home/RequestRelationship.tsx b/components/home/RequestRelationship.tsx new file mode 100644 index 0000000..4770515 --- /dev/null +++ b/components/home/RequestRelationship.tsx @@ -0,0 +1,148 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import {TextInput, FlatList, TouchableOpacity, StyleSheet, ActivityIndicator } from 'react-native'; +import { ThemedView } from '@/components/ThemedView'; +import { ThemedText } from '@/components/ThemedText'; +import { getUserData } from '@/components/services/securestorage/UserData'; +import { Colors } from '@/constants/Colors'; +import { useColorScheme } from '@/hooks/useColorScheme'; +import debounce from 'lodash.debounce'; + +type UserData = { + id: number; + appleId: string | null; + appleEmail: string | null; + fullName: string; + pfpURL: string | null; + pushToken: string; + createdAt: Date; +}; + +type RelationshipData = { + relationship: { + id: number; + title: string; + status: "pending" | "accepted" | "rejected"; + relationshipStartDate: Date; + createdAt: Date; + }; + partner: UserData; +}; + +const RequestRelationship: React.FC<{ onRequestSent: (data: RelationshipData) => void }> = ({ onRequestSent }) => { + const scheme = useColorScheme() ?? 'light'; + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [currentUserId, setCurrentUserId] = useState(null); + + useEffect(() => { + const fetchCurrentUser = async () => { + const userData = await getUserData(); + if (userData) { + setCurrentUserId(userData.id); + } + }; + fetchCurrentUser(); + }, []); + + const searchUsers = useCallback( + debounce(async (term: string) => { + if (term.length < 3) { + setSearchResults([]); + return; + } + setIsLoading(true); + try { + const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/users/getUserByName?usersName=${encodeURIComponent(term)}`, { + headers: { + 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', + }, + }); + if (!response.ok) throw new Error('Search failed'); + const data = await response.json(); + setSearchResults(data); + } catch (error) { + console.error('Error searching users:', error); + } finally { + setIsLoading(false); + } + }, 300), + [] + ); + + const handleSearch = (text: string) => { + setSearchTerm(text); + searchUsers(text); + }; + + const sendRequest = async (targetUserId: number) => { + if (!currentUserId) return; + try { + const response = await fetch(`${process.env.EXPO_PUBLIC_API_URL}/relationships/createRequest`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-api-key': process.env.EXPO_PUBLIC_API_KEY ?? '', + }, + body: JSON.stringify({ userId: currentUserId, targetUserId }), + }); + if (!response.ok) throw new Error('Request failed'); + const data: RelationshipData = await response.json(); + onRequestSent(data); + } catch (error) { + console.error('Error sending relationship request:', error); + } + }; + + const renderUserItem = ({ item }: { item: UserData }) => ( + sendRequest(item.id)}> + {item.fullName} + {item.appleEmail} + + ); + + return ( + + + {isLoading ? ( + + ) : ( + item.id.toString()} + ListEmptyComponent={No users found} + /> + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + }, + searchInput: { + height: 40, + borderColor: 'gray', + borderWidth: 1, + marginBottom: 20, + paddingHorizontal: 10, + }, + userItem: { + padding: 10, + borderBottomWidth: 1, + borderBottomColor: 'gray', + }, +}); + +export default RequestRelationship; diff --git a/components/home/UserInfo.tsx b/components/home/UserInfo.tsx index 0e3ed3a..7988f5e 100644 --- a/components/home/UserInfo.tsx +++ b/components/home/UserInfo.tsx @@ -7,11 +7,15 @@ import { manipulateAsync, SaveFormat } from 'expo-image-manipulator'; import * as ImagePicker from 'expo-image-picker'; import * as FileSystem from 'expo-file-system'; + type UserData = { + id: number; + appleId: string | null; + appleEmail: string | null; fullName: string; - appleEmail: string; - appleId: string; - pfpURL: string; + pfpURL: string | null; + pushToken: string; + createdAt: Date; }; type UserInfoProps = { @@ -24,7 +28,7 @@ const UserInfo: React.FC = ({ onProfilePictureUpdate }) => { useEffect(() => { const fetchUserData = async () => { try { - const data = await getUserData(); + const data: UserData = await getUserData() as UserData; setUserData(data); if (data.pfpURL) { onProfilePictureUpdate(data.pfpURL); @@ -90,7 +94,7 @@ const UserInfo: React.FC = ({ onProfilePictureUpdate }) => { const newPfpURL = responseData.pfpURL; // Update local state - setUserData(prevData => prevData ? {...prevData, pfpURL: newPfpURL} : null); + setUserData(prevData => prevData ? {...prevData, pfpURL: newPfpURL} as UserData : null); // Update SecureStorage await updateUserData({ pfpURL: newPfpURL }); diff --git a/components/services/securestorage/RelationshipData.tsx b/components/services/securestorage/RelationshipData.tsx new file mode 100644 index 0000000..d5adb43 --- /dev/null +++ b/components/services/securestorage/RelationshipData.tsx @@ -0,0 +1,57 @@ +import * as SecureStore from 'expo-secure-store'; + +type UserData = { + id: number; + appleId: string | null; + appleEmail: string | null; + fullName: string; + pfpURL: string | null; + pushToken: string; + createdAt: Date; +}; + +type Relationship = { + id: number; + title: string; + status: 'pending' | 'accepted' | 'rejected'; + relationshipStartDate: Date; + createdAt: Date; +}; + +type RelationshipData = { + relationship: Relationship; + partner: UserData; +}; + +export const saveRelationshipData = async (RelationshipData: RelationshipData) => { + try { + await SecureStore.setItemAsync('RelationshipData', JSON.stringify(RelationshipData)); + } catch (error) { + console.error('Error saving user data:', error); + } +}; + +export const getRelationshipData = async () => { + try { + const RelationshipData = await SecureStore.getItemAsync('RelationshipData'); + return RelationshipData ? JSON.parse(RelationshipData) : null; + } catch (error) { + console.error('Error getting user data:', error); + return null; + } +}; + +export const updateRelationshipData = async (updatedFields: Partial) => { + try { + const currentUserData: RelationshipData = await getRelationshipData() as RelationshipData; + if (currentUserData) { + const updatedUserData: RelationshipData = { ...currentUserData, ...updatedFields }; + await saveRelationshipData(updatedUserData); + return updatedUserData as RelationshipData; + } + return null; + } catch (error) { + console.error('Error updating user data:', error); + return null; + } +}; diff --git a/components/services/securestorage/UserData.tsx b/components/services/securestorage/UserData.tsx index d6a3138..cffa9f2 100644 --- a/components/services/securestorage/UserData.tsx +++ b/components/services/securestorage/UserData.tsx @@ -1,6 +1,16 @@ import * as SecureStore from 'expo-secure-store'; -export const saveUserData = async (userData: any) => { +type UserData = { + id: number; + appleId: string | null; + appleEmail: string | null; + fullName: string; + pfpURL: string | null; + pushToken: string; + createdAt: Date; +}; + +export const saveUserData = async (userData: UserData) => { try { await SecureStore.setItemAsync('userData', JSON.stringify(userData)); } catch (error) { @@ -20,11 +30,11 @@ export const getUserData = async () => { export const updateUserData = async (updatedFields: Partial) => { try { - const currentUserData = await getUserData(); + const currentUserData: UserData = await getUserData() as UserData; if (currentUserData) { - const updatedUserData = { ...currentUserData, ...updatedFields }; + const updatedUserData: UserData = { ...currentUserData, ...updatedFields }; await saveUserData(updatedUserData); - return updatedUserData; + return updatedUserData as UserData; } return null; } catch (error) { diff --git a/example/assets/fonts/SpaceMono-Regular.ttf b/example/assets/fonts/SpaceMono-Regular.ttf old mode 100755 new mode 100644 diff --git a/example/scripts/reset-project.js b/example/scripts/reset-project.js old mode 100755 new mode 100644 diff --git a/package-lock.json b/package-lock.json index afd58ab..1673067 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,12 @@ "dependencies": { "@expo/vector-icons": "^14.0.2", "@react-navigation/native": "^6.0.2", + "@types/lodash.debounce": "^4.0.9", "expo": "~51.0.28", "expo-apple-authentication": "~6.4.2", "expo-clipboard": "~6.0.3", "expo-constants": "~16.0.2", "expo-device": "~6.0.2", - "expo-fast-image": "^1.1.3", "expo-file-system": "~17.0.1", "expo-font": "~12.0.9", "expo-image": "~1.13.0", @@ -31,6 +31,7 @@ "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.3", + "lodash.debounce": "^4.0.8", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.5", @@ -7271,6 +7272,15 @@ "integrity": "sha512-YpS0zzoduEhuOWjAotS6A5AVCva7X4lVlYLF0FYHAY9sdraBfnatttHItlWeZdGhuEkf+OzMNg2ZYAx8t+52uQ==", "license": "MIT" }, + "node_modules/@types/lodash.debounce": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.9.tgz", + "integrity": "sha512-Ma5JcgTREwpLRwMM+XwBR7DaWe96nC38uCBDFKZWbNKD+osjVzdpnUSwBcqCptrp16sSOLBAUb50Car5I0TCsQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.isequal": { "version": "4.5.8", "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz", @@ -10201,12 +10211,6 @@ "expo": "*" } }, - "node_modules/expo-fast-image": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/expo-fast-image/-/expo-fast-image-1.1.3.tgz", - "integrity": "sha512-dpHvUFJxImD+yDVJk3ubCMGSYgbewPfbsPMOpD/EP7PKDA4RurtkJZcUDF8T/0gxuxZMCldPgHup6np/hrK/MQ==", - "license": "ISC" - }, "node_modules/expo-file-system": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-17.0.1.tgz", diff --git a/package.json b/package.json index d735d1c..3dd7001 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,12 @@ "dependencies": { "@expo/vector-icons": "^14.0.2", "@react-navigation/native": "^6.0.2", + "@types/lodash.debounce": "^4.0.9", "expo": "~51.0.28", "expo-apple-authentication": "~6.4.2", "expo-clipboard": "~6.0.3", "expo-constants": "~16.0.2", "expo-device": "~6.0.2", - "expo-fast-image": "^1.1.3", "expo-file-system": "~17.0.1", "expo-font": "~12.0.9", "expo-image": "~1.13.0", @@ -38,6 +38,7 @@ "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.3", + "lodash.debounce": "^4.0.8", "react": "18.2.0", "react-dom": "18.2.0", "react-native": "0.74.5",