Fetching PDF data from API and displaying as PDF in a device's default browser using React Native Expo

1.8k Views Asked by At

I have a React Native project created using Expo where I need to make an API call to get a PDF document and display it in the default web browser of my device. The API call works without any issues in the other applications where it is utilized and returns data which has a content type of "application/json" when called. Once the data is returned from the API call, I need to display that PDF document in the device's browser (I am currently testing on iOS).

This is the API call ("API_URL" and "AUTH_TOKEN" are placeholders for the actual API url and user's auth token for authentication):

const res = await fetch(
  'API_URL', // API_URL changes based on the user logged in and is NOT a static URL
  {
    method: "GET",
    headers: {
      "content-type": "application/json",
      Authorization: 'AUTH_TOKEN',
    }
  }
);

It returns the following (with the URL, blob IDs, name, etc. omitted):

{"type":"default","status":200,"ok":true,"statusText":"","headers":{"map":{"cache-control":"no-cache","content-length":"185942","content-type":"application/json; charset=utf-8","date":"Mon, 22 May 2023 18:00:06 GMT","expires":"-1","pragma":"no-cache","server":"Microsoft-IIS/10.0","x-aspnet-version":"4.0.30319","x-powered-by":"ASP.NET"}},"url":"API_URL","bodyUsed":false,"_bodyInit":{"_data":{"size":185942,"offset":0,"blobId":"BLOB_ID","type":"application/json","name":"NAME.json","__collector":{}}},"_bodyBlob":{"_data":{"size":185942,"offset":0,"blobId":"BLOB_ID","type":"application/json","name":"NAME.json","__collector":{}}}}

I'm able to successfully get data from the API, but I am unsure how to proceed with displaying this data as a PDF without ejecting from Expo, therefore I can't make use of libraries like react-native-pdf, rn-fetch-blob, etc.

I've read through several different solutions for similar questions but as I am still learning, most of those questions didn't seem to apply to what I'm trying to do. Several questions that I found regarding using Expo to do this such as this question suggested using expo-file-system and expo-sharing but when successfully implemented, this only brings up the share menu with options like airdropping, printing, and sending the file via email, where I want to open it in a web browser on the device.

I did try to make use of FileSystem.downloadAsync() as mentioned in Solution 1 of this answer but was unable to download the file due to an authorization error with the API.

This answer mentions using Linking to open the URL which I am not able to successfully do as the URL is a local file. This is my current attempt at doing so inside of my api.js file where I make the API calls for my app:

const res = await fetch(
  'API_URL,
  {
    method: "GET", 
    headers: {
      "content-type": "application/json",
      Authorization: 'AUTH_TOKEN',
    }
  }
); //This successfully returns JSON data

  const fileName = res._bodyBlob._data.name;
  const buff = Buffer.from(JSON.stringify(res._bodyBlob), "base64");
  const base64 = buff.toString("base64");

  const fileUri =
    FileSystem.documentDirectory + `${encodeURI("document")}.pdf`;

  await FileSystem.writeAsStringAsync(fileUri, base64, {
    encoding: FileSystem.EncodingType.Base64,
  });

  Linking.openURL(fileUri);

Executing this results in the following error:

Error: Unable to open URL: file:///var/mobile/Containers/Data/Application/164552D9-15EC-43DD-8539-62F5FC30EFF0/Documents/ExponentExperienceData/%2540anonymous%252Fmy-project-4427d22d-8683-4006-a8d3-75402adefad9/document.pdf

I'll be the first to admit this code is a hodgepodge of ideas from several different posts over the span of a few days so I'm unsure if FileSystem is how I should approach accomplishing this or if local files are even able to be opened through Linking. If not, what would be the best way to fetch PDF data from the API and display it in the device's web browser?

Thanks in advance!

EDIT: I've implemented the solution mentioned by Plipus here inside my api.js file and am experiencing a total app crash. I've added some debug lines to narrow down where it's happening at and have included the output below.

export const getPDF = async () => {
  try {
    const response = await fetch(
      'API_URL,
    {
        method: "GET",
        headers: {
          "content-type": "application/json",
          Authorization: 'AUTH_TOKEN',
        }
      }
    ); //This successfully returns JSON data

    console.log('step 1: response to blob');
    const pdfData = await response.blob();

    console.log('step 2: create file URI');
    const fileUri = FileSystem.cacheDirectory + 'temp.pdf';
    console.log(fileUri);
    console.log('step 3: write as string async'); //crash happens after this line

    await FileSystem.writeAsStringAsync(fileUri, pdfData, {
      encoding: FileSystem.EncodingType.Base64,
    }); //possible cause of crash

    console.log('step 4: open web browser');
    await WebBrowser.openBrowserAsync(fileUri);

  } catch (error) {
    console.error('Error fetching or displaying PDF:', error);
  }

Console Output:

 LOG  step 1: response to blob
 LOG  step 2: create file URI
 LOG  file:///var/mobile/Containers/Data/Application/164552D9-15EC-43DD-8539-62F5FC30EFF0/Library/Caches/ExponentExperienceData/%2540anonymous%252Fmy-project-4427d22d-8683-4006-a8d3-75402adefad9/temp.pdf       
 LOG  step 3: write as string async

I believe the FileSystem.writeAsStringAsync line is the culprit as the "step 3" debug line is the last to be hit before the crash happens.

1

There are 1 best solutions below

0
On BEST ANSWER

I was able to implement a solution to open the PDF in a component instead of the device's default browser using RN-PDF-Reader. This worked perfectly for what I needed in my own application, so while it's not the device browser like originally planned, I figured it's worth sharing.

Updated Method:

export const getPDFData = async () => {
  try {
    const response = await fetch(
      'API_URL,
    {
        method: "GET",
        headers: {
          "content-type": "application/json",
          Authorization: 'AUTH_TOKEN',
        }
      }
    ); //get JSON object containing file contents

  const data = await response.json(); //reads the PDF data from the object
  
  let pdfData = `data:application/pdf;base64,${responseData}`; //formatted for use with PdfReader library
  
  return pdfData;

  } catch (error) {
    console.error('Error fetching or displaying PDF:', error);
  }
}

With the string returned above, this can be implemented inside of a PDFReader component wherever needed like so:

PDFReader Component:

//Don't forget to import PDFReader using "import PDFReader from 'rn-pdf-reader-js';"

<PDFReader
    source={{ base64: pdfData }} //"pdfData" is the data returned from getPDFData()
/>

The file contents from the API were already contained in a Base64 string, but in order to actually access the contents in code for what I needed, I was missing the following line: const data = await response.json(); For the sake of conciseness, I didn't include some of the additional overhead that exists in my app involving using state, etc., but hope this little snippet helps.