From d003ca74294021a9a09e1b2beea7a52405a935ee Mon Sep 17 00:00:00 2001 From: Quan Vo <45813912+quanvo298Wizeline@users.noreply.github.com> Date: Thu, 12 Sep 2024 01:52:33 +0700 Subject: [PATCH] WALLET AccessPrompt Confirm request access (#46) * Show loading, and handle error * Show loading, and handle error * Handle error * Show loading when search Resource and handle error * Revert icon file * Revert file aoo.config.js * fixup! Revert file aoo.config.js --------- Co-authored-by: Nicolas Ayral Seydoux Co-authored-by: Chelsea Pinka <4137008+chelseapinka@users.noreply.github.com> --- api/apiRequest.ts | 5 +++ app/(tabs)/home/download.tsx | 4 +- app/_layout.tsx | 43 ++++++++++---------- app/access-prompt/index.tsx | 32 ++++++++++++++- app/scan-qr.tsx | 3 ++ assets/images/close.svg | 2 + components/error/ErrorPopup.tsx | 69 +++++++++++++++++++++++++++++++++ hooks/useError.tsx | 53 +++++++++++++++++++++++++ 8 files changed, 188 insertions(+), 23 deletions(-) create mode 100644 assets/images/close.svg create mode 100644 components/error/ErrorPopup.tsx create mode 100644 hooks/useError.tsx diff --git a/api/apiRequest.ts b/api/apiRequest.ts index e8f5a0e..d39887f 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -51,6 +51,11 @@ export const makeApiRequest = async ( router.replace("/login?logout=true"); throw new Error(`Unauthorized: ${response.status}`); } + + if (response.status === 404) { + return null as T; + } + if (!response.ok) { throw new Error(`Network response was not ok: ${response.statusText}`); } diff --git a/app/(tabs)/home/download.tsx b/app/(tabs)/home/download.tsx index 96803c6..ba389e3 100644 --- a/app/(tabs)/home/download.tsx +++ b/app/(tabs)/home/download.tsx @@ -25,6 +25,7 @@ import IconResourceName from "@/components/common/IconResourceName"; import { RDF_CONTENT_TYPE } from "@/utils/constants"; import type { WalletFile } from "@/types/WalletFile"; import { isDownloadQR } from "@/types/accessPrompt"; +import { useError } from "@/hooks/useError"; interface FileDetailProps { file: WalletFile; @@ -34,6 +35,7 @@ interface FileDetailProps { const Page: React.FC = () => { const params = useLocalSearchParams(); + const { showErrorMsg } = useError(); if (!isDownloadQR(params)) { throw new Error( "Incorrect params for download request: uri and contentType are required" @@ -50,8 +52,8 @@ const Page: React.FC = () => { await queryClient.invalidateQueries({ queryKey: ["files"] }); }, onError: (error) => { - // TODO: there needs to be better error handling here... console.warn(error); + showErrorMsg("Unable to save the file into your Wallet."); }, mutationKey: ["filesMutation"], }); diff --git a/app/_layout.tsx b/app/_layout.tsx index 987b7f8..1e5f2aa 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -31,6 +31,7 @@ import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; import type { AppStateStatus } from "react-native"; import { AppState, Platform } from "react-native"; import { LoginWebViewProvider } from "@/hooks/useInruptLogin"; +import { ErrorViewProvider } from "@/hooks/useError"; // Prevent the splash screen from auto-hiding before asset loading is complete. // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -81,28 +82,30 @@ export default function RootLayout() { - - - + - + > + + + + diff --git a/app/access-prompt/index.tsx b/app/access-prompt/index.tsx index 1a5f81d..e17120e 100644 --- a/app/access-prompt/index.tsx +++ b/app/access-prompt/index.tsx @@ -31,16 +31,18 @@ import { isAccessPromptQR } from "@/types/accessPrompt"; import { faEye } from "@fortawesome/free-solid-svg-icons/faEye"; import CardInfo from "@/components/common/CardInfo"; import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome"; +import Loading from "@/components/LoadingButton"; const Page: React.FC = () => { const params = useLocalSearchParams(); + if (!isAccessPromptQR(params)) { throw new Error( "Incorrect params for access prompt request: webId, client and type are required" ); } const router = useRouter(); - const { data } = useQuery({ + const { data, isLoading, isFetching } = useQuery({ queryKey: ["accessPromptResource"], queryFn: () => getAccessPromptResource({ @@ -87,7 +89,23 @@ const Page: React.FC = () => { }, }); }; - if (!data) return ; + + if (isLoading || isFetching) + return ( + + + + ); + + if (!data) { + return ( + + + Resource not found + + + ); + } return ( @@ -134,6 +152,7 @@ const Page: React.FC = () => { customStyle={styles.button} /> + ); }; @@ -148,6 +167,15 @@ const styles = StyleSheet.create({ content: { flex: 1, }, + emptyState: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + emptyStateText: { + fontSize: 18, + color: Colors.light.grey, + }, title: { paddingTop: 20, paddingBottom: 8, diff --git a/app/scan-qr.tsx b/app/scan-qr.tsx index 52c04fa..1f89e64 100644 --- a/app/scan-qr.tsx +++ b/app/scan-qr.tsx @@ -16,6 +16,7 @@ import { View, StyleSheet, Dimensions, TouchableOpacity } from "react-native"; import { useNavigation, useRouter } from "expo-router"; import { useEffect, useState } from "react"; +import { useError } from "@/hooks/useError"; import { ThemedText } from "@/components/ThemedText"; import type { BarcodeScanningResult } from "expo-camera"; import { Camera, CameraView } from "expo-camera"; @@ -31,6 +32,7 @@ class UnrecognisedQrCodeError extends Error { export default function Logout() { const { goBack } = useNavigation(); + const { showErrorMsg } = useError(); const { replace, navigate } = useRouter(); const [scanned, setScanned] = useState(false); useEffect(() => { @@ -68,6 +70,7 @@ export default function Logout() { throw new UnrecognisedQrCodeError(); } } catch (err) { + showErrorMsg("QR code not a valid format"); if (err instanceof UnrecognisedQrCodeError) { console.warn(err); } else { diff --git a/assets/images/close.svg b/assets/images/close.svg new file mode 100644 index 0000000..3343224 --- /dev/null +++ b/assets/images/close.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/components/error/ErrorPopup.tsx b/components/error/ErrorPopup.tsx new file mode 100644 index 0000000..81a8f2a --- /dev/null +++ b/components/error/ErrorPopup.tsx @@ -0,0 +1,69 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React from "react"; +import { View, StyleSheet, TouchableOpacity } from "react-native"; +import Close from "@/assets/images/close.svg"; +import { ThemedText } from "../ThemedText"; + +interface ErrorPopupProps { + errorMsg: string; + onClose: () => void; +} + +const ErrorPopup: React.FC = ({ + errorMsg, + onClose = () => null, +}) => { + return ( + + {errorMsg} + + + + + ); +}; + +const styles = StyleSheet.create({ + errorPopup: { + position: "absolute", + bottom: 80, + left: 24, + right: 24, + backgroundColor: "white", + justifyContent: "space-between", + alignItems: "center", + flexDirection: "row", + shadowColor: "rgba(0, 0, 0)", + shadowOffset: { width: 2, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 30, + elevation: 5, + borderRadius: 16, + paddingVertical: 16, + }, + loadingText: { + paddingLeft: 16, + fontSize: 16, + }, + closeView: { + alignItems: "center", + paddingLeft: 24, + paddingRight: 16, + }, +}); + +export default ErrorPopup; diff --git a/hooks/useError.tsx b/hooks/useError.tsx new file mode 100644 index 0000000..d559587 --- /dev/null +++ b/hooks/useError.tsx @@ -0,0 +1,53 @@ +// +// Copyright Inrupt Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import React, { createContext, useCallback, useState } from "react"; +import ErrorPopup from "@/components/error/ErrorPopup"; + +const ErrorContext = createContext<{ + showErrorMsg: (msg: string) => void; +}>({ + showErrorMsg: () => null, +}); + +export const ErrorViewProvider = ({ children }: React.PropsWithChildren) => { + const [showError, setShowError] = useState(false); + const [errorMsg, setErrorMsg] = useState(undefined); + + const showErrorMsg = useCallback((msg: string) => { + setShowError(true); + setErrorMsg(msg); + }, []); + + const handleClose = useCallback(() => { + setShowError(false); + setErrorMsg(undefined); + }, []); + + return ( + + {children} + {showError && Boolean(errorMsg) && ( + + )} + + ); +}; + +export const useError = () => React.useContext(ErrorContext);