diff --git a/.env.example b/.env.example index 7f48820..c3123a6 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ POSTGRES_NAME= POSTGRES_HOST= DATABASE_URL= JWT_SECRET= +API_URL= \ No newline at end of file diff --git a/back/src/main.ts b/back/src/main.ts index f9d8c18..354fbed 100644 --- a/back/src/main.ts +++ b/back/src/main.ts @@ -18,6 +18,7 @@ async function bootstrap() { SwaggerModule.setup('api', app, document); app.useGlobalPipes(new ValidationPipe()); + app.enableCors(); await app.listen(3000); } bootstrap(); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 225affa..1036960 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,7 +10,8 @@ services: - ./back:/app - ./musics:/musics depends_on: - - "db" + db: + condition: service_healthy env_file: - .env @@ -21,5 +22,10 @@ services: - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} - POSTGRES_DB=${POSTGRES_DB} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 ports: - "5432:5432" diff --git a/docker-compose.yml b/docker-compose.yml index 461d3e5..3ff70f7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,8 @@ services: ports: - "3000:3000" depends_on: - - "db" + db: + condition: service_healthy env_file: - .env volumes: @@ -18,9 +19,20 @@ services: - POSTGRES_DB=${POSTGRES_DB} ports: - "5432:5432" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 5s + timeout: 5s + retries: 5 + front: - build: ./front + build: + context: ./front + args: + - API_URL=${API_URL} ports: - - "8080:80" + - "80:80" depends_on: - "back" + env_file: + - .env diff --git a/front/API.ts b/front/API.ts index edc4c18..29b88a6 100644 --- a/front/API.ts +++ b/front/API.ts @@ -1,3 +1,4 @@ +import Artist from "./models/Artist"; import AuthToken from "./models/AuthToken"; import Chapter from "./models/Chapter"; import Lesson from "./models/Lesson"; @@ -5,51 +6,93 @@ import LessonHistory from "./models/LessonHistory"; import Song from "./models/Song"; import SongHistory from "./models/SongHistory"; import User from "./models/User"; -import { translate } from "./i18n/i18n"; +import Constants from 'expo-constants'; +import store from "./state/Store"; -const delay = (seconds: number) => new Promise(resolve => setTimeout(resolve, seconds * 1000)); +type AuthenticationInput = { username: string, password: string }; +type RegistrationInput = AuthenticationInput & { email: string }; +export type AccessToken = string; -declare type AuthenticationInput = { email: string, password: string }; +type FetchParams = { + route: string; + body?: Object; + method?: 'GET' | 'POST' | 'DELETE' +} + +const dummyIllustration = "https://i.discogs.com/syRCX8NaLwK2SMk8X6TVU_DWc8RRqE4b-tebAQ6kVH4/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTgyNTQz/OC0xNjE3ODE0NDI2/LTU1MjUuanBlZw.jpeg"; export default class API { + private static async fetch(params: FetchParams) { + const jwtToken = store.getState().user.accessToken; + const header = { + 'Content-Type': 'application/json' + } + const response = await fetch(`${Constants.manifest?.extra?.apiUrl}${params.route}`, { + headers: jwtToken && { ...header, 'Authorization': `Bearer ${jwtToken}` } || header, + body: JSON.stringify(params.body), + method: params.method ?? 'GET' + }); + const body = await response.text(); + try { + const jsonResponse = body.length != 0 ? JSON.parse(body) : {}; + if (!response.ok) { + throw new Error(jsonResponse.error ?? response.statusText) + } + return jsonResponse; + } catch (e) { + if (e instanceof SyntaxError) + throw new Error("Error while parsing Server's response"); + throw e; + } + } + + public static async authenticate(authenticationInput: AuthenticationInput): Promise { + return API.fetch({ + route: '/auth/login', + body: authenticationInput, + method: 'POST' + }).then((responseBody) => responseBody.access_token) + } + /** + * Create a new user profile, with an email and a password + * @param registrationInput the credentials to create a new profile + * @returns A Promise. On success, will be resolved into an instance of the API wrapper + */ + public static async createAccount(registrationInput: RegistrationInput): Promise { + await API.fetch({ + route: '/auth/register', + body: registrationInput, + method: 'POST' + }); + return API.authenticate({ username: registrationInput.username, password: registrationInput.password }); + } + /*** * Retrieve information of the currently authentified user */ - static async getUserInfo(): Promise { + public static async getUserInfo(): Promise { + return API.fetch({ + route: '/auth/me' + }); + } + + public static async getUserSkills() { return { - name: "User", - email: "user@chromacase.com", - xp: 2345, - premium: false, - metrics: {}, - settings: {}, - id: 1 + pedalsCompetency: Math.random() * 100, + rightHandCompetency: Math.random() * 100, + leftHandCompetency: Math.random() * 100, + accuracyCompetency: Math.random() * 100, + arpegeCompetency: Math.random() * 100, + chordsCompetency: Math.random() * 100, } } - /** - * Logs the user in, with an email and a password - * @param _credentials the credentials to get an authentication token - * @returns an authentication token, that must be used for authentified requests - */ - static async login(_credentials: AuthenticationInput): Promise { - return "12345"; - } - - /** - * Create a new user profile, with an email and a password - * @param _credentials the credentials to create a new profile - * @returns an empty promise. On error, the promise will not be resolved - */ - static async register(_credentials: AuthenticationInput): Promise { - return; - } - /** * Authentify a new user through Google */ - static async authWithGoogle(): Promise { + public static async authWithGoogle(): Promise { + //TODO return "11111"; } @@ -57,22 +100,27 @@ export default class API { * Retrive a song * @param songId the id to find the song */ - static async getSong(songId: number): Promise { - return delay(1).then(() => ({ - title: "Song", - description: "A very very very very very very very very very very very very very very very very very very very very very very very very good song", - album: "Album", - metrics: {}, - id: songId - })); - + public static async getSong(songId: number): Promise { + return API.fetch({ + route: `/song/${songId}` + }); } + /** + * Retrive an artist + */ + public static async getArtist(artistId: number): Promise { + return API.fetch({ + route: `/artist/${artistId}` + }); + } + + /** * Retrive a song's chapters * @param songId the id to find the song */ - static async getSongChapters(songId: number): Promise { + public static async getSongChapters(songId: number): Promise { return [1, 2, 3, 4, 5].map((value) => ({ start: 100 * (value - 1), end: 100 * value, @@ -89,7 +137,7 @@ export default class API { * Retrieve a song's play history * @param songId the id to find the song */ - static async getSongHistory(songId: number): Promise { + public static async getSongHistory(songId: number): Promise { return [6, 1, 2, 3, 4, 5].map((value) => ({ songId: songId, userId: 1, @@ -101,21 +149,23 @@ export default class API { * Search a song by its name * @param query the string used to find the songs */ - static async searchSongs(query: string): Promise { - return [{ - title: "Song", - description: "A song", - album: "Album", - metrics: {}, - id: 1 - }]; + public static async searchSongs(query: string): Promise { + return Array.of(4).map((i) => ({ + id: i, + name: `Searched Song ${i}`, + artistId: i, + genreId: i, + albumId: i, + cover: dummyIllustration, + metrics: {} + })); } /** * Retrieve a lesson * @param lessonId the id to find the lesson */ - static async getLesson(lessonId: number): Promise { + public static async getLesson(lessonId: number): Promise { return { title: "Song", description: "A song", @@ -125,43 +175,60 @@ export default class API { }; } + /** + * Retrieve the authenticated user's search history + * @param lessonId the id to find the lesson + */ + public static async getSearchHistory(): Promise { + return Array.of(4).map((i) => ({ + id: i, + name: `Song in history ${i}`, + artistId: i, + genreId: i, + albumId: i, + cover: dummyIllustration, + metrics: {} + })); + } + + /** + * Retrieve the authenticated user's recommendations + */ + public static async getUserRecommendations(): Promise { + return Array.of(4).map((i) => ({ + id: i, + name: `Recommended Song ${i}`, + artistId: i, + genreId: i, + albumId: i, + cover: dummyIllustration, + metrics: {} + })); + } + + /** + * Retrieve the authenticated user's play history + */ + public static async getUserPlayHistory(): Promise { + return Array.of(4).map((i) => ({ + id: i, + name: `played Song ${i}`, + artistId: i, + genreId: i, + albumId: i, + cover: dummyIllustration, + metrics: {} + })); + } + /** * Retrieve a lesson's history * @param lessonId the id to find the lesson */ - static async getLessonHistory(lessonId: number): Promise { + public static async getLessonHistory(lessonId: number): Promise { return [{ lessonId, userId: 1 }]; } - - /** - * Get the login information status - * - */ - static async checkSigninCredentials(username: string, password: string): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (username === "katerina" && password === "1234") { - return resolve("token signin"); - } - return reject(translate("invalidCredentials")); - }, 1000); - }); - }; - - /** - * Get the register information status - */ - static async checkSignupCredentials(username: string, password: string, email: string): Promise { - return new Promise((resolve, reject) => { - setTimeout(() => { - if (username === "bluub") { - return reject(translate("usernameTaken")); - } - return resolve("token signup"); - }, 1000); - }); - } } \ No newline at end of file diff --git a/front/App.tsx b/front/App.tsx index 46a8bd2..d67f5f8 100644 --- a/front/App.tsx +++ b/front/App.tsx @@ -3,20 +3,31 @@ import Theme from './Theme'; import React from 'react'; import { QueryClient, QueryClientProvider } from 'react-query'; import { Provider } from 'react-redux'; -import store from './state/Store'; +import store, { persistor } from './state/Store'; import { Router } from './Navigation'; import './i18n/i18n'; +import * as SplashScreen from 'expo-splash-screen'; +import { PersistGate } from "redux-persist/integration/react"; +import LanguageGate from "./i18n/LanguageGate"; const queryClient = new QueryClient(); export default function App() { + + SplashScreen.preventAutoHideAsync(); + setTimeout(SplashScreen.hideAsync, 500); + return ( - - - - - + + + + + + + + + ); } diff --git a/front/Dockerfile b/front/Dockerfile index df325fd..ae0fb16 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -10,6 +10,9 @@ RUN yarn global add expo-cli # add sharp-cli (^2.1.0) for faster image processing RUN yarn global add sharp-cli@^2.1.0 COPY . . +ARG API_URL +ENV API_URL=$API_URL + RUN expo build:web # Serve the app diff --git a/front/Navigation.tsx b/front/Navigation.tsx index 3292f76..d267777 100644 --- a/front/Navigation.tsx +++ b/front/Navigation.tsx @@ -8,6 +8,8 @@ import { NavigationContainer } from '@react-navigation/native'; import { useSelector } from './state/Store'; import SongLobbyView from './views/SongLobbyView'; import { translate } from './i18n/i18n'; +import { useQuery } from 'react-query'; +import API from './API'; const Stack = createNativeStackNavigator(); @@ -23,12 +25,20 @@ export const publicRoutes = ; export const Router = () => { - const isAuthentified = useSelector((state) => state.user.token !== undefined) + const isAuthentified = useSelector((state) => state.user.accessToken !== undefined); + const userProfile = useQuery(['user', 'me'], () => API.getUserInfo(), { + enabled: isAuthentified + }); return ( - - {isAuthentified ? protectedRoutes : publicRoutes} - + {isAuthentified && !userProfile.isError + ? + {protectedRoutes} + + : + {publicRoutes} + + } ) } diff --git a/front/app.config.ts b/front/app.config.ts new file mode 100644 index 0000000..5bf8e63 --- /dev/null +++ b/front/app.config.ts @@ -0,0 +1,40 @@ +module.exports = { + "name": "Chromacase", + "slug": "Chromacase", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splashLogo.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "updates": { + "fallbackToCacheTimeout": 0 + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#FFFFFF", + "package": "com.chromacase.chromacase", + "versionCode": 1 + }, + "package": "build.apk" + }, + "web": { + "favicon": "./assets/favicon.png" + }, + "extra": { + apiUrl: process.env.API_URL, + "eas": { + "projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2" + } + } +} diff --git a/front/app.json b/front/app.json index 53f6efb..b4c10ac 100644 --- a/front/app.json +++ b/front/app.json @@ -7,8 +7,8 @@ "icon": "./assets/icon.png", "userInterfaceStyle": "light", "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", + "image": "./assets/splashLogo.png", + "resizeMode": "cover", "backgroundColor": "#ffffff" }, "updates": { diff --git a/front/assets/splashLogo.png b/front/assets/splashLogo.png new file mode 100644 index 0000000..a7205cb Binary files /dev/null and b/front/assets/splashLogo.png differ diff --git a/front/components/SearchBar.tsx b/front/components/SearchBar.tsx index 2429814..cdf7159 100644 --- a/front/components/SearchBar.tsx +++ b/front/components/SearchBar.tsx @@ -11,7 +11,7 @@ import { Square, } from "native-base"; import React from "react"; -import { Ionicons, FontAwesome } from "@expo/vector-icons"; +import { Ionicons } from "@expo/vector-icons"; export enum SuggestionType { TEXT, diff --git a/front/components/SearchBarSuggestions.tsx b/front/components/SearchBarSuggestions.tsx index 3772a95..9818dd8 100644 --- a/front/components/SearchBarSuggestions.tsx +++ b/front/components/SearchBarSuggestions.tsx @@ -1,5 +1,5 @@ import React from "react"; -import SearchBar from "../components/SearchBar"; +import SearchBar, { IllustratedSuggestionProps } from "../components/SearchBar"; import { SuggestionList, SuggestionType } from "../components/SearchBar"; interface SearchBarSuggestionsProps { onTextSubmit: (text: string) => void; @@ -15,7 +15,7 @@ const filterSuggestions = (text: string, suggestions: SuggestionList) => { case SuggestionType.ILLUSTRATED: return ( suggestion.data.text.toLowerCase().includes(text.toLowerCase()) || - suggestion.data.subtext.toLowerCase().includes(text.toLowerCase()) + (suggestion.data as IllustratedSuggestionProps).subtext.toLowerCase().includes(text.toLowerCase()) ); } }); diff --git a/front/components/SongCard.tsx b/front/components/SongCard.tsx index 3216fa0..c128b52 100644 --- a/front/components/SongCard.tsx +++ b/front/components/SongCard.tsx @@ -22,7 +22,7 @@ const SongCard = (props: SongCardProps) => { > {[props.songTitle, diff --git a/front/components/SongCardGrid.tsx b/front/components/SongCardGrid.tsx index 89ffeb7..7f816ea 100644 --- a/front/components/SongCardGrid.tsx +++ b/front/components/SongCardGrid.tsx @@ -6,7 +6,7 @@ import { Heading, VStack } from 'native-base'; type SongCardGrid = { songs: Parameters[0][]; maxItemPerRow?: number, - heading?: string + heading?: JSX.Element, } const SongCardGrid = (props: SongCardGrid) => { diff --git a/front/components/Translate.tsx b/front/components/Translate.tsx new file mode 100644 index 0000000..ef6da7c --- /dev/null +++ b/front/components/Translate.tsx @@ -0,0 +1,20 @@ +import { translate } from "../i18n/i18n"; +import { en } from "../i18n/Translations"; +import { RootState, useSelector } from "../state/Store"; + +type TranslateProps = { + translationKey: keyof typeof en; + format?: (translated: string) => string; +} +/** + * Translation component + * @param param0 + * @returns + */ +const Translate = ({ translationKey, format }: TranslateProps) => { + const selectedLanguage = useSelector((state: RootState) => state.language.value); + const translated = translate(translationKey, selectedLanguage); + return <>{format ? format(translated) : translated}; +} + +export default Translate; \ No newline at end of file diff --git a/front/components/forms/signinform.tsx b/front/components/forms/signinform.tsx index 88d3db5..1b3005a 100644 --- a/front/components/forms/signinform.tsx +++ b/front/components/forms/signinform.tsx @@ -1,7 +1,7 @@ // a form for sign in import React from "react"; -import { translate } from "../../i18n/i18n"; +import { Translate, translate } from "../../i18n/i18n"; import { string } from "yup"; import { FormControl, @@ -51,7 +51,9 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => { formData.password.error !== null } > - {translate("username")} + + + { > {formData.username.error} - {translate("password")} + + + { formData.username.value, formData.password.value ); - toast.show({ description: resp, colorScheme: 'secondary' }) + toast.show({ description: resp, colorScheme: 'secondary' }); + setSubmittingForm(false); } catch (e) { toast.show({ description: e as string, colorScheme: 'red', avoidKeyboard: true }) - } finally { setSubmittingForm(false); } }} > - {translate("login")} + diff --git a/front/components/forms/signupform.tsx b/front/components/forms/signupform.tsx index 146e23a..2b1c0db 100644 --- a/front/components/forms/signupform.tsx +++ b/front/components/forms/signupform.tsx @@ -1,7 +1,7 @@ // a form for sign up import React from "react"; -import { translate } from "../../i18n/i18n"; +import { Translate, translate } from "../../i18n/i18n"; import { string } from "yup"; import { FormControl, @@ -52,7 +52,7 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => { .min(4, translate("passwordTooShort")) .max(100, translate("passwordTooLong")) .matches( - /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/, + /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/, translate( "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" ) @@ -74,7 +74,9 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => { formData.email.error !== null } > - {translate("username")} + + + { > {formData.username.error} - {translate("email")} + + + { > {formData.email.error} - {translate("password")} + + + { > {formData.password.error} - {translate("repeatPassword")} + + + { formData.email.value ); toast.show({ description: resp }); + setSubmittingForm(false); } catch (e) { toast.show({ description: e as string }); - } finally { setSubmittingForm(false); } }} > - {translate("signUp")} + diff --git a/front/i18n/LanguageGate.ts b/front/i18n/LanguageGate.ts new file mode 100644 index 0000000..12f9d4e --- /dev/null +++ b/front/i18n/LanguageGate.ts @@ -0,0 +1,18 @@ +import { RootState, useSelector } from "../state/Store"; +import i18n from "./i18n"; + +type LanguageGateProps = { + children: any; +} + +/** + * Gate to handle language update at startup and on every dispatch + * @param props the children to render + */ +const LanguageGate = (props: LanguageGateProps) => { + const language = useSelector((state: RootState) => state.language.value); + i18n.changeLanguage(language); + return props.children; +} + +export default LanguageGate; \ No newline at end of file diff --git a/front/i18n/i18n.ts b/front/i18n/i18n.ts index 132ca3d..6879540 100644 --- a/front/i18n/i18n.ts +++ b/front/i18n/i18n.ts @@ -1,7 +1,7 @@ import { en, fr, sp } from './Translations'; import i18n from "i18next"; import { initReactI18next } from "react-i18next"; - +import Translate from '../components/Translate'; export type AvailableLanguages = 'en' | 'fr' | 'sp'; export const DefaultLanguage: AvailableLanguages = 'en'; @@ -9,6 +9,7 @@ export const DefaultLanguage: AvailableLanguages = 'en'; i18n .use(initReactI18next) .init({ + compatibilityJSON: 'v3', resources: { en: { translation: en @@ -28,9 +29,16 @@ i18n }); export default i18n; + /** * Typesafe translation method * @param textKey the key of th text to translate * @returns the translated text */ -export const translate = (textKey: keyof typeof en) => i18n.t(textKey); \ No newline at end of file +export const translate = (key: keyof typeof en, language?: AvailableLanguages) => { + return i18n.t(key, { + lng: language + }); +} + +export { Translate }; \ No newline at end of file diff --git a/front/models/Song.ts b/front/models/Song.ts index 009eedf..11b30d7 100644 --- a/front/models/Song.ts +++ b/front/models/Song.ts @@ -2,9 +2,11 @@ import Metrics from "./Metrics"; import Model from "./Model"; interface Song extends Model { - title: string; - description: string; - album: string; + name: string + artistId: number | null + albumId: number | null + genreId: number | null; + cover: string; metrics: Metrics; } diff --git a/front/package.json b/front/package.json index cbfe5fa..c3f7201 100644 --- a/front/package.json +++ b/front/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@expo/vector-icons": "^13.0.0", + "@react-native-async-storage/async-storage": "^1.17.11", "@react-navigation/native": "^6.0.11", "@react-navigation/native-stack": "^6.7.0", "@reduxjs/toolkit": "^1.8.3", @@ -25,6 +26,8 @@ "expo": "~45.0.0", "expo-asset": "~8.5.0", "expo-dev-client": "~1.0.0", + "expo-splash-screen": "~0.15.1", + "expo-secure-store": "~11.2.0", "expo-status-bar": "~1.3.0", "format-duration": "^2.0.0", "i18next": "^21.8.16", @@ -43,13 +46,17 @@ "react-native-testing-library": "^6.0.0", "react-native-web": "0.17.7", "react-redux": "^8.0.2", + "redux-persist": "^6.0.0", "yup": "^0.32.11" }, "devDependencies": { "@babel/core": "^7.12.9", "@testing-library/react-native": "^11.0.0", + "@types/node": "^18.11.8", "@types/react": "^18.0.18", "@types/react-native": "^0.69.6", + "@types/react-navigation": "^3.4.0", + "babel-plugin-transform-inline-environment-variables": "^0.4.4", "react-test-renderer": "17.0.2", "typescript": "~4.3.5" }, diff --git a/front/state/LanguageSlice.ts b/front/state/LanguageSlice.ts index 1f1b571..9ecbaa7 100644 --- a/front/state/LanguageSlice.ts +++ b/front/state/LanguageSlice.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import i18n, { AvailableLanguages, DefaultLanguage } from "../i18n/i18n"; +import { AvailableLanguages, DefaultLanguage } from "../i18n/i18n"; export const languageSlice = createSlice({ @@ -10,11 +10,9 @@ export const languageSlice = createSlice({ reducers: { useLanguage: (state, action: PayloadAction) => { state.value = action.payload; - i18n.changeLanguage(state.value); }, resetLanguage: (state) => { state.value = DefaultLanguage; - i18n.changeLanguage(DefaultLanguage); }, }, }); diff --git a/front/state/SettingsSlice.ts b/front/state/SettingsSlice.ts new file mode 100644 index 0000000..a49f2ea --- /dev/null +++ b/front/state/SettingsSlice.ts @@ -0,0 +1,36 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export type SettingsState = { + colorScheme: "dark" | "light" | "system", + enablePushNotifications: boolean, + enableMailNotifications: boolean, + enableLessongsReminders: boolean, + enableReleaseAlerts: boolean, + preferedLevel: 'easy' | 'medium' | 'hard', + colorBlind: boolean, + micLevel: number, + preferedInputName?: string +} + +export const settingsSlice = createSlice({ + name: 'settings', + initialState: { + settings: { + enablePushNotifications: true, + enableMailNotifications: true, + enableLessongsReminders: true, + enableReleaseAlerts: true, + preferedLevel: 'easy', + colorBlind: false, + micLevel: 50, + colorScheme: "system" + }, + }, + reducers: { + updateSettings: (state, action: PayloadAction>) => { + state.settings = { ...state.settings, ...action.payload }; + } + } +}); +export const { updateSettings } = settingsSlice.actions; +export default settingsSlice.reducer; \ No newline at end of file diff --git a/front/state/Store.ts b/front/state/Store.ts index 9a2d390..3af6bdd 100644 --- a/front/state/Store.ts +++ b/front/state/Store.ts @@ -1,14 +1,30 @@ import userReducer from '../state/UserSlice'; +import settingsReduder from '../state/SettingsSlice'; import { configureStore } from '@reduxjs/toolkit'; import languageReducer from './LanguageSlice'; import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux' +import { persistStore, persistCombineReducers, FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from "redux-persist"; +import AsyncStorage from '@react-native-async-storage/async-storage'; -const store = configureStore({ - reducer: { +const persistConfig = { + key: 'root', + storage: AsyncStorage +} + +let store = configureStore({ + reducer: persistCombineReducers(persistConfig, { user: userReducer, - language: languageReducer - }, + language: languageReducer, + settings: settingsReduder + }), + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: { + ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], + }, + }), }) +let persistor = persistStore(store); // Infer the `RootState` and `AppDispatch` types from the store itself export type RootState = ReturnType @@ -18,4 +34,5 @@ export type AppDispatch = typeof store.dispatch; export const useDispatch: () => AppDispatch = reduxDispatch export const useSelector: TypedUseSelectorHook = reduxSelector -export default store \ No newline at end of file +export default store +export { persistor } diff --git a/front/state/UserSlice.ts b/front/state/UserSlice.ts index a7904f7..44cbc25 100644 --- a/front/state/UserSlice.ts +++ b/front/state/UserSlice.ts @@ -1,19 +1,19 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import AuthToken from '../models/AuthToken'; +import { AccessToken } from '../API'; export const userSlice = createSlice({ name: 'user', initialState: { - token: undefined as AuthToken | undefined + accessToken: undefined as AccessToken | undefined }, reducers: { - setUserToken: (state, action: PayloadAction) => { - state.token = action.payload; + setAccessToken: (state, action: PayloadAction) => { + state.accessToken = action.payload; }, - unsetUserToken: (state) => { - state.token = undefined; + unsetAccessToken: (state) => { + state.accessToken = undefined; }, }, }); -export const { setUserToken, unsetUserToken } = userSlice.actions; +export const { setAccessToken, unsetAccessToken } = userSlice.actions; export default userSlice.reducer; \ No newline at end of file diff --git a/front/tsconfig.json b/front/tsconfig.json index ed7f7e9..db05a4e 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -32,7 +32,7 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - "types": ["react-native", "jest"], /* Specify type package names to be included without being referenced in a source file. */ + "types": ["react-native", "jest", "node"], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ "resolveJsonModule": true, /* Enable importing .json files */ // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ diff --git a/front/views/AuthenticationView.tsx b/front/views/AuthenticationView.tsx index 9f2e1e3..332cbf5 100644 --- a/front/views/AuthenticationView.tsx +++ b/front/views/AuthenticationView.tsx @@ -1,8 +1,8 @@ import React from "react"; import { useDispatch } from '../state/Store'; -import { translate } from "../i18n/i18n"; +import { Translate, translate } from "../i18n/i18n"; import API from "../API"; -import { setUserToken } from "../state/UserSlice"; +import { setAccessToken } from "../state/UserSlice"; import { Center, Button, Text } from 'native-base'; import SigninForm from "../components/forms/signinform"; import SignupForm from "../components/forms/signupform"; @@ -10,23 +10,23 @@ import { Provider } from "react-redux"; import store from '../state/Store'; -const hanldeSignin = async (username: string, password: string, tokenSetter: (token: string) => void): Promise => { +const hanldeSignin = async (username: string, password: string, apiSetter: (accessToken: string) => void): Promise => { try { - const response = await API.checkSigninCredentials(username, password); - tokenSetter(response); + const apiAccess = await API.authenticate({ username, password }); + apiSetter(apiAccess); return translate("loggedIn"); } catch (error) { - return error as string; + return "Username or password incorrect"; } }; -const handleSignup = async (username: string, password: string, email: string, tokenSetter: (t: string) => void): Promise => { +const handleSignup = async (username: string, password: string, email: string, apiSetter: (accessToken: string) => void): Promise => { try { - const response = await API.checkSignupCredentials(username, password, email); - tokenSetter(response); + const apiAccess = await API.createAccount({ username, password, email }); + apiSetter(apiAccess); return translate("loggedIn"); } catch (error) { - return error as string; + return "User already exists"; } }; @@ -37,17 +37,16 @@ const AuthenticationView = () => { return (
- {translate('welcome')} - {mode === "signin" ? (<> - username, password: - katerina, 1234 - hanldeSignin(username, password, (token) => dispatch(setUserToken(token)))} /> - ) : ( - handleSignup(username, password, email, (token) => dispatch(setUserToken(token)))} /> - )} + + {mode === "signin" + ? hanldeSignin(username, password, (accessToken) => dispatch(setAccessToken(accessToken)))} /> + : handleSignup(username, password, email, (accessToken) => dispatch(setAccessToken(accessToken)))} /> + } { mode ==="signin" && }
diff --git a/front/views/HomeView.tsx b/front/views/HomeView.tsx index 9aa5bb8..6ee0a30 100644 --- a/front/views/HomeView.tsx +++ b/front/views/HomeView.tsx @@ -1,12 +1,12 @@ import React from "react"; -import { useQuery } from "react-query"; +import { useQueries, useQuery } from "react-query"; import API from "../API"; import LoadingComponent from "../components/Loading"; -import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Progress, Button, useTheme, Heading, Divider } from 'native-base'; +import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Progress, Button, useTheme, Heading } from 'native-base'; import { useNavigation } from "@react-navigation/native"; import SongCardGrid from '../components/SongCardGrid'; import CompetenciesTable from '../components/CompetenciesTable' -import { translate } from "../i18n/i18n"; +import { Translate } from "../i18n/i18n"; const ProgressBar = ({ xp }: { xp: number}) => { const level = Math.floor(xp / 1000); @@ -16,11 +16,17 @@ const ProgressBar = ({ xp }: { xp: number}) => { return ( - {`${translate('level')} ${level}`} + + `${id} ${level}`}/> + - {xp} / {nextLevelThreshold} {translate('levelProgress')} + + `${xp} / ${nextLevelThreshold} ${levelProgress}`} + /> + ); } @@ -31,17 +37,24 @@ const HomeView = () => { const screenSize = useBreakpointValue({ base: 'small', md: "big"}); const flexDirection = useBreakpointValue({ base: 'column', xl: "row"}); const userQuery = useQuery(['user'], () => API.getUserInfo()); - - if (!userQuery.data) { + const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory()); + const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory()); + const skillsQuery = useQuery(['skills'], () => API.getUserSkills()); + const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getUserRecommendations()); + const artistsQueries = useQueries((playHistoryQuery.data?.concat(searchHistoryQuery.data ?? []).concat(nextStepQuery.data ?? []) ?? []).map((song) => ( + { queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) } + ))); + if (!userQuery.data || !skillsQuery.data || !searchHistoryQuery.data || !playHistoryQuery.data) { return } return - - {`${translate('welcome')} ${userQuery.data.name}!`} + + `${welcome} ${userQuery.data.name}!`}/> + @@ -50,40 +63,37 @@ const HomeView = () => { ({ - albumCover: "", - songTitle: "Song", - artistName: "Artist", - songId: 1 - }))} + heading={} + songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)) + .map((song) => ({ + albumCover: song.cover, + songTitle: song.name, + songId: song.id, + artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name + })) ?? [] + } /> - {translate('mySkillsToImprove')} + - + } maxItemPerRow={2} - songs={[ ...Array(4).keys() ].map(() => ({ - albumCover: "", - songTitle: "Song", - artistName: "Artist", - songId: 1 - }))} + songs={playHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)) + .map((song) => ({ + albumCover: song.cover, + songTitle: song.name, + songId: song.id, + artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name + })) ?? [] + } /> @@ -94,22 +104,26 @@ const HomeView = () => { - + ({ - albumCover: "", - songTitle: "Song", - artistName: "Artist", - songId: 1 - }))} + heading={} + songs={searchHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)) + .map((song) => ({ + albumCover: song.cover, + songTitle: song.name, + songId: song.id, + artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name + })) ?? [] + } /> - +
diff --git a/front/views/SearchView.tsx b/front/views/SearchView.tsx index 9bfd986..73591fa 100644 --- a/front/views/SearchView.tsx +++ b/front/views/SearchView.tsx @@ -1,63 +1,36 @@ -import React from "react"; -import { useDispatch } from "../state/Store"; -import { translate } from "../i18n/i18n"; -import { Box, Button } from "native-base"; +import React, { useEffect, useState } from "react"; +import { Box } from "native-base"; import { useNavigation } from "@react-navigation/native"; import SearchBarSuggestions from "../components/SearchBarSuggestions"; -import { - SuggestionList, - SuggestionType, - IllustratedSuggestionProps, -} from "../components/SearchBar"; - -const onTextSubmit = (text: string) => { - console.log(text); -}; +import { useQueries, useQuery } from "react-query"; +import { SuggestionType } from "../components/SearchBar"; +import API from "../API"; const SearchView = () => { + const [query, setQuery] = useState(); const navigation = useNavigation(); - - const IllustratedSuggestion: IllustratedSuggestionProps = { - text: "Love Story", - subtext: "Taylor Swift", - imageSrc: - "https://i.discogs.com/yHqu3pnLgJq-cVpYNVYu6mE-fbzIrmIRxc6vES5Oi48/rs:fit/g:sm/q:90/h:556/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTE2NjQ2/ODUwLTE2MDkwNDU5/NzQtNTkxOS5qcGVn.jpeg", - onPress: () => navigation.navigate("Song", { songId: 1 }), - }; - // fill the suggestions with the data from the backend - const suggestions: SuggestionList = [ - { - type: SuggestionType.ILLUSTRATED, - data: IllustratedSuggestion, - }, - { - type: SuggestionType.ILLUSTRATED, - data: IllustratedSuggestion, - }, - { - type: SuggestionType.ILLUSTRATED, - data: { - text: "Shed a Light", - subtext: "Robin Schulz & David Guetta", - imageSrc: - "https://imgs.search.brave.com/O9j2Z-oWiniq3lj7d-dAOgXLWCIqnHaFegmaSeIkWOY/rs:fit:560:320:1/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvZW4vdGh1bWIv/OC84ZS9TaGVkX2Ff/TGlnaHRfUm9iaW5f/U2NodWx6LmpwZy81/MTJweC1TaGVkX2Ff/TGlnaHRfUm9iaW5f/U2NodWx6LmpwZw", - onPress: () => navigation.navigate("Song", { songId: 1 }), - }, - }, - { - type: SuggestionType.TEXT, - data: { - text: "Lady Gaga", - onPress: () => navigation.navigate("Song", { songId: 1 }), - }, - }, - ]; + const searchQuery = useQuery( + ['search', query], + () => API.searchSongs(query!), + { enabled: query != undefined } + ); + const artistsQueries = useQueries(searchQuery.data?.map((song) => ( + { queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) } + )) ??[]); return ( ({ + type: SuggestionType.ILLUSTRATED, + data: { + text: searchResult.name, + subtext: artistsQueries.find((artistQuery) => artistQuery.data?.id == searchResult.artistId)?.data?.name ?? "", + imageSrc: searchResult.cover, + onPress: () => navigation.navigate("Song", { songId: searchResult.id }) + } + })) ?? []} /> ); diff --git a/front/views/SettingsView.tsx b/front/views/SettingsView.tsx index 5ff0f97..6e43900 100644 --- a/front/views/SettingsView.tsx +++ b/front/views/SettingsView.tsx @@ -2,10 +2,12 @@ import React from 'react'; import { View } from 'react-native'; import { Center, Button, Text, Switch, Slider, Select, Heading } from "native-base"; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { unsetUserToken } from '../state/UserSlice'; -import { useDispatch, useSelector } from "react-redux"; +import { unsetAccessToken } from '../state/UserSlice'; +import { useDispatch } from "react-redux"; +import { RootState, useSelector } from '../state/Store'; import { useLanguage } from "../state/LanguageSlice"; -import i18n, { AvailableLanguages, DefaultLanguage, translate } from "../i18n/i18n"; +import { SettingsState, updateSettings } from '../state/SettingsSlice'; +import { AvailableLanguages, translate, Translate } from "../i18n/i18n"; const SettingsStack = createNativeStackNavigator(); @@ -15,31 +17,31 @@ export const MainView = ({navigation}) => { return (
-
) @@ -47,20 +49,25 @@ export const MainView = ({navigation}) => { export const PreferencesView = ({navigation}) => { const dispatch = useDispatch(); - const language: AvailableLanguages = useSelector((state) => state.language.value); - + const language: AvailableLanguages = useSelector((state: RootState) => state.language.value); + const settings = useSelector((state: RootState) => (state.settings.settings as SettingsState)); return (
- { translate('prefBtn')} - - + + + + - { - let newLanguage = DefaultLanguage; - newLanguage = itemValue;Heading - dispatch(useLanguage(newLanguage)); + onValueChange={(itemValue) => { + dispatch(useLanguage(itemValue as AvailableLanguages)); }}> @@ -84,12 +89,12 @@ export const PreferencesView = ({navigation}) => { - change device} - > + onValueChange={(itemValue: string) => { dispatch(updateSettings({ preferedInputName: itemValue })) }} + > @@ -126,31 +135,41 @@ export const PreferencesView = ({navigation}) => { ) } -export const NotificationsView = ({navigation}) => { +const NotificationsView = ({navigation}) => { + const dispatch = useDispatch(); + const settings: SettingsState = useSelector((state: RootState) => state.settings); return (
- { translate('notifBtn')} - - + + + + Push notifications - + { dispatch(updateSettings({ enablePushNotifications: value })) }} + /> - Email notifications - + { dispatch(updateSettings({ enableMailNotifications: value })) }} + /> - Training reminder - + { dispatch(updateSettings({ enableLessongsReminders: value })) }} + /> - New songs - + { dispatch(updateSettings({ enableReleaseAlerts: value })) }} + />
) @@ -159,9 +178,13 @@ export const NotificationsView = ({navigation}) => { export const PrivacyView = ({navigation}) => { return (
- { translate('privBtn')} + + + - + Data Collection diff --git a/front/views/SongLobbyView.tsx b/front/views/SongLobbyView.tsx index 4bad7aa..088ae39 100644 --- a/front/views/SongLobbyView.tsx +++ b/front/views/SongLobbyView.tsx @@ -1,13 +1,12 @@ import { useRoute } from "@react-navigation/native"; import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base"; -import API from "../API"; import { useQuery } from 'react-query'; import LoadingComponent from "../components/Loading"; import React, { useEffect, useState } from "react"; -import logo from '../assets/cover.png'; -import { translate } from "../i18n/i18n"; +import { Translate, translate } from "../i18n/i18n"; import formatDuration from "format-duration"; import { Ionicons } from '@expo/vector-icons'; +import API from "../API"; interface SongLobbyProps { // The unique identifier to find a song @@ -34,35 +33,45 @@ const SongLobbyView = () => { - + {songQuery.data!.title} - {'3:20'} - {translate('level')} { chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length } - + + `3:20 - ${level} - ${ chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }`} + /> + + - {translate('bestScore') } + + + {scoresQuery.data!.sort()[0]?.score} - {translate('lastScore') } + + + {scoresQuery.data!.slice(-1)[0]!.score} - {songQuery.data!.description} + {/* {songQuery.data!.description} */} diff --git a/front/yarn.lock b/front/yarn.lock index eab0c20..c61317f 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -1221,6 +1221,20 @@ slugify "^1.3.4" sucrase "^3.20.0" +"@expo/configure-splash-screen@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@expo/configure-splash-screen/-/configure-splash-screen-0.6.0.tgz#07d97ee512fd859fcc09506ba3762fd6263ebc39" + integrity sha512-4DyPoNXJqx9bN4nEwF3HQreo//ECu7gDe1Xor3dnnzFm9P/VDxAKdbEhA0n+R6fgkNfT2onVHWijqvdpTS3Xew== + dependencies: + color-string "^1.5.3" + commander "^5.1.0" + fs-extra "^9.0.0" + glob "^7.1.6" + lodash "^4.17.15" + pngjs "^5.0.0" + xcode "^3.0.0" + xml-js "^1.6.11" + "@expo/dev-server@0.1.116": version "0.1.116" resolved "https://registry.yarnpkg.com/@expo/dev-server/-/dev-server-0.1.116.tgz#65774a28cbe1ab22101be4f41626b7530b4f7560" @@ -2221,6 +2235,13 @@ "@react-aria/ssr" "^3.0.1" "@react-aria/utils" "^3.3.0" +"@react-native-async-storage/async-storage@^1.17.11": + version "1.17.11" + resolved "https://registry.yarnpkg.com/@react-native-async-storage/async-storage/-/async-storage-1.17.11.tgz#7ec329c1b9f610e344602e806b04d7c928a2341d" + integrity sha512-bzs45n5HNcDq6mxXnSsOHysZWn1SbbebNxldBXCQs8dSvF8Aor9KCdpm+TpnnGweK3R6diqsT8lFhX77VX0NFw== + dependencies: + merge-options "^3.0.4" + "@react-native-community/cli-debugger-ui@^7.0.3": version "7.0.3" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-7.0.3.tgz#3eeeacc5a43513cbcae56e5e965d77726361bcb4" @@ -2405,6 +2426,16 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== +"@react-navigation/core@^3.7.9": + version "3.7.9" + resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.7.9.tgz#3f7ba0fcb6c8d74a77a057382af198d84c7c4e3b" + integrity sha512-EknbzM8OI9A5alRxXtQRV5Awle68B+z1QAxNty5DxmlS3BNfmduWNGnim159ROyqxkuDffK9L/U/Tbd45mx+Jg== + dependencies: + hoist-non-react-statics "^3.3.2" + path-to-regexp "^1.8.0" + query-string "^6.13.6" + react-is "^16.13.0" + "@react-navigation/core@^6.4.0": version "6.4.0" resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.0.tgz#c44d33a8d8ef010a102c7f831fc8add772678509" @@ -2430,6 +2461,14 @@ "@react-navigation/elements" "^1.3.6" warn-once "^0.1.0" +"@react-navigation/native@^3.8.4": + version "3.8.4" + resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.8.4.tgz#4d77f86506364ecf18b33c7f8740afb6763d0b37" + integrity sha512-gXSVcL7bfFDyVkvyg1FiAqTCIgZub5K1X/TZqURBs2CPqDpfX1OsCtB9D33eTF14SpbfgHW866btqrrxoCACfg== + dependencies: + hoist-non-react-statics "^3.3.2" + react-native-safe-area-view "^0.14.9" + "@react-navigation/native@^6.0.11": version "6.0.13" resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.0.13.tgz#ec504120e193ea6a7f24ffa765a1338be5a3160a" @@ -2976,6 +3015,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067" integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA== +"@types/node@^18.11.8": + version "18.11.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265" + integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -3005,6 +3049,13 @@ dependencies: "@types/react" "*" +"@types/react-navigation@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@types/react-navigation/-/react-navigation-3.4.0.tgz#d610d13c9162312079a8ca102660143f07432cbf" + integrity sha512-Y7F5zU8BTBK8tEOvUqgvwvPZ7+9vnc2UI1vHwJ/9ZJG98TntNv04GWa6lrn4MA4149pqw6cyNw/V49Yd2osAFQ== + dependencies: + react-navigation "*" + "@types/react-query@^1.2.9": version "1.2.9" resolved "https://registry.yarnpkg.com/@types/react-query/-/react-query-1.2.9.tgz#61df5a0594ea4b90234f9c0fdd8f12a06e331fed" @@ -3440,6 +3491,11 @@ babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0: resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf" integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ== +babel-plugin-transform-inline-environment-variables@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-0.4.4.tgz#974245008b3cbbd646bd81707af147aea3acca43" + integrity sha512-bJILBtn5a11SmtR2j/3mBOjX4K3weC6cq+NNZ7hG22wCAqpc3qtj/iN7dSe9HDiS46lgp1nHsQgeYrea/RUe+g== + babel-preset-current-node-syntax@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" @@ -3972,7 +4028,7 @@ color-name@^1.0.0, color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -color-string@^1.6.0: +color-string@^1.5.3, color-string@^1.6.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== @@ -4015,6 +4071,11 @@ commander@^4.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + commander@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" @@ -4810,6 +4871,19 @@ expo-modules-core@0.9.2: compare-versions "^3.4.0" invariant "^2.2.4" +expo-secure-store@~11.2.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/expo-secure-store/-/expo-secure-store-11.2.0.tgz#c2c8fdcac39c5e1828a8fea45765028a11625575" + integrity sha512-PlmDplx9QNqaTVKNLgqSurRhzYf6YbVTTiSKX5JlEMWgOiBTz77Nh6mpMMjI1jwrvtz9grD+CT2AlDEGXe+Ovg== + +expo-splash-screen@~0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/expo-splash-screen/-/expo-splash-screen-0.15.1.tgz#bdfb2434bda7fb1cde97e029fc7a791d7b3e3125" + integrity sha512-Yvz6p/ig+cQp9c1PLSm1YshpNJXRp/xtxajfwPq2kampf61zA+xnoMk+J6YcNeXeIlqHysj3ND2tMhEEQjM/ow== + dependencies: + "@expo/configure-splash-screen" "^0.6.0" + "@expo/prebuild-config" "~4.0.0" + expo-status-bar@~1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-1.3.0.tgz#d71fd0b880ea201905f5dd8abcd18db7476c9f03" @@ -5358,6 +5432,11 @@ hermes-profile-transformer@^0.0.6: dependencies: source-map "^0.7.3" +hoist-non-react-statics@^2.3.1: + version "2.5.5" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" + integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== + hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" @@ -5750,6 +5829,11 @@ is-path-inside@^3.0.2: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -5811,6 +5895,11 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" @@ -6893,6 +6982,13 @@ memory-cache@~0.2.0: resolved "https://registry.yarnpkg.com/memory-cache/-/memory-cache-0.2.0.tgz#7890b01d52c00c8ebc9d533e1f8eb17e3034871a" integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== +merge-options@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/merge-options/-/merge-options-3.0.4.tgz#84709c2aa2a4b24c1981f66c179fe5565cc6dbb7" + integrity sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ== + dependencies: + is-plain-obj "^2.1.0" + merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -7859,6 +7955,13 @@ path-parse@^1.0.5, path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-to-regexp@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== + dependencies: + isarray "0.0.1" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -7918,6 +8021,11 @@ pngjs@^3.3.0: resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f" integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w== +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + posix-character-classes@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" @@ -8050,6 +8158,16 @@ qs@6.7.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== +query-string@^6.13.6: + version "6.14.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + query-string@^7.0.0: version "7.1.1" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1" @@ -8182,6 +8300,13 @@ react-native-safe-area-context@4.2.4: resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.2.4.tgz#4df42819759c4d3c74252c8678c2772cfa2271a6" integrity sha512-OOX+W2G4YYufvryonn6Kw6YnyT8ZThkxPHZBD04NLHaZmicUaaDVII/PZ3M5fD1o5N62+T+8K4bCS5Un2ggvkA== +react-native-safe-area-view@^0.14.9: + version "0.14.9" + resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.14.9.tgz#90ee8383037010d9a5055a97cf97e4c1da1f0c3d" + integrity sha512-WII/ulhpVyL/qbYb7vydq7dJAfZRBcEhg4/UWt6F6nAKpLa3gAceMOxBxI914ppwSP/TdUsandFy6lkJQE0z4A== + dependencies: + hoist-non-react-statics "^2.3.1" + react-native-screens@~3.11.1: version "3.11.1" resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.11.1.tgz#9bca9968986ca9195cb1e7e6fca37543bde64ecb" @@ -8263,6 +8388,14 @@ react-native@0.68.2: whatwg-fetch "^3.0.0" ws "^6.1.4" +react-navigation@*: + version "4.4.4" + resolved "https://registry.yarnpkg.com/react-navigation/-/react-navigation-4.4.4.tgz#8cda2219196311db440e54998bc724523359949f" + integrity sha512-08Nzy1aKEd73496CsuzN49vLFmxPKYF5WpKGgGvkQ10clB79IRM2BtAfVl6NgPKuUM8FXq1wCsrjo/c5ftl5og== + dependencies: + "@react-navigation/core" "^3.7.9" + "@react-navigation/native" "^3.8.4" + react-query@*: version "3.39.2" resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.2.tgz#9224140f0296f01e9664b78ed6e4f69a0cc9216f" @@ -8379,6 +8512,11 @@ recast@^0.20.4: source-map "~0.6.1" tslib "^2.0.1" +redux-persist@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" + integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== + redux-thunk@^2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" @@ -9876,6 +10014,13 @@ xcode@^3.0.0, xcode@^3.0.1: simple-plist "^1.1.0" uuid "^7.0.3" +xml-js@^1.6.11: + version "1.6.11" + resolved "https://registry.yarnpkg.com/xml-js/-/xml-js-1.6.11.tgz#927d2f6947f7f1c19a316dd8eea3614e8b18f8e9" + integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g== + dependencies: + sax "^1.2.4" + xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"