From 3154f46fff16197794adc316b2c53eac6271d96c Mon Sep 17 00:00:00 2001 From: Ronnie Beggs <66931067+ronniebeggs@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:06:54 -0700 Subject: [PATCH] [cases] error handling and feedback improvements (#91) * [feat] add loading feedback to qr code scanner button. * [feat] add loading feedback to AddCase screen button. * [feat] add loading feedback to confirm eligibility button. * [feat] loading feedback for ineligibility and opt out actions. * [feat] add loading feedback to file claim action. * [fix] restructure CaseScreen and CaseContext to prevent unnecessary reloads. * [refactor] integrate getCaseStatus with case context. * [refactor] case status -> claim status. * [feat] create basic loading spinner component. * [feat] replace loading text with spinner on all screens. * [feat] add light grey background as image placeholder. * [refactor] loading screen component. * [refactor] move add/remove case to case context. * [cleanup] add ios/ to gitignore. * [cleanup] use case context function for fetching context data. * [cleanup] remove unused import. * [wip][feat] configure timeout behavior for poor network connection settings. * [refactor] move resetAndPushToRoute to auth queries; cleanup. * [feat] create resetAndPushToHome that navigates to the home screen (relative to whether they're logged in or not). * [feat] create wip full stop error function; refactor screen loading component. * [cleanup] misc cleanup; update screen loading text for clarity. * [cleanup] update doc strings. * [feat] begin integrating error handler for case fetching functions. * [wip] continue adding error handling to case related screens. * [feat] add error handler to all case related screens. * [fix] case context code performance and clarity improvements. * [cleanup] misc refactoring and adding comments. * [fix] resolve missing caseData on FileClaim screen. --- .gitignore | 1 + package-lock.json | 83 +++++++++++ package.json | 1 + src/Components/CaseSummaryCard/styles.ts | 1 + src/Components/CaseSummaryContent/styles.ts | 1 + src/Components/FormsCard/FormsCard.tsx | 10 +- .../ScreenLoadingComponent.tsx | 42 ++++++ .../ScreenLoadingComponent/styles.ts | 10 ++ .../AllCases/CaseScreen/[caseUid].tsx | 59 ++++---- .../AllCases/CaseSummaryScreen/[caseUid].tsx | 13 +- .../ConfirmIneligibility/[caseUid].tsx | 33 +++-- .../AllCases/EligibilityForm/[caseUid].tsx | 59 +++++--- .../AllCases/EligibilityForm/styles.ts | 1 + .../AllCases/FileClaim/[caseUid].tsx | 52 ++++--- .../AllCases/Forms/[caseUid].tsx | 36 +++-- .../OptOut/ConfirmOptOut/[caseUid].tsx | 33 +++-- .../AllCases/OptOut/[caseUid].tsx | 17 ++- .../Updates/UpdateView/[updateUid].tsx | 9 +- .../AllCases/Updates/[caseUid].tsx | 15 +- .../(BottomTabNavigation)/AllCases/index.tsx | 9 +- .../QRCodeScanner/AddCase/[caseUid].tsx | 33 +++-- .../QRCodeScanner/index.tsx | 25 +++- src/app/index.tsx | 13 +- src/context/CaseContext.tsx | 137 +++++++++++++----- src/supabase/queries/auth.ts | 55 +++++++ src/supabase/queries/cases.ts | 112 ++------------ src/supabase/queries/forms.ts | 2 +- src/types/types.tsx | 12 +- 28 files changed, 567 insertions(+), 307 deletions(-) create mode 100644 src/Components/ScreenLoadingComponent/ScreenLoadingComponent.tsx create mode 100644 src/Components/ScreenLoadingComponent/styles.ts diff --git a/.gitignore b/.gitignore index f8532194..95316b4b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ node_modules/ .expo/ dist/ web-build/ +ios/ # Native *.orig.* diff --git a/package-lock.json b/package-lock.json index 96bcbf48..c65001f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "react-native-elements": "^3.4.3", "react-native-gesture-handler": "~2.16.1", "react-native-otp-textinput": "^1.1.5", + "react-native-paper": "^5.12.3", "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", "react-native-svg": "15.2.0", @@ -2104,6 +2105,26 @@ "node": ">=6.9.0" } }, + "node_modules/@callstack/react-theme-provider": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", + "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==", + "dependencies": { + "deepmerge": "^3.2.0", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, + "node_modules/@callstack/react-theme-provider/node_modules/deepmerge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", + "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -20850,6 +20871,31 @@ "react-native": "^0.72.4" } }, + "node_modules/react-native-paper": { + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.12.3.tgz", + "integrity": "sha512-nH1e1pGPE/aOE5YR2GRX7CfMHFA9cAfrAfgCtwL4amJPDZCoVjc5yt2VDiUE1rT+JUfk0qdICMP3UggxvjMgug==", + "dependencies": { + "@callstack/react-theme-provider": "^3.0.9", + "color": "^3.1.2", + "use-latest-callback": "^0.1.5" + }, + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-safe-area-context": "*", + "react-native-vector-icons": "*" + } + }, + "node_modules/react-native-paper/node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/react-native-ratings": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.0.4.tgz", @@ -27621,6 +27667,22 @@ "to-fast-properties": "^2.0.0" } }, + "@callstack/react-theme-provider": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@callstack/react-theme-provider/-/react-theme-provider-3.0.9.tgz", + "integrity": "sha512-tTQ0uDSCL0ypeMa8T/E9wAZRGKWj8kXP7+6RYgPTfOPs9N07C9xM8P02GJ3feETap4Ux5S69D9nteq9mEj86NA==", + "requires": { + "deepmerge": "^3.2.0", + "hoist-non-react-statics": "^3.3.0" + }, + "dependencies": { + "deepmerge": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-3.3.0.tgz", + "integrity": "sha512-GRQOafGHwMHpjPx9iCvTgpu9NojZ49q794EEL94JVEw6VaeA8XTUyBKvAkOOjBX9oJNiV6G3P+T+tihFjo2TqA==" + } + } + }, "@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -41602,6 +41664,27 @@ "react": "^18.2.0" } }, + "react-native-paper": { + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.12.3.tgz", + "integrity": "sha512-nH1e1pGPE/aOE5YR2GRX7CfMHFA9cAfrAfgCtwL4amJPDZCoVjc5yt2VDiUE1rT+JUfk0qdICMP3UggxvjMgug==", + "requires": { + "@callstack/react-theme-provider": "^3.0.9", + "color": "^3.1.2", + "use-latest-callback": "^0.1.5" + }, + "dependencies": { + "color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "requires": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + } + } + }, "react-native-ratings": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/react-native-ratings/-/react-native-ratings-8.0.4.tgz", diff --git a/package.json b/package.json index e3b88a35..bb81779d 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-native-elements": "^3.4.3", "react-native-gesture-handler": "~2.16.1", "react-native-otp-textinput": "^1.1.5", + "react-native-paper": "^5.12.3", "react-native-safe-area-context": "4.10.1", "react-native-screens": "3.31.1", "react-native-svg": "15.2.0", diff --git a/src/Components/CaseSummaryCard/styles.ts b/src/Components/CaseSummaryCard/styles.ts index 55d0a162..ecb8b9a2 100644 --- a/src/Components/CaseSummaryCard/styles.ts +++ b/src/Components/CaseSummaryCard/styles.ts @@ -14,6 +14,7 @@ export default StyleSheet.create({ justifyContent: 'space-between', borderTopRightRadius: 4, borderTopLeftRadius: 4, + backgroundColor: colors.lightGrey, }, infoContainer: { flexDirection: 'column', diff --git a/src/Components/CaseSummaryContent/styles.ts b/src/Components/CaseSummaryContent/styles.ts index a157d5dc..97c5278a 100644 --- a/src/Components/CaseSummaryContent/styles.ts +++ b/src/Components/CaseSummaryContent/styles.ts @@ -18,6 +18,7 @@ export default StyleSheet.create({ borderRadius: 5, marginTop: 30, marginBottom: 20, + backgroundColor: colors.lightGrey, }, summaryContainer: { width: '100%', diff --git a/src/Components/FormsCard/FormsCard.tsx b/src/Components/FormsCard/FormsCard.tsx index 9289189e..7d4b6687 100644 --- a/src/Components/FormsCard/FormsCard.tsx +++ b/src/Components/FormsCard/FormsCard.tsx @@ -14,13 +14,9 @@ export default function FormsCard(caseData: Case) { const [featuredForm, setFeaturedForm] = useState
(); const getForm = async () => { - const formData = await getFeaturedForm( - caseData.id, - caseData.featuredFormName, - ); - if (formData) { - setFeaturedForm(formData); - } + await getFeaturedForm(caseData.id, caseData.featuredFormName) + .then(formData => setFeaturedForm(formData)) + .catch(response => console.warn(response)); }; useEffect(() => { diff --git a/src/Components/ScreenLoadingComponent/ScreenLoadingComponent.tsx b/src/Components/ScreenLoadingComponent/ScreenLoadingComponent.tsx new file mode 100644 index 00000000..b3cd92cd --- /dev/null +++ b/src/Components/ScreenLoadingComponent/ScreenLoadingComponent.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react'; +import { View, Text } from 'react-native'; +import { ActivityIndicator } from 'react-native-paper'; + +import styles from './styles'; +import { colors } from '../../styles/colors'; +import { fonts } from '../../styles/fonts'; + +export default function ScreenLoadingComponent() { + const [loadingPromptExists, setLoadingPromptExists] = + useState(false); + const [timeoutReached, setTimeoutReached] = useState(false); + + useEffect(() => { + setTimeout(() => { + setLoadingPromptExists(true); + setTimeout(() => { + setTimeoutReached(true); + }, 10000); + }, 5000); + }, []); + + return ( + + + {loadingPromptExists && ( + + {!timeoutReached ? ( + This is taking longer than usual... + ) : ( + + There seems to be an issue connecting with Impact Fund servers...{' '} + {'\n'} + {'\n'} + Please check your internet connection or try again later. + + )} + + )} + + ); +} diff --git a/src/Components/ScreenLoadingComponent/styles.ts b/src/Components/ScreenLoadingComponent/styles.ts new file mode 100644 index 00000000..c02d065d --- /dev/null +++ b/src/Components/ScreenLoadingComponent/styles.ts @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +export default StyleSheet.create({ + container: { + width: 300, + alignItems: 'center', + marginTop: 20, + rowGap: 20, + }, +}); diff --git a/src/app/(BottomTabNavigation)/AllCases/CaseScreen/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/CaseScreen/[caseUid].tsx index e559e085..b10de71d 100644 --- a/src/app/(BottomTabNavigation)/AllCases/CaseScreen/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/CaseScreen/[caseUid].tsx @@ -1,4 +1,4 @@ -import { useLocalSearchParams, useNavigation } from 'expo-router'; +import { useLocalSearchParams } from 'expo-router'; import React, { useEffect, useState } from 'react'; import { View, ScrollView, Text } from 'react-native'; @@ -9,50 +9,45 @@ import ClaimStatusBar from '../../../../Components/ClaimStatusBar/ClaimStatusBar import EducationalBar from '../../../../Components/EducationalBar/EducationalBar'; import EligibilityCard from '../../../../Components/EligibilityCard/EligibilityCard'; import FormsCard from '../../../../Components/FormsCard/FormsCard'; +import ScreenLoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; import StatusUpdatesBar from '../../../../Components/StatusUpdatesBar/StatusUpdatesBar'; +import { useCaseContext } from '../../../../context/CaseContext'; import { fonts } from '../../../../styles/fonts'; import { device } from '../../../../styles/global'; -import { getCaseStatus, getCaseById } from '../../../../supabase/queries/cases'; -import { Case, Eligibility } from '../../../../types/types'; +import { fullStopErrorHandler } from '../../../../supabase/queries/auth'; +import { getCaseById } from '../../../../supabase/queries/cases'; +import { Case, ClaimStatus } from '../../../../types/types'; -function CaseScreen() { +export default function CaseScreen() { const { caseUid } = useLocalSearchParams<{ caseUid: string }>(); - const [status, setStatus] = useState(); + const [status, setStatus] = useState(); const [caseData, setCaseData] = useState(); - const [isLoading, setIsLoading] = useState(true); - const navigation = useNavigation(); - const getCase = async (uid: string) => { - const caseData = await getCaseById(uid); - setCaseData(caseData); - setIsLoading(false); + const { getClaimStatus } = useCaseContext(); + + const getCase = async (caseUid: string) => { + await getCaseById(caseUid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); }; - const getStatus = async (uid: string) => { - const caseStatus = await getCaseStatus(uid); - setStatus(caseStatus); + const getStatus = async (caseUid: string) => { + await getClaimStatus(caseUid) + .then(claimStatus => setStatus(claimStatus)) + .catch(response => fullStopErrorHandler(response)); }; useEffect(() => { - if (caseUid !== undefined) { + if (caseUid) { getCase(caseUid); + getStatus(caseUid); } }, []); - useEffect(() => { - navigation.addListener('focus', async () => { - setIsLoading(true); - if (caseUid !== undefined) { - await getStatus(caseUid); - } - setIsLoading(false); - }); - }, [navigation]); - return ( - {isLoading || caseData === undefined ? ( - Loading... + {!caseData || !status ? ( + ) : ( - {status === Eligibility.ELIGIBLE && ( + {status === ClaimStatus.ELIGIBLE && ( )} - {status === Eligibility.CLAIM_FILED && ( + {status === ClaimStatus.CLAIM_FILED && ( )} - {(status === Eligibility.INELIGIBLE || - status === Eligibility.UNDETERMINED) && ( + {(status === ClaimStatus.INELIGIBLE || + status === ClaimStatus.UNDETERMINED) && ( )} ); } - -export default CaseScreen; diff --git a/src/app/(BottomTabNavigation)/AllCases/CaseSummaryScreen/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/CaseSummaryScreen/[caseUid].tsx index 32b1258a..38fc51cb 100644 --- a/src/app/(BottomTabNavigation)/AllCases/CaseSummaryScreen/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/CaseSummaryScreen/[caseUid].tsx @@ -1,11 +1,13 @@ import { useLocalSearchParams } from 'expo-router'; import React, { useState, useEffect } from 'react'; -import { View, Text } from 'react-native'; +import { View } from 'react-native'; import styles from './styles'; import CaseSummaryContent from '../../../../Components/CaseSummaryContent/CaseSummaryContent'; import ExternalSiteLink from '../../../../Components/ExternalSiteLink/ExternalSiteLink'; +import ScreenLoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; import { device } from '../../../../styles/global'; +import { fullStopErrorHandler } from '../../../../supabase/queries/auth'; import { getCaseById } from '../../../../supabase/queries/cases'; import { Case } from '../../../../types/types'; @@ -14,12 +16,13 @@ export default function CaseSummaryScreen() { const [caseData, setCaseData] = useState(); const getCase = async (uid: string) => { - const caseData = await getCaseById(uid); - setCaseData(caseData); + await getCaseById(uid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); }; useEffect(() => { - if (caseUid !== undefined) { + if (caseUid) { getCase(caseUid); } }, []); @@ -27,7 +30,7 @@ export default function CaseSummaryScreen() { return ( {caseData === undefined ? ( - Loading... + ) : ( <> diff --git a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/ConfirmIneligibility/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/ConfirmIneligibility/[caseUid].tsx index 95c4c591..a2faccf8 100644 --- a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/ConfirmIneligibility/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/ConfirmIneligibility/[caseUid].tsx @@ -1,6 +1,6 @@ import { router, useLocalSearchParams } from 'expo-router'; -import React, { useContext } from 'react'; -import { View, Text } from 'react-native'; +import React, { useState } from 'react'; +import { View, Text, ActivityIndicator } from 'react-native'; import Alarm from '../../../../../../assets/alarm-triangle.svg'; import Ex from '../../../../../../assets/cancel-x-icon.svg'; @@ -10,24 +10,30 @@ import { ButtonBlack, ButtonWhite, } from '../../../../../Components/AuthButton/AuthButton'; -import { CaseContext } from '../../../../../context/CaseContext'; +import { useCaseContext } from '../../../../../context/CaseContext'; import { fonts } from '../../../../../styles/fonts'; import { device } from '../../../../../styles/global'; import { input } from '../../../../../styles/input'; import { instruction } from '../../../../../styles/instruction'; +import { + fullStopErrorHandler, + resetAndPushToRoute, +} from '../../../../../supabase/queries/auth'; import { CaseUid } from '../../../../../types/types'; export default function ConfirmEligibility() { const { caseUid } = useLocalSearchParams<{ caseUid: CaseUid }>(); - const { leaveCase } = useContext(CaseContext); + const { leaveCase } = useCaseContext(); + const [queryLoading, setQueryLoading] = useState(false); async function deleteCase() { - if (caseUid !== undefined) { - await leaveCase(caseUid); - router.push({ - pathname: '/AllCases', - }); + setQueryLoading(true); + if (caseUid) { + await leaveCase(caseUid) + .then(() => resetAndPushToRoute('/AllCases')) + .catch(response => fullStopErrorHandler(response)); } + setQueryLoading(false); } return ( @@ -73,9 +79,14 @@ export default function ConfirmEligibility() { - deleteCase()} $halfWidth $centeredContent> + deleteCase()} + disabled={queryLoading} + $halfWidth + $centeredContent + > - + {queryLoading ? : } Yes, I do diff --git a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/[caseUid].tsx index e48db4e2..771c689f 100644 --- a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/[caseUid].tsx @@ -1,7 +1,7 @@ import { Image } from 'expo-image'; import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useState } from 'react'; -import { View, Text, FlatList } from 'react-native'; +import { View, Text, FlatList, ActivityIndicator } from 'react-native'; import styles from './styles'; import Check from '../../../../../assets/check-circle.svg'; @@ -12,18 +12,21 @@ import { ButtonWhite, } from '../../../../Components/AuthButton/AuthButton'; import PressableRequirement from '../../../../Components/PressableRequirement/PressableRequirement'; +import ScreenLoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; +import { useCaseContext } from '../../../../context/CaseContext'; import { fonts } from '../../../../styles/fonts'; import { device } from '../../../../styles/global'; import { input } from '../../../../styles/input'; import { - updateCaseStatus, - getCaseById, -} from '../../../../supabase/queries/cases'; + fullStopErrorHandler, + resetAndPushToRoute, +} from '../../../../supabase/queries/auth'; +import { getCaseById } from '../../../../supabase/queries/cases'; import { getRequirementsByCaseUid } from '../../../../supabase/queries/eligibility'; import { Case, CaseUid, - Eligibility, + ClaimStatus, EligibilityRequirement, } from '../../../../types/types'; @@ -34,26 +37,33 @@ export default function EligibilityForm() { EligibilityRequirement[] >([]); const [checkCount, setCheckCount] = useState(0); + const [queryLoading, setQueryLoading] = useState(false); - async function fetchCaseData() { - if (caseUid) { - const caseData = await getCaseById(caseUid); - setCaseData(caseData); - } + const { updateClaimStatus } = useCaseContext(); + + async function fetchCaseData(caseUid: CaseUid) { + await getCaseById(caseUid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); } - async function fetchEligibilityRequirments() { - if (caseUid) { - const requirements = await getRequirementsByCaseUid(caseUid); - setEligibilityRequirements(requirements); - } + async function fetchEligibilityRequirments(caseUid: CaseUid) { + await getRequirementsByCaseUid(caseUid) + .then(requirements => setEligibilityRequirements(requirements)) + .catch(response => fullStopErrorHandler(response)); } async function confirmEligibility() { + setQueryLoading(true); if (caseUid !== undefined) { - await updateCaseStatus(caseUid, Eligibility.ELIGIBLE); - router.back(); + await updateClaimStatus(caseUid, ClaimStatus.ELIGIBLE) + .then(() => { + resetAndPushToRoute('/AllCases'); + router.push(`/AllCases/CaseScreen/${caseUid}`); + }) + .catch(response => fullStopErrorHandler(response)); } + setQueryLoading(false); } function confirmIneligibility() { @@ -61,14 +71,16 @@ export default function EligibilityForm() { } useEffect(() => { - fetchCaseData(); - fetchEligibilityRequirments(); + if (caseUid) { + fetchCaseData(caseUid); + fetchEligibilityRequirments(caseUid); + } }, []); return ( {caseData === undefined ? ( - Loading... + ) : ( confirmEligibility()} $halfWidth $centeredContent > - + {queryLoading ? : } Yes, I do diff --git a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/styles.ts b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/styles.ts index bc6db7fc..db207dfa 100644 --- a/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/styles.ts +++ b/src/app/(BottomTabNavigation)/AllCases/EligibilityForm/styles.ts @@ -51,6 +51,7 @@ export default StyleSheet.create({ imageContainer: { aspectRatio: 1.75, borderRadius: 5, + backgroundColor: colors.lightGrey, }, titleText: { fontWeight: 'bold', diff --git a/src/app/(BottomTabNavigation)/AllCases/FileClaim/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/FileClaim/[caseUid].tsx index 134b3a9a..a3a21f16 100644 --- a/src/app/(BottomTabNavigation)/AllCases/FileClaim/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/FileClaim/[caseUid].tsx @@ -1,6 +1,6 @@ import { router, useLocalSearchParams } from 'expo-router'; import React, { useEffect, useState } from 'react'; -import { View, Text } from 'react-native'; +import { View, Text, ActivityIndicator } from 'react-native'; import BlackRightArrow from '../../../../../assets/black-right-arrow.svg'; import Document from '../../../../../assets/document-add.svg'; @@ -11,26 +11,25 @@ import { ButtonBlack, ButtonWhite, } from '../../../../Components/AuthButton/AuthButton'; +import ScreenLoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; +import { useCaseContext } from '../../../../context/CaseContext'; import { fonts } from '../../../../styles/fonts'; import { device } from '../../../../styles/global'; import { instruction } from '../../../../styles/instruction'; import { - getCaseById, - updateCaseStatus, -} from '../../../../supabase/queries/cases'; -import { Case, CaseUid, Eligibility } from '../../../../types/types'; + fullStopErrorHandler, + resetAndPushToRoute, +} from '../../../../supabase/queries/auth'; +import { getCaseById } from '../../../../supabase/queries/cases'; +import { Case, CaseUid, ClaimStatus } from '../../../../types/types'; import { openUrl } from '../utils'; export default function FileClaimScreen() { const { caseUid } = useLocalSearchParams<{ caseUid: CaseUid }>(); const [caseData, setCaseData] = useState(); + const [queryLoading, setQueryLoading] = useState(false); - async function confirmClaimFiled() { - if (caseUid !== undefined) { - await updateCaseStatus(caseUid, Eligibility.CLAIM_FILED); - router.back(); - } - } + const { updateClaimStatus } = useCaseContext(); function navigateToClaimLink() { const claimLink = caseData?.claimLink; @@ -39,21 +38,35 @@ export default function FileClaimScreen() { } } - async function fetchCaseData() { + async function fetchCaseData(caseUid: CaseUid) { + await getCaseById(caseUid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); + } + + async function confirmClaimFiled() { + setQueryLoading(true); if (caseUid) { - const caseData = await getCaseById(caseUid); - setCaseData(caseData); + await updateClaimStatus(caseUid, ClaimStatus.CLAIM_FILED) + .then(() => { + resetAndPushToRoute('/AllCases'); + router.push(`/AllCases/CaseScreen/${caseUid}`); + }) + .catch(response => fullStopErrorHandler(response)); } + setQueryLoading(false); } useEffect(() => { - fetchCaseData(); + if (caseUid) { + fetchCaseData(caseUid); + } }, []); return ( {caseData === undefined ? ( - Loading... + ) : ( @@ -99,9 +112,12 @@ export default function FileClaimScreen() { - confirmClaimFiled()}> + confirmClaimFiled()} + disabled={queryLoading} + > I’ve already filed a claim! - + {queryLoading ? : } diff --git a/src/app/(BottomTabNavigation)/AllCases/Forms/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/Forms/[caseUid].tsx index 27fe1675..8d472530 100644 --- a/src/app/(BottomTabNavigation)/AllCases/Forms/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/Forms/[caseUid].tsx @@ -6,8 +6,10 @@ import styles from './styles'; import { getAllForms } from './utils'; import ExternalSiteLink from '../../../../Components/ExternalSiteLink/ExternalSiteLink'; import FormListItem from '../../../../Components/FormListItem/FormListItem'; +import LoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; import { fonts } from '../../../../styles/fonts'; import { device } from '../../../../styles/global'; +import { fullStopErrorHandler } from '../../../../supabase/queries/auth'; import { Form, CaseUid } from '../../../../types/types'; export default function FormsScreen() { @@ -19,15 +21,17 @@ export default function FormsScreen() { const [forms, setForms] = useState([]); async function getFormsOnLoad(uid: CaseUid) { - getAllForms(uid).then(data => { - if (data.length > 0) { - setForms(data); - } - }); + getAllForms(uid) + .then(data => { + if (data.length > 0) { + setForms(data); + } + }) + .catch(response => fullStopErrorHandler(response)); } useEffect(() => { - if (caseUid !== undefined) { + if (caseUid) { getFormsOnLoad(caseUid); } }, []); @@ -38,14 +42,18 @@ export default function FormsScreen() { Documents - - item.formUid} - ItemSeparatorComponent={() => } - renderItem={({ item }) => } - /> - + {forms.length === 0 ? ( + + ) : ( + + item.formUid} + ItemSeparatorComponent={() => } + renderItem={({ item }) => } + /> + + )} {caseSite === undefined ? null : ( diff --git a/src/app/(BottomTabNavigation)/AllCases/OptOut/ConfirmOptOut/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/OptOut/ConfirmOptOut/[caseUid].tsx index a0932d17..b694f073 100644 --- a/src/app/(BottomTabNavigation)/AllCases/OptOut/ConfirmOptOut/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/OptOut/ConfirmOptOut/[caseUid].tsx @@ -1,6 +1,6 @@ import { router, useLocalSearchParams } from 'expo-router'; -import React, { useContext } from 'react'; -import { Text, View } from 'react-native'; +import React, { useState } from 'react'; +import { ActivityIndicator, Text, View } from 'react-native'; import CircleCheckWhite from '../../../../../../assets/circle-check-white.svg'; import LittlePerson from '../../../../../../assets/little-person.svg'; @@ -10,24 +10,30 @@ import { ButtonBlack, ButtonWhite, } from '../../../../../Components/AuthButton/AuthButton'; -import { CaseContext } from '../../../../../context/CaseContext'; +import { useCaseContext } from '../../../../../context/CaseContext'; import { fonts } from '../../../../../styles/fonts'; import { device } from '../../../../../styles/global'; import { input } from '../../../../../styles/input'; import { instruction } from '../../../../../styles/instruction'; +import { + fullStopErrorHandler, + resetAndPushToRoute, +} from '../../../../../supabase/queries/auth'; import { CaseUid } from '../../../../../types/types'; function ConfirmOptOut() { const { caseUid } = useLocalSearchParams<{ caseUid: CaseUid }>(); - const { leaveCase } = useContext(CaseContext); + const { leaveCase } = useCaseContext(); + const [queryLoading, setQueryLoading] = useState(false); async function deleteCase() { - if (caseUid !== undefined) { - await leaveCase(caseUid); - router.push({ - pathname: '/AllCases', - }); + setQueryLoading(true); + if (caseUid) { + await leaveCase(caseUid) + .then(() => resetAndPushToRoute('/AllCases')) + .catch(response => fullStopErrorHandler(response)); } + setQueryLoading(false); } return ( @@ -72,9 +78,14 @@ function ConfirmOptOut() { - + - + {queryLoading ? : } Continue diff --git a/src/app/(BottomTabNavigation)/AllCases/OptOut/[caseUid].tsx b/src/app/(BottomTabNavigation)/AllCases/OptOut/[caseUid].tsx index 3ee1cce9..e68d786b 100644 --- a/src/app/(BottomTabNavigation)/AllCases/OptOut/[caseUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/OptOut/[caseUid].tsx @@ -10,9 +10,11 @@ import { ButtonBlack, ButtonWhite, } from '../../../../Components/AuthButton/AuthButton'; +import ScreenLoadingComponent from '../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; import { fonts } from '../../../../styles/fonts'; import { device } from '../../../../styles/global'; import { instruction } from '../../../../styles/instruction'; +import { fullStopErrorHandler } from '../../../../supabase/queries/auth'; import { getCaseById } from '../../../../supabase/queries/cases'; import { Case, CaseUid } from '../../../../types/types'; import { openUrl } from '../utils'; @@ -21,11 +23,10 @@ export default function OptOutScreen() { const { caseUid } = useLocalSearchParams<{ caseUid: CaseUid }>(); const [caseData, setCaseData] = useState(); - async function fetchCaseData() { - if (caseUid) { - const caseData = await getCaseById(caseUid); - setCaseData(caseData); - } + async function fetchCaseData(caseUid: CaseUid) { + await getCaseById(caseUid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); } function navigateToOptOutLink() { @@ -36,13 +37,15 @@ export default function OptOutScreen() { } useEffect(() => { - fetchCaseData(); + if (caseUid) { + fetchCaseData(caseUid); + } }, []); return ( {caseData === undefined ? ( - Loading... + ) : ( diff --git a/src/app/(BottomTabNavigation)/AllCases/Updates/UpdateView/[updateUid].tsx b/src/app/(BottomTabNavigation)/AllCases/Updates/UpdateView/[updateUid].tsx index 742514d0..fd8d6120 100644 --- a/src/app/(BottomTabNavigation)/AllCases/Updates/UpdateView/[updateUid].tsx +++ b/src/app/(BottomTabNavigation)/AllCases/Updates/UpdateView/[updateUid].tsx @@ -4,8 +4,10 @@ import { View, Text, ScrollView } from 'react-native'; import styles from './styles'; import NotificationBell from '../../../../../../assets/red-notification-bell.svg'; +import LoadingComponent from '../../../../../Components/ScreenLoadingComponent/ScreenLoadingComponent'; import { fonts } from '../../../../../styles/fonts'; import { device, shawdowStyles } from '../../../../../styles/global'; +import { fullStopErrorHandler } from '../../../../../supabase/queries/auth'; import { getUpdateById } from '../../../../../supabase/queries/updates'; import { Update, UpdateUid } from '../../../../../types/types'; import { formatDate } from '../../utils'; @@ -17,8 +19,9 @@ export default function UpdateView() { const [update, setUpdate] = useState(); async function getUpdate(uid: UpdateUid) { - const update = await getUpdateById(uid); - setUpdate(update); + await getUpdateById(uid) + .then(update => setUpdate(update)) + .catch(response => fullStopErrorHandler(response)); } useEffect(() => { @@ -30,7 +33,7 @@ export default function UpdateView() { return ( {update === undefined ? ( - Loading... + ) : ( <> ([]); async function getUpdatesOnLoad(uid: CaseUid) { - fetchAllUpdates(uid).then(data => { - setUpdates(data); - }); + fetchAllUpdates(uid) + .then(data => setUpdates(data)) + .catch(response => fullStopErrorHandler(response)); } async function getCaseData(uid: CaseUid) { - const caseData = await getCaseById(uid); - setCaseData(caseData); + await getCaseById(uid) + .then(caseData => setCaseData(caseData)) + .catch(response => fullStopErrorHandler(response)); } useEffect(() => { @@ -39,7 +42,7 @@ export default function UpdatesScreen() { return ( {isLoading || caseData === undefined ? ( - Loading... + ) : ( (); - const { allCases, loading } = useContext(CaseContext); + const { allCases, loading } = useCaseContext(); // const [url, setUrl] = useState(null); @@ -95,7 +96,7 @@ function CasesScreen() { {loading ? ( - Loading... + ) : ( (); - const { joinCase } = useContext(CaseContext); + const { joinCase } = useCaseContext(); const [caseData, setCaseData] = useState(); - - const addToCases = async (newCase: Case) => { - await joinCase(newCase); - router.back(); - router.replace('/AllCases'); - }; + const [queryLoading, setQueryLoading] = useState(false); const getCase = async (uid: CaseUid) => { const caseData = await getCaseById(uid); setCaseData(caseData); }; + const addToCases = async (newCase: Case) => { + setQueryLoading(true); + await joinCase(newCase) + .then(() => { + router.back(); + router.replace('/AllCases'); + setQueryLoading(false); + }) + .catch(response => fullStopErrorHandler(response)); + }; + useEffect(() => { if (caseUid !== undefined) { getCase(caseUid); @@ -42,7 +50,7 @@ export default function AddCase() { return ( {caseData === undefined ? ( - Loading... + ) : ( <> @@ -60,11 +68,12 @@ export default function AddCase() { addToCases(caseData)} + disabled={queryLoading} $halfWidth $centeredContent > - + {queryLoading ? : } Add Case diff --git a/src/app/(BottomTabNavigation)/QRCodeScanner/index.tsx b/src/app/(BottomTabNavigation)/QRCodeScanner/index.tsx index b1c36789..a42ab0ba 100644 --- a/src/app/(BottomTabNavigation)/QRCodeScanner/index.tsx +++ b/src/app/(BottomTabNavigation)/QRCodeScanner/index.tsx @@ -5,8 +5,8 @@ import { } from 'expo-camera'; import { router, useNavigation } from 'expo-router'; import { debounce } from 'lodash'; -import React, { useEffect, useState, useContext, useCallback } from 'react'; -import { Text, View } from 'react-native'; +import React, { useEffect, useState, useCallback } from 'react'; +import { ActivityIndicator, Text, View } from 'react-native'; import Toast, { ErrorToast, SuccessToast, @@ -19,7 +19,7 @@ import CheckIcon from '../../../../assets/green-check.svg'; import Arrow from '../../../../assets/right-arrow-white.svg'; import ErrorIcon from '../../../../assets/warning.svg'; import { ButtonBlack } from '../../../Components/AuthButton/AuthButton'; -import { CaseContext } from '../../../context/CaseContext'; +import { useCaseContext } from '../../../context/CaseContext'; import { fonts } from '../../../styles/fonts'; import { device } from '../../../styles/global'; import { getScannedData } from '../../../supabase/queries/cases'; @@ -60,7 +60,8 @@ export default function QRCodeScannerScreen() { const [scannerState, setScannerState] = useState< 'valid' | 'invalid' | 'scanned' | '' >(''); - const { allCases } = useContext(CaseContext); + const [queryLoading, setQueryLoading] = useState(false); + const { allCases } = useCaseContext(); const navigation = useNavigation(); @@ -143,25 +144,32 @@ export default function QRCodeScannerScreen() { } const routeToAddCase = () => { + setQueryLoading(true); if (scannedCase) { router.push(`/QRCodeScanner/AddCase/${scannedCase.id}`); } + setQueryLoading(false); }; useEffect(() => { requestPermission(); - }); + }, []); useEffect(() => { - navigation.addListener('blur', async () => { + const blurListener = navigation.addListener('blur', async () => { resetScanner(); setScannedCase(undefined); }); - navigation.addListener('focus', async () => { + const focusListener = navigation.addListener('focus', async () => { resetScanner(); setScannedCase(undefined); setTimeout(() => {}, 1000); }); + // unsubscribe listeners + return () => { + blurListener(); + focusListener(); + }; }, [navigation]); return !permission ? ( @@ -196,9 +204,10 @@ export default function QRCodeScannerScreen() { routeToAddCase()} + disabled={queryLoading} > View Case - + {queryLoading ? : } )} diff --git a/src/app/index.tsx b/src/app/index.tsx index 80c6ceea..6af753a7 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -1,4 +1,3 @@ -import { router } from 'expo-router'; import { useEffect } from 'react'; import supabase from '../supabase/createClient'; @@ -6,13 +5,7 @@ import { registerForPushNotifications, updatePushToken, } from '../supabase/pushNotifications'; - -const resetAndPushToRouter = (path: string) => { - while (router.canGoBack()) { - router.back(); - } - router.replace(path); -}; +import { resetAndPushToRoute } from '../supabase/queries/auth'; function StartScreen() { useEffect(() => { @@ -20,10 +13,10 @@ function StartScreen() { // determine routing based on supabase auth events if (session) { if (_event !== 'USER_UPDATED') { - resetAndPushToRouter('/AllCases'); + resetAndPushToRoute('/AllCases'); } } else { - resetAndPushToRouter('/Welcome'); + resetAndPushToRoute('/Welcome'); } // generate a new push token on sign in if (session && _event === 'SIGNED_IN') { diff --git a/src/context/CaseContext.tsx b/src/context/CaseContext.tsx index 39e909da..023ec28e 100644 --- a/src/context/CaseContext.tsx +++ b/src/context/CaseContext.tsx @@ -1,17 +1,28 @@ -import React, { createContext, useEffect, useMemo } from 'react'; +import React, { + createContext, + useEffect, + useMemo, + useContext, + useState, +} from 'react'; import { fetchAllCases } from '../app/(BottomTabNavigation)/AllCases/utils'; import supabase from '../supabase/createClient'; -import { addCase, removeCase } from '../supabase/queries/cases'; -import { Case, CaseUid } from '../types/types'; - -export const CaseContext = createContext({} as CaseState); +import { Case, CaseUid, ClaimStatus, UserUid } from '../types/types'; export interface CaseState { allCases: Case[]; loading: boolean; joinCase: (newCase: Case) => Promise; leaveCase: (targetCase: CaseUid) => Promise; + getClaimStatus: (caseId: CaseUid) => Promise; + updateClaimStatus: (caseId: CaseUid, status: ClaimStatus) => Promise; +} + +export const CaseContext = createContext({} as CaseState); + +export function useCaseContext() { + return useContext(CaseContext); } export function CaseContextProvider({ @@ -19,21 +30,21 @@ export function CaseContextProvider({ }: { children: React.ReactNode; }) { - const [cases, setCases] = React.useState([]); - const [isLoading, setIsLoading] = React.useState(true); + const [cases, setCases] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [userId, setUserId] = useState(); useEffect(() => { const fetchCases = async () => { - const user = await supabase.auth.getUser(); - if (user.data.user?.id) { - const userId = user.data.user.id; - const allCases = await fetchAllCases(userId); + const { data } = await supabase.auth.getUser(); + if (data.user) { + const allCases = await fetchAllCases(data.user.id); + setUserId(data.user.id); setCases(allCases); } setIsLoading(false); }; fetchCases(); - // TODO: Might want to put something in dependency array when implementing refresh }, []); /** @@ -42,43 +53,95 @@ export function CaseContextProvider({ */ async function joinCase(newCase: Case) { try { - const { - data: { user }, - } = await supabase.auth.getUser(); - const userUid = user?.id; - if (userUid) { - await addCase(newCase.id, userUid); + if (isLoading || !userId) { + throw new Error('User not found'); + } + if (userId) { + await supabase.from('status').insert({ caseId: newCase.id, userId }); setCases([...cases, newCase]); } } catch (error) { - console.warn(error); + console.warn('(joinCase)', error); + throw error; } } /** * Remove this case from a user's list of cases, both locally and on supabase. - * @param caseUid case being removed. + * @param caseId case being removed. */ - async function leaveCase(caseUid: CaseUid) { + async function leaveCase(caseId: CaseUid) { try { - const { - data: { user }, - } = await supabase.auth.getUser(); - const userUid = user?.id; - if (userUid) { - await removeCase(caseUid, userUid); - let targetIndex = -1; - for (let i = 0; i < cases.length; i++) { - if (cases[i].id === caseUid) { - targetIndex = i; - } - } - if (targetIndex > -1) { - cases.splice(targetIndex, 1); + if (isLoading || !userId) { + throw new Error('User not found'); + } + await supabase + .from('status') + .delete() + .eq('userId', userId) + .eq('caseId', caseId); + + let targetIndex = -1; + for (let i = 0; i < cases.length; i++) { + if (cases[i].id === caseId) { + targetIndex = i; } } + if (targetIndex > -1) { + cases.splice(targetIndex, 1); + } + } catch (error) { + console.warn('(leaveCase)', error); + throw error; + } + } + + /** + * Fetch the claim status for the user in a given `caseId`. + * @param caseId target case. + * @returns object describing the claim status. + */ + async function getClaimStatus(caseId: CaseUid): Promise { + try { + if (isLoading || !userId) { + throw new Error('User not found'); + } + const { data } = await supabase + .from('status') + .select() + .eq('userId', userId) + .eq('caseId', caseId); + if (!data) { + throw new Error('Claim status not found'); + } + return data[0].claimStatus; + } catch (error) { + console.warn('(getClaimStatus)', error); + throw error; + } + } + + /** + * Update the user's claim status for a given `caseId`. + * @param caseId specified caseId + * @param status status to be updated in the specific User/Case row + */ + async function updateClaimStatus( + caseId: CaseUid, + status: ClaimStatus, + ): Promise { + try { + if (isLoading || !userId) { + throw new Error('User not found'); + } + await supabase + .from('status') + .update({ claimStatus: status }) + .eq('userId', userId) + .eq('caseId', caseId); } catch (error) { - console.warn(error); + console.warn('(updateClaimStatus)', error); + throw error; } } @@ -88,6 +151,8 @@ export function CaseContextProvider({ loading: isLoading, joinCase, leaveCase, + getClaimStatus, + updateClaimStatus, }), [cases, setCases, isLoading], ); diff --git a/src/supabase/queries/auth.ts b/src/supabase/queries/auth.ts index ac1f58ff..dc03d165 100644 --- a/src/supabase/queries/auth.ts +++ b/src/supabase/queries/auth.ts @@ -1,5 +1,14 @@ +import { router } from 'expo-router'; +import { isError } from 'lodash'; +import { Alert } from 'react-native'; + import supabase from '../createClient'; +/** + * Checks whether the provided email is associated with an account in the public users table. + * @param email + * @returns whether the email exists + */ export const emailExists = async (email: string): Promise => { try { const { data } = await supabase.from('users').select().eq('email', email); @@ -13,3 +22,49 @@ export const emailExists = async (email: string): Promise => { throw error; } }; + +/** + * Clear the stack to prevent users going back. Allows push animation to be used. + * @param path + */ +export function resetAndPushToRoute(path: string) { + while (router.canGoBack()) { + router.back(); + } + router.replace(path); +} + +/** + * Clear the stack and route users to their home screen (relative to their login status). + */ +export async function resetAndPushToHome() { + const { data } = await supabase.auth.getSession(); + while (router.canGoBack()) { + router.back(); + } + if (data.session) { + router.replace('/AllCases'); + } else { + router.replace('/Welcome'); + } +} + +/** + * Alerts the user that an error has occurred. Routes them away from the erroring screen. + * @param response error object caught and passed to this handler. + */ +export function fullStopErrorHandler(response: any) { + let alertMessage: string; + if (isError(response)) { + alertMessage = 'Message: ' + response.message; + } else { + alertMessage = 'Unknown Error'; + } + Alert.alert('An Error Has Occurred', alertMessage, [ + { + text: 'Navigate Home', + onPress: () => resetAndPushToHome(), + style: 'cancel', + }, + ]); +} diff --git a/src/supabase/queries/cases.ts b/src/supabase/queries/cases.ts index 6fab7d4d..f8e50781 100644 --- a/src/supabase/queries/cases.ts +++ b/src/supabase/queries/cases.ts @@ -2,8 +2,8 @@ import { Case, CasePartial, CaseUid, - Eligibility, UserUid, + ScannerQueryResponse, } from '../../types/types'; import supabase from '../createClient'; @@ -52,12 +52,16 @@ export async function getAllCaseIds(): Promise { } return data.map(item => item.caseId as CaseUid); } catch (error) { - console.warn(error); + console.warn('(getAllCaseIds)', error); throw error; } } -// Fetch a single case using its ID +/** + * Fetch a single `Case` by it's id. + * @param caseId target case id. + * @returns `Case` data. + */ export async function getCaseById(caseId: CaseUid): Promise { try { const { data } = await supabase.from('cases').select().eq('caseId', caseId); @@ -71,16 +75,6 @@ export async function getCaseById(caseId: CaseUid): Promise { } } -type ScannerQueryResponse = - | { - data: { case: Case }; - error: null; - } - | { - data: null; - error: any; - }; - /** * Query supabase according to the QR code scanner result. * @param scannedData @@ -114,41 +108,6 @@ export async function getScannedData( } } -/** - * Create a case-user association on supabase. - * @param caseId case being added. - * @param userId user joining that case. - */ -export async function addCase(caseId: CaseUid, userId: UserUid): Promise { - try { - await supabase.from('status').insert({ caseId, userId }); - } catch (error) { - console.warn(error); - throw error; - } -} - -/** - * Remove a case-user association from supabase. - * @param caseId case to be removed. - * @param userId user leaving the case. - */ -export async function removeCase( - caseId: CaseUid, - userId: UserUid, -): Promise { - try { - await supabase - .from('status') - .delete() - .eq('userId', userId) - .eq('caseId', caseId); - } catch (error) { - console.warn(error); - throw error; - } -} - /** * Fetch the Case objects corresponding to an array of `CaseId`s. Fetches cases from `cases` table. * @@ -180,6 +139,12 @@ export async function getCasesByIds(caseIds: CaseUid[]): Promise { } } +/** + * Format raw case data from supabase into `Case` object. + * + * @param item raw supabase data. + * @returns `Case` object. + */ export async function formatCase(item: any): Promise { const partialCase = formatPartialCaseFromQuery(item); const imageUrl = await getImageUrl(partialCase.id); @@ -189,6 +154,7 @@ export async function formatCase(item: any): Promise { }; return caseData; } + /** * Parse supabase case query and return `CasePartial` object. * @@ -214,56 +180,6 @@ export function formatPartialCaseFromQuery(item: any): CasePartial { return formattedPartial; } -/** - * Update a specific User/Case status - * - * @param caseId specified caseId - * @param status status to be updated in the specific User/Case row - * @returns nothing - */ -export async function updateCaseStatus( - caseId: CaseUid, - status: Eligibility, -): Promise { - try { - const { - data: { user }, - } = await supabase.auth.getUser(); - const userId = user?.id; - await supabase - .from('status') - .update({ eligibility: status }) - .eq('userId', userId) - .eq('caseId', caseId); - } catch (error) { - // eslint-disable-next-line no-console - console.warn('(updateCaseStatus)', error); - throw error; - } -} - -export async function getCaseStatus(caseId: CaseUid): Promise { - try { - const { - data: { user }, - } = await supabase.auth.getUser(); - const userId = user?.id; - const { data } = await supabase - .from('status') - .select() - .eq('userId', userId) - .eq('caseId', caseId); - if (!data) { - throw new Error('Status not found'); - } - return data[0].eligibility; - } catch (error) { - // eslint-disable-next-line no-console - console.warn('(getCaseStatus)', error); - throw error; - } -} - /** * Fetch image from Supabase storage and return its public URL. * diff --git a/src/supabase/queries/forms.ts b/src/supabase/queries/forms.ts index 0606ea18..35a392de 100644 --- a/src/supabase/queries/forms.ts +++ b/src/supabase/queries/forms.ts @@ -106,7 +106,7 @@ export async function formatForm( }; return completeForm; } catch (error) { - // propogate error downward for handling + console.warn('(formatForm)', error); throw error; } } diff --git a/src/types/types.tsx b/src/types/types.tsx index 40423197..ff12228e 100644 --- a/src/types/types.tsx +++ b/src/types/types.tsx @@ -62,7 +62,7 @@ export interface EligibilityRequirement { requirement: string; } -export enum Eligibility { +export enum ClaimStatus { ELIGIBLE = 'ELIGIBLE', INELIGIBLE = 'INELIGIBLE', UNDETERMINED = 'UNDETERMINED', @@ -106,3 +106,13 @@ export enum YellowStatusOptions { export enum RedStatusOptions { 'Action Required', } + +export type ScannerQueryResponse = + | { + data: { case: Case }; + error: null; + } + | { + data: null; + error: any; + };