diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml new file mode 100644 index 0000000..cd33776 --- /dev/null +++ b/.github/workflows/cd.yml @@ -0,0 +1,25 @@ +name: Inrupt Data Wallet CD + +on: + push: + branches: [ main ] + +env: + CI: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: npm + - run: npm ci + - run: npm install -g eas-cli + - run: eas build --platform all --profile preview --message "SNAPSHOT build" + env: + EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + EAS_PROJECT_ID: ${{ secrets.EAS_PROJECT_ID }} + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e72ecf8..2f75dab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Wallet Frontend Application CI +name: Inrupt Data Wallet CI on: pull_request: {} @@ -15,22 +15,21 @@ jobs: lint: uses: inrupt/typescript-sdk-tools/.github/workflows/reusable-lint.yml@v3 - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest ] - node-version: [ "20.x" ] + unit-test: + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 + with: + node-version-file: ".nvmrc" + cache: npm - run: npm ci - run: npm run test:snapshot sonarqube: name: run sonarqube if: ${{ github.actor != 'dependabot[bot]' }} - needs: [build] + needs: [unit-test] runs-on: ubuntu-latest steps: - name: Checking out diff --git a/api/apiRequest.ts b/api/apiRequest.ts index d39887f..5987eb1 100644 --- a/api/apiRequest.ts +++ b/api/apiRequest.ts @@ -15,6 +15,7 @@ // import * as SecureStore from "expo-secure-store"; import { router } from "expo-router"; +import { handleErrorResponse } from "@inrupt/solid-client-errors"; export const SESSION_KEY = "session"; @@ -52,12 +53,12 @@ export const makeApiRequest = async ( 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}`); + throw handleErrorResponse( + response, + await response.text(), + `${endpoint} returned an error response.` + ); } const responseType = response.headers.get("content-type"); diff --git a/api/files.ts b/api/files.ts index 7647ae8..ab12076 100644 --- a/api/files.ts +++ b/api/files.ts @@ -14,7 +14,9 @@ // limitations under the License. // import mime from "mime"; +import FormData from "form-data"; import type { WalletFile } from "@/types/WalletFile"; +import { handleErrorResponse } from "@inrupt/solid-client-errors"; import { makeApiRequest } from "./apiRequest"; interface FileObject { @@ -27,38 +29,63 @@ export const fetchFiles = async (): Promise => { return makeApiRequest("wallet"); }; -export const postFile = async (file: FileObject): Promise => { - const formData = new FormData(); +export const postFile = async (fileMetadata: FileObject): Promise => { + const acceptValue = fileMetadata.type ?? mime.getType(fileMetadata.name); + const acceptHeader = new Headers(); + if (acceptValue !== null) { + acceptHeader.append("Accept", acceptValue); + } + // Make a HEAD request to the file to report on potential errors + // in more details than the fetch with the FormData. + const fileResponse = await fetch(fileMetadata.uri, { + headers: acceptHeader, + method: "HEAD", + }); + if (!fileResponse.ok) { + throw handleErrorResponse( + fileResponse, + await fileResponse.text(), + "Failed to fetch file to upload" + ); + } + // The following is declared as `any` because there is a type inconsistency, + // and the global FormData (expected by the fetch body) is actually not compliant + // with the Web spec and its TS declarations. formData.set doesn't exist, and + // formData.append doesn't support a Blob being passed. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const formData: any = new FormData(); formData.append("file", { - name: file.name, - type: file.type || mime.getType(file.name) || "application/octet-stream", - uri: file.uri, - } as unknown as Blob); - + name: fileMetadata.name, + type: + fileMetadata.type || + mime.getType(fileMetadata.name) || + "application/octet-stream", + uri: fileMetadata.uri, + }); + let response: Response; try { - const response = await fetch( - `${process.env.EXPO_PUBLIC_WALLET_API}/wallet`, + response = await fetch( + new URL("wallet", process.env.EXPO_PUBLIC_WALLET_API), { method: "PUT", body: formData, } ); + } catch (e) { + console.debug("Resolving the file and uploading it to the wallet failed."); + throw e; + } - if (response.ok) { - console.debug( - `Uploaded file to Wallet. HTTP response status:${response.status}` - ); - } else { - throw Error( - `Failed to upload file to Wallet. HTTP response status from Wallet Backend service:${ - response.status - }` - ); - } - } catch (error) { - throw Error("Failed to retrieve and upload file to Wallet", { - cause: error, - }); + if (response.ok) { + console.debug( + `Uploaded file to Wallet. HTTP response status:${response.status}` + ); + } else { + throw handleErrorResponse( + response, + await response.text(), + "Failed to upload file to Wallet" + ); } }; diff --git a/app/(tabs)/home/download.tsx b/app/(tabs)/home/download.tsx index ba389e3..09d2b35 100644 --- a/app/(tabs)/home/download.tsx +++ b/app/(tabs)/home/download.tsx @@ -26,6 +26,7 @@ import { RDF_CONTENT_TYPE } from "@/utils/constants"; import type { WalletFile } from "@/types/WalletFile"; import { isDownloadQR } from "@/types/accessPrompt"; import { useError } from "@/hooks/useError"; +import { hasProblemDetails } from "@inrupt/solid-client-errors"; interface FileDetailProps { file: WalletFile; @@ -52,7 +53,15 @@ const Page: React.FC = () => { await queryClient.invalidateQueries({ queryKey: ["files"] }); }, onError: (error) => { - console.warn(error); + if (hasProblemDetails(error)) { + console.debug( + `${error.problemDetails.status}: ${error.problemDetails.title}.` + ); + console.debug(error.problemDetails.detail); + } else { + console.debug("A non-HTTP error happened."); + console.debug(error); + } showErrorMsg("Unable to save the file into your Wallet."); }, mutationKey: ["filesMutation"], diff --git a/app/access-prompt/index.test.tsx b/app/access-prompt/index.test.tsx index fafac42..876b7de 100644 --- a/app/access-prompt/index.test.tsx +++ b/app/access-prompt/index.test.tsx @@ -30,6 +30,7 @@ function mockUseQuery( ): ReturnType { return { data, + error: null, isLoading: false, isFetching: false, refetch: jest.fn["refetch"]>(), diff --git a/app/access-prompt/index.tsx b/app/access-prompt/index.tsx index e17120e..9f52ab5 100644 --- a/app/access-prompt/index.tsx +++ b/app/access-prompt/index.tsx @@ -32,6 +32,7 @@ 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"; +import { hasProblemDetails } from "@inrupt/solid-client-errors"; const Page: React.FC = () => { const params = useLocalSearchParams(); @@ -42,14 +43,16 @@ const Page: React.FC = () => { ); } const router = useRouter(); - const { data, isLoading, isFetching } = useQuery({ - queryKey: ["accessPromptResource"], - queryFn: () => - getAccessPromptResource({ - type: params.type, - webId: params.webId, - }), - }); + const { data, error, isLoading, isFetching } = useQuery( + { + queryKey: ["accessPromptResource"], + queryFn: () => + getAccessPromptResource({ + type: params.type, + webId: params.webId, + }), + } + ); const navigation = useNavigation(); const mutation = useMutation({ mutationFn: requestAccessPrompt, @@ -90,16 +93,13 @@ const Page: React.FC = () => { }); }; - if (isLoading || isFetching) - return ( - - - - ); - - if (!data) { + if ( + error !== null && + hasProblemDetails(error) && + error.problemDetails.status === 404 + ) { return ( - + Resource not found @@ -107,6 +107,15 @@ const Page: React.FC = () => { ); } + if (isLoading || isFetching) + return ( + + + + ); + + if (!data) return ; + return ( diff --git a/package-lock.json b/package-lock.json index 3df4e7f..db2428e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-native-fontawesome": "^0.3.2", "@gorhom/bottom-sheet": "^4.6.4", + "@inrupt/solid-client-errors": "^0.0.2", "@react-native-cookies/cookies": "^6.2.1", "@react-navigation/native": "^6.1.18", "@tanstack/react-query": "^5.51.23", @@ -3734,6 +3735,14 @@ "node": ">=14.17" } }, + "node_modules/@inrupt/solid-client-errors": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@inrupt/solid-client-errors/-/solid-client-errors-0.0.2.tgz", + "integrity": "sha512-Nhq39DJMKDMc35/VFT168v9JwuKzfzCHPN4fYYAE/Q0ECtM6PuBGT7nu0gZ06+S0pZQasHDyTkOGXRIx+zkvJA==", + "engines": { + "node": "^18.0.0 || ^20.0.0 || ^22.0.0" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index a9e4978..3898bdc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/react-native-fontawesome": "^0.3.2", "@gorhom/bottom-sheet": "^4.6.4", + "@inrupt/solid-client-errors": "^0.0.2", "@react-native-cookies/cookies": "^6.2.1", "@react-navigation/native": "^6.1.18", "@tanstack/react-query": "^5.51.23",