Feature/notification system #26
19 changed files with 3037 additions and 6874 deletions
3
app.json
3
app.json
|
|
@ -26,7 +26,8 @@
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-font"
|
"expo-font",
|
||||||
|
"expo-web-browser"
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,29 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { router, Stack } from 'expo-router';
|
import { router, Stack } from 'expo-router';
|
||||||
import { GlobalVariablesProvider, WebSocketProvider } from '../contexts';
|
import { AuthProvider, GlobalVariablesProvider, WebSocketProvider } from '@/contexts';
|
||||||
import { useNotifications, useWebSocketContext } from '@/hooks';
|
import { useNotifications, useWebSocketContext } from '@/hooks';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
export const unstable_settings = {
|
|
||||||
initialRouteName: 'login',
|
|
||||||
};
|
|
||||||
|
|
||||||
function AppTest() {
|
function AppTest() {
|
||||||
const [ oldMessage, setOldMessage ] = useState(1);
|
const [ oldMessage, setOldMessage ] = useState(1);
|
||||||
|
const { user } = useAuth();
|
||||||
const { lastMessage } = useWebSocketContext();
|
const { lastMessage } = useWebSocketContext();
|
||||||
const { schedulePushNotification } = useNotifications();
|
const { schedulePushNotification } = useNotifications();
|
||||||
|
const [isReady, setIsReady] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsReady(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
router.replace('/login');
|
if (isReady) {
|
||||||
}, []);
|
if (user) {
|
||||||
|
router.replace('./incidents');
|
||||||
|
} else {
|
||||||
|
router.replace('/login');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isReady, user]);
|
||||||
|
|
||||||
const parseAddress = (data) => {
|
const parseAddress = (data) => {
|
||||||
const { Address } = data;
|
const { Address } = data;
|
||||||
|
|
@ -40,11 +49,11 @@ function AppTest() {
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
|
gestureEnabled: false,
|
||||||
|
headerBackVisible: false,
|
||||||
headerShown: false
|
headerShown: false
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<Stack.Screen name="login" />
|
|
||||||
</Stack>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +61,9 @@ export default function App() {
|
||||||
return (
|
return (
|
||||||
<GlobalVariablesProvider>
|
<GlobalVariablesProvider>
|
||||||
<WebSocketProvider>
|
<WebSocketProvider>
|
||||||
<AppTest />
|
<AuthProvider>
|
||||||
|
<AppTest />
|
||||||
|
</AuthProvider>
|
||||||
</WebSocketProvider>
|
</WebSocketProvider>
|
||||||
</GlobalVariablesProvider>
|
</GlobalVariablesProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export default function TabTwoScreen() {
|
||||||
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
|
||||||
different screen densities
|
different screen densities
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
<Image source={require('@/assets/images/tones-logo.png')} style={{ alignSelf: 'center' }} />
|
<Image source={require('../assets/images/tones-logo.png')} style={{ alignSelf: 'center' }} />
|
||||||
<ExternalLink href="https://reactnative.dev/docs/images">
|
<ExternalLink href="https://reactnative.dev/docs/images">
|
||||||
<ThemedText type="link">Learn more</ThemedText>
|
<ThemedText type="link">Learn more</ThemedText>
|
||||||
</ExternalLink>
|
</ExternalLink>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import {
|
import {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
PageFooter,
|
PageFooter,
|
||||||
} from '../components/generalHelpers.jsx';
|
} from '@/components/generalHelpers.jsx';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { AccidentAndEmergency } from "healthicons-react-native/dist/outline";
|
import { AccidentAndEmergency } from "healthicons-react-native/dist/outline";
|
||||||
import ActionSheet from 'react-native-actions-sheet';
|
import ActionSheet from 'react-native-actions-sheet';
|
||||||
|
|
|
||||||
119
app/login.jsx
119
app/login.jsx
|
|
@ -20,74 +20,73 @@ import {
|
||||||
ExtraText,
|
ExtraText,
|
||||||
TextLinkContent,
|
TextLinkContent,
|
||||||
LoginTextInput
|
LoginTextInput
|
||||||
} from '../components/generalHelpers.jsx';
|
} from '@/components/generalHelpers.jsx';
|
||||||
|
|
||||||
|
import { signInWithEmailAndPassword } from 'firebase/auth';
|
||||||
|
import { auth } from '@/contexts/firebase';
|
||||||
import { useNotifications } from '@/hooks';
|
import { useNotifications } from '@/hooks';
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const [hidePassword, setHidePassword] = useState(true);
|
const [hidePassword, setHidePassword] = useState(true);
|
||||||
const [loginButtonDisabled, setLoginButtonDisabled] = useState(true);
|
const [loginButtonDisabled, setLoginButtonDisabled] = useState(true);
|
||||||
const [auth, setAuth] = useState(false);
|
const [error, setError] = useState('');
|
||||||
const { expoPushToken } = useNotifications();
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { expoPushToken } = useNotifications();
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
number: '',
|
email: '',
|
||||||
password: ''
|
password: ''
|
||||||
},
|
},
|
||||||
onSubmit: (values) => {
|
onSubmit: async (values) => {
|
||||||
values.number = values.number.replace(/[()\-\s]/g, '');
|
setError('');
|
||||||
console.log(values);
|
setLoading(true);
|
||||||
setAuth(true);
|
try {
|
||||||
},
|
await signInWithEmailAndPassword(auth, values.email, values.password);
|
||||||
});
|
|
||||||
|
|
||||||
const formValues = formik.values;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (formValues) {
|
|
||||||
if (formValues.number.length === 14 && formValues.password) {
|
|
||||||
setLoginButtonDisabled(false);
|
|
||||||
} else {
|
|
||||||
setLoginButtonDisabled(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [formValues])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Temp for Testing
|
|
||||||
// if (!auth) {
|
|
||||||
// setTimeout(() => {
|
|
||||||
// router.replace('./incidents');
|
|
||||||
// }, 1000);
|
|
||||||
// }
|
|
||||||
if (auth) {
|
|
||||||
router.replace('./incidents');
|
router.replace('./incidents');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [auth])
|
},
|
||||||
|
});
|
||||||
return (
|
|
||||||
<React.Fragment>
|
const formValues = formik.values;
|
||||||
<PageHeader>
|
|
||||||
<View style={{ flexDirection: 'row', height: 80, alignItems: 'center' }} />
|
useEffect(() => {
|
||||||
</PageHeader>
|
if (formValues) {
|
||||||
<StyledContainer>
|
if (formValues.email && formValues.password) {
|
||||||
<StatusBar style="dark" />
|
setLoginButtonDisabled(false);
|
||||||
<SafeAreaView />
|
} else {
|
||||||
<InnerContainer>
|
setLoginButtonDisabled(true);
|
||||||
<PageImage resizeMode="cover" source={require('./../assets/images/tones-logo.png')} />
|
}
|
||||||
<Title>Tones</Title>
|
}
|
||||||
<SubTitle>Account Login</SubTitle>
|
}, [formValues]);
|
||||||
<StyledFormArea>
|
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<PageHeader>
|
||||||
|
<View style={{ flexDirection: 'row', height: 80, alignItems: 'center' }} />
|
||||||
|
</PageHeader>
|
||||||
|
<StyledContainer>
|
||||||
|
<StatusBar style="dark" />
|
||||||
|
<SafeAreaView />
|
||||||
|
<InnerContainer>
|
||||||
|
<PageImage resizeMode="cover" source={require('./../assets/images/tones-logo.png')} />
|
||||||
|
<Title>Tones</Title>
|
||||||
|
<SubTitle>Account Login</SubTitle>
|
||||||
|
<StyledFormArea>
|
||||||
|
{error ? <MessageBox style={{ color: 'red' }}>{error}</MessageBox> : null}
|
||||||
<LoginTextInput
|
<LoginTextInput
|
||||||
label="Phone Number"
|
label="Email Address"
|
||||||
icon="call-outline"
|
icon="mail-outline"
|
||||||
placeholder="123-456-7890"
|
placeholder="test@organization.com"
|
||||||
placeholderTextColor="gray"
|
placeholderTextColor="gray"
|
||||||
onChangeText={formik.handleChange('number')}
|
onChangeText={formik.handleChange('email')}
|
||||||
onBlur={formik.handleBlur('number')}
|
onBlur={formik.handleBlur('email')}
|
||||||
value={formik.values.number.replace(/^(\d{3})(\d{3})(\d+)$/, "($1) $2-$3")}
|
value={formik.values.email}
|
||||||
keyboardType="number-pad"
|
keyboardType="email-address"
|
||||||
maxLength={14}
|
|
||||||
/>
|
/>
|
||||||
<LoginTextInput
|
<LoginTextInput
|
||||||
label="Password"
|
label="Password"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { router } from 'expo-router';
|
import { router } from 'expo-router';
|
||||||
|
import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth';
|
||||||
|
import { auth } from '@/contexts/firebase';
|
||||||
|
import { useAuth } from '@/contexts/AuthContext';
|
||||||
import { View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
import { View, ScrollView, Text, TouchableOpacity } from 'react-native';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik';
|
||||||
|
|
@ -22,11 +25,12 @@ import {
|
||||||
} from '../components/generalHelpers.jsx';
|
} from '../components/generalHelpers.jsx';
|
||||||
|
|
||||||
export default function Register() {
|
export default function Register() {
|
||||||
|
|
||||||
const [providerDropdownOpen, setProviderDropdownOpen] = useState(false);
|
const [providerDropdownOpen, setProviderDropdownOpen] = useState(false);
|
||||||
const [hidePassword, setHidePassword] = useState(true);
|
const [hidePassword, setHidePassword] = useState(true);
|
||||||
const [registerButtonDisabled, setRegisterButtonDisabled] = useState(true);
|
const [registerButtonDisabled, setRegisterButtonDisabled] = useState(true);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const { user } = useAuth();
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
|
@ -34,13 +38,24 @@ export default function Register() {
|
||||||
lastName: '',
|
lastName: '',
|
||||||
number: '',
|
number: '',
|
||||||
provider: '',
|
provider: '',
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordConfirmation: ''
|
passwordConfirmation: ''
|
||||||
},
|
},
|
||||||
onSubmit: (values) => {
|
onSubmit: async (values) => {
|
||||||
values.number = values.number.replace(/[()\-\s]/g, '');
|
setError('');
|
||||||
console.log(values);
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const userCredential = await createUserWithEmailAndPassword(auth, values.email, values.password);
|
||||||
|
await updateProfile(userCredential.user, {
|
||||||
|
displayName: `${values.firstName} ${values.lastName}`
|
||||||
|
});
|
||||||
|
router.replace('./incidents');
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -48,19 +63,27 @@ export default function Register() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formValues) {
|
if (formValues) {
|
||||||
if ((formValues.number.length === 14 || (formValues.number.length === 10 && !formValues.number.includes('('))) && formValues.email && formValues.firstName && formValues.lastName) {
|
if (
|
||||||
if (formValues.password.length !== 0 && (formValues.password === formValues.passwordConfirmation)) {
|
formValues.email &&
|
||||||
setRegisterButtonDisabled(false);
|
formValues.firstName &&
|
||||||
} else {
|
formValues.lastName &&
|
||||||
setRegisterButtonDisabled(true);
|
formValues.password.length !== 0 &&
|
||||||
}
|
formValues.password === formValues.passwordConfirmation
|
||||||
|
) {
|
||||||
|
setRegisterButtonDisabled(false);
|
||||||
} else {
|
} else {
|
||||||
setRegisterButtonDisabled(true);
|
setRegisterButtonDisabled(true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setRegisterButtonDisabled(true);
|
setRegisterButtonDisabled(true);
|
||||||
}
|
}
|
||||||
}, [formValues])
|
}, [formValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (user) {
|
||||||
|
router.replace('./incidents');
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
|
@ -78,6 +101,7 @@ export default function Register() {
|
||||||
<SafeAreaView />
|
<SafeAreaView />
|
||||||
<InnerContainer>
|
<InnerContainer>
|
||||||
<PageImage resizeMode="cover" source={require('./../assets/images/tones-logo.png')} />
|
<PageImage resizeMode="cover" source={require('./../assets/images/tones-logo.png')} />
|
||||||
|
{error ? <MessageBox style={{ color: 'red' }}>{error}</MessageBox> : null}
|
||||||
<Title>Tones</Title>
|
<Title>Tones</Title>
|
||||||
<SubTitle>Account Register</SubTitle>
|
<SubTitle>Account Register</SubTitle>
|
||||||
<StyledFormArea>
|
<StyledFormArea>
|
||||||
|
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { Image, StyleSheet, Platform } from 'react-native';
|
|
||||||
|
|
||||||
import { HelloWave } from '@/components/HelloWave';
|
|
||||||
import ParallaxScrollView from '@/components/ParallaxScrollView';
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
|
||||||
return (
|
|
||||||
<ParallaxScrollView
|
|
||||||
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
|
|
||||||
headerImage={
|
|
||||||
<Image
|
|
||||||
source={require('@/assets/images/tones-logo.png')}
|
|
||||||
style={styles.reactLogo}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ThemedView style={styles.titleContainer}>
|
|
||||||
<ThemedText type="title">Welcome!</ThemedText>
|
|
||||||
<HelloWave />
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
|
|
||||||
Press{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">
|
|
||||||
{Platform.select({ ios: 'cmd + d', android: 'cmd + m' })}
|
|
||||||
</ThemedText>{' '}
|
|
||||||
to open developer tools.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
Tap the Explore tab to learn more about what's included in this starter app.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
When you're ready, run{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
When you're ready, run{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
<ThemedView style={styles.stepContainer}>
|
|
||||||
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
|
|
||||||
<ThemedText>
|
|
||||||
When you're ready, run{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
|
|
||||||
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
|
|
||||||
</ThemedText>
|
|
||||||
</ThemedView>
|
|
||||||
</ParallaxScrollView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
titleContainer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
stepContainer: {
|
|
||||||
gap: 8,
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
reactLogo: {
|
|
||||||
height: 178,
|
|
||||||
width: 290,
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
@ -2,8 +2,8 @@ import Ionicons from '@expo/vector-icons/Ionicons';
|
||||||
import { PropsWithChildren, useState } from 'react';
|
import { PropsWithChildren, useState } from 'react';
|
||||||
import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
|
import { StyleSheet, TouchableOpacity, useColorScheme } from 'react-native';
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '../components/ThemedText';
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
import { ThemedView } from '../components/ThemedView';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
|
|
||||||
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Animated, {
|
||||||
withSequence,
|
withSequence,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
import { ThemedText } from '@/components/ThemedText';
|
import { ThemedText } from '../components/ThemedText';
|
||||||
|
|
||||||
export function HelloWave() {
|
export function HelloWave() {
|
||||||
const rotationAnimation = useSharedValue(0);
|
const rotationAnimation = useSharedValue(0);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Animated, {
|
||||||
useScrollViewOffset,
|
useScrollViewOffset,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
import { ThemedView } from '@/components/ThemedView';
|
import { ThemedView } from '../components/ThemedView';
|
||||||
|
|
||||||
const HEADER_HEIGHT = 250;
|
const HEADER_HEIGHT = 250;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Text, type TextProps, StyleSheet } from 'react-native';
|
import { Text, type TextProps, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
import { useThemeColor } from '../hooks/useThemeColor';
|
||||||
|
|
||||||
export type ThemedTextProps = TextProps & {
|
export type ThemedTextProps = TextProps & {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { View, type ViewProps } from 'react-native';
|
import { View, type ViewProps } from 'react-native';
|
||||||
|
|
||||||
import { useThemeColor } from '@/hooks/useThemeColor';
|
import { useThemeColor } from '../hooks/useThemeColor';
|
||||||
|
|
||||||
export type ThemedViewProps = ViewProps & {
|
export type ThemedViewProps = ViewProps & {
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
|
|
|
||||||
30
contexts/AuthContext.js
Normal file
30
contexts/AuthContext.js
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||||
|
import { onAuthStateChanged, signOut } from 'firebase/auth';
|
||||||
|
import { auth } from './firebase';
|
||||||
|
|
||||||
|
const AuthContext = createContext();
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
|
||||||
|
setUser(firebaseUser);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const logout = () => signOut(auth);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={{ user, loading, logout }}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
return useContext(AuthContext);
|
||||||
|
};
|
||||||
19
contexts/firebase.js
Normal file
19
contexts/firebase.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { initializeApp } from 'firebase/app';
|
||||||
|
import { initializeAuth, getReactNativePersistence } from 'firebase/auth';
|
||||||
|
import ReactNativeAsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY,
|
||||||
|
authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
||||||
|
projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID,
|
||||||
|
storageBucket: process.env.EXPO_PUBLIC_FIREBASE_STORAGE_BUCKET,
|
||||||
|
messagingSenderId: process.env.EXPO_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
|
||||||
|
appId: process.env.EXPO_PUBLIC_FIREBASE_APP_ID,
|
||||||
|
};
|
||||||
|
|
||||||
|
const app = initializeApp(firebaseConfig);
|
||||||
|
const auth = initializeAuth(app, {
|
||||||
|
persistence: getReactNativePersistence(ReactNativeAsyncStorage)
|
||||||
|
});
|
||||||
|
|
||||||
|
export { auth };
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
|
export { AuthProvider } from './AuthContext';
|
||||||
export { GlobalVariablesProvider, GlobalVariablesContext } from './GlobalVariablesContext';
|
export { GlobalVariablesProvider, GlobalVariablesContext } from './GlobalVariablesContext';
|
||||||
export { WebSocketProvider, WebSocketContext } from './WebSocketContext';
|
export { WebSocketProvider, WebSocketContext } from './WebSocketContext';
|
||||||
|
|
@ -39,7 +39,7 @@ const registerForPushNotificationsAsync = async () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.isDevice) {
|
// if (Device.isDevice) {
|
||||||
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
const { status: existingStatus } = await Notifications.getPermissionsAsync();
|
||||||
let finalStatus = existingStatus;
|
let finalStatus = existingStatus;
|
||||||
if (existingStatus !== 'granted') {
|
if (existingStatus !== 'granted') {
|
||||||
|
|
@ -67,9 +67,9 @@ const registerForPushNotificationsAsync = async () => {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
token = `${e}`;
|
token = `${e}`;
|
||||||
}
|
}
|
||||||
} else {
|
// } else {
|
||||||
alert('Must use physical device for Push Notifications');
|
// alert('Must use physical device for Push Notifications');
|
||||||
}
|
// }
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
@ -96,10 +96,8 @@ export const useNotifications = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
notificationListener.current &&
|
notificationListener.current?.remove();
|
||||||
Notifications.removeNotificationSubscription(notificationListener.current);
|
responseListener.current?.remove();
|
||||||
responseListener.current &&
|
|
||||||
Notifications.removeNotificationSubscription(responseListener.current);
|
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
9433
package-lock.json
generated
9433
package-lock.json
generated
File diff suppressed because it is too large
Load diff
59
package.json
59
package.json
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "tones",
|
"name": "tones",
|
||||||
"main": "expo-router/entry",
|
"main": "expo-router/entry",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
|
"push-server": "node scripts/expo-push-server.js",
|
||||||
"reset-project": "node ./scripts/reset-project.js",
|
"reset-project": "node ./scripts/reset-project.js",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
"ios": "expo run:ios",
|
"ios": "expo run:ios",
|
||||||
|
|
@ -15,44 +16,42 @@
|
||||||
"preset": "jest-expo"
|
"preset": "jest-expo"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.28.2",
|
||||||
|
"@emotion/unitless": "^0.10.0",
|
||||||
"@expo/vector-icons": "^14.0.2",
|
"@expo/vector-icons": "^14.0.2",
|
||||||
"@react-navigation/native": "^6.0.2",
|
"@react-native-async-storage/async-storage": "^1.24.0",
|
||||||
"expo": "^52.0.46",
|
"@react-navigation/native": "^7.1.17",
|
||||||
"expo-constants": "~17.0.8",
|
"expo": "^53.0.20",
|
||||||
|
"expo-constants": "~17.1.7",
|
||||||
"expo-device": "^7.0.3",
|
"expo-device": "^7.0.3",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.3.2",
|
||||||
"expo-linking": "~7.0.5",
|
"expo-linking": "~7.1.7",
|
||||||
"expo-notifications": "^0.29.14",
|
"expo-notifications": "~0.31.4",
|
||||||
"expo-router": "~4.0.20",
|
"expo-router": "~5.1.4",
|
||||||
"expo-splash-screen": "~0.29.24",
|
"expo-splash-screen": "~0.30.10",
|
||||||
"expo-status-bar": "~2.0.1",
|
"expo-status-bar": "~2.2.3",
|
||||||
"expo-system-ui": "~4.0.9",
|
"expo-system-ui": "~5.0.10",
|
||||||
"expo-web-browser": "~14.0.2",
|
"expo-web-browser": "~14.2.0",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"firebase": "^12.1.0",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"healthicons-react-native": "^3.0.0",
|
"healthicons-react-native": "^3.5.0",
|
||||||
"react": "18.3.1",
|
"react": "19.0.0",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.79.5",
|
||||||
"react-native-actions-sheet": "^0.9.7",
|
"react-native-actions-sheet": "^0.9.7",
|
||||||
"react-native-dropdown-picker": "^5.4.6",
|
"react-native-dropdown-picker": "^5.4.6",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.11.1",
|
||||||
"react-native-svg": "15.8.0",
|
"react-native-svg": "15.11.2",
|
||||||
"react-native-textinput-effects": "^0.6.3",
|
"react-native-textinput-effects": "^0.6.3",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "^0.20.0",
|
||||||
"styled-components": "^6.1.12"
|
"styled-components": "^6.1.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.20.0",
|
"typescript": "~5.8.3"
|
||||||
"@types/jest": "^29.5.12",
|
|
||||||
"@types/react": "~18.3.12",
|
|
||||||
"@types/react-test-renderer": "^18.0.7",
|
|
||||||
"jest": "^29.2.1",
|
|
||||||
"jest-expo": "~52.0.6",
|
|
||||||
"react-test-renderer": "18.2.0",
|
|
||||||
"typescript": "~5.3.3"
|
|
||||||
},
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
39
scripts/expo-push-server.js
Normal file
39
scripts/expo-push-server.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
const express = require('express');
|
||||||
|
const fetch = require('node-fetch');
|
||||||
|
const bodyParser = require('body-parser');
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
app.use(bodyParser.json());
|
||||||
|
|
||||||
|
app.post('/send-notification', async (req, res) => {
|
||||||
|
const { expoPushToken, title, body, data } = req.body;
|
||||||
|
if (!expoPushToken) {
|
||||||
|
return res.status(400).json({ error: 'expoPushToken is required' });
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch('https://exp.host/--/api/v2/push/send', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Accept-Encoding': 'gzip, deflate',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
to: expoPushToken,
|
||||||
|
sound: 'default',
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
data,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await response.json();
|
||||||
|
res.json(result);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3001;
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Notification server running on port ${PORT}`);
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue