How to Save a PDF from an API Response in React Native
Welcome, fellow React Native enthusiasts! Today, we’re diving into the thrilling world of converting a byte array PDF from an API response into a file saved on your user’s device. In this simple implementation, we would mimic downloading an account statement. So, buckle up, because we’re about to make saving PDFs as easy as frying an egg (or boiling water lol 😊).
Setting Up the Environment
First, let’s get our ducks in a row by importing the necessary libraries and components:
import { useState } from 'react';
import Container from '../../components/container';
import {
View,
Text,
Platform,
Button as RNButton,
} from 'react-native';
import { styles } from './styles';
import RNPickerSelect from 'react-native-picker-select';
import Button from '../../components/button';
import DateTimePicker, { DateTimePickerEvent } from '@react-native-community/datetimepicker';
import { colors } from '../../assets/colors';
import axios from 'axios';
import RNFS from 'react-native-fs';
import * as base64 from 'base64-js';
import Share from 'react-native-share';
Why These Components?
- useState: To manage the state of our app, such as selected dates, plan name, and loading status.
- Container: A custom component for consistent layout styling.
- View, Text, Button: Basic building blocks for UI in React Native.
- RNPickerSelect: A simple and customizable picker component for selecting plans.
- Button: A custom button component for consistent styling and functionality.
- DateTimePicker: A cross-platform date picker for selecting dates.
- axios: For making HTTP requests to download the account statement.
- RNFS: React Native File System for handling file operations.
- base64: For converting ArrayBuffer to base64.
- Share: For sharing the downloaded PDF file.
Defining Plans
For simplicity, we’ll define an array of plans directly in the code:
const plans = [
{ id: 1, planName: 'Plan A' },
{ id: 2, planName: 'Plan B' },
{ id: 3, planName: 'Plan C' },
];
Why Define Plans Here?
We define the plans array directly to keep the example simple and focused. In a real application, this data might come from an API. Plus, who needs a server call to tell us about Plan A, B, or C?
Managing State
We’ll use the useState
hook to manage various pieces of state in our component:
const GenerateAccountStatement = () => {
const [fromRawDate, setFromRawDate] = useState<Date>(new Date());
const [toRawDate, setToRawDate] = useState<Date>(new Date());
const [showFromDatePicker, setShowFromDatePicker] = useState(false);
const [showToDatePicker, setShowToDatePicker] = useState(false);
const [planName, setPlanName] = useState('');
const [planId, setPlanId] = useState(0);
const [fromFormattedDate, setFromFormattedDate] = useState('');
const [toFormattedDate, setToFormattedDate] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
Why These State Variables?
- fromRawDate, toRawDate: To store the selected dates.
- showFromDatePicker, showToDatePicker: To control the visibility of date pickers.
- planName, planId: To store the selected plan’s name and ID.
- fromFormattedDate, toFormattedDate: To store formatted dates (as required by the backend) for API requests.
- isLoading: To manage the loading state during the download process.
- error: To store and display error messages.
Handling Plan Selection
We use RNPickerSelect
to allow users to select a plan:
const handlePlanSelection = (value: string) => {
setPlanName(value);
const selectedId = plans.find(plan => plan.planName === value)?.id;
setPlanId(selectedId || 0);
};
Why RNPickerSelect?RNPickerSelect
is a flexible and easy-to-use component for creating dropdowns in React Native. It simplifies the process of selecting an item from a list. Because let's face it, dropdowns should be the least of our worries!
Handling Date Changes
We use DateTimePicker
to allow users to select dates:
const handleFromDateChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
if (selectedDate) {
setFromRawDate(selectedDate);
const formattedDate = selectedDate.toISOString().split('T')[0];
setFromFormattedDate(formattedDate);
}
setShowFromDatePicker(false);
};
const handleToDateChange = (event: DateTimePickerEvent, selectedDate?: Date) => {
if (selectedDate) {
setToRawDate(selectedDate);
const formattedDate = selectedDate.toISOString().split('T')[0];
setToFormattedDate(formattedDate);
}
setShowToDatePicker(false);
};
Why DateTimePicker?DateTimePicker
provides a consistent and user-friendly way to select dates on both Android and iOS platforms. Because nothing says "professional" like a good date picker.
Completing the Download
The core functionality involves making an API request to download the account statement and handling the response. Here’s the complete function to handle this:
const completeDownloadStatement = async () => {
setError('');
if (!planId || !planName || !fromFormattedDate || !toFormattedDate) {
setError('Please select all fields to proceed');
return;
}
setIsLoading(true);
try {
const response = await axios.post(
'https://baseUrl/document/downloadEndpoint',
{
requestType: planName,
planId: planId,
startDate: fromFormattedDate,
endDate: toFormattedDate,
},
{
responseType: 'arraybuffer',
},
);
if (response === undefined) {
setError('Something went wrong. Please Try again.');
return;
}
if (response.status === 200) {
const pdfData = response.data;
const path = `${
Platform.OS === 'android'
? RNFS.DownloadDirectoryPath
: RNFS.DocumentDirectoryPath
}/Account_statement_${fromFormattedDate}_to_${toFormattedDate}.pdf`;
try {
// Convert the ArrayBuffer to base64 string
const base64Data = base64.fromByteArray(new Uint8Array(pdfData));
await RNFS.writeFile(path, base64Data, 'base64');
const fileExists = await RNFS.exists(path);
if (fileExists) {
if (Platform.OS === 'ios') {
await Share.open({
url: `file://${path}`,
type: 'application/pdf',
title: 'Open PDF',
});
}
} else {
console.error('File not found after saving:', path);
setError('Error saving file');
return;
}
} catch (writeError) {
console.error('Error writing file:', writeError);
setError('Error saving file');
return;
}
//Navagate to success screen or show success alart message
} else {
setError(
response?.data.message ||'Something went wrong, please try again',
);
}
} catch (error) {
setError(`${error}` ||'Something went wrong, please try again');
} finally {
setIsLoading(false);
}
};
Why These Approaches?
- API Request with Axios: We use
axios
to make an HTTP POST request to the server, requesting the PDF file. TheresponseType: 'arraybuffer'
, a part ofaxios
config, is crucial here because it allows us to handle binary data. - Handling the API Response:
- Check for Successful Response: We check if the response status is
200
, which indicates that the request was successful and the server returned the PDF data. - Save File Path: We define the file path where the PDF will be saved. For Android, it’s
RNFS.DownloadDirectoryPath
, and for iOS, it'sRNFS.DocumentDirectoryPath
. For Android, the PDF saves directly to the download directory on the user’s phone. For iOS, the app prompts the user to select the destination to save the PDF.
3. Convert ArrayBuffer to Base64:
- Why Base64?: Files are written as base64 strings because the file system operations in React Native often require the data to be in base64 format.
4. Writing the File:
- RNFS.writeFile: We use
RNFS.writeFile
to write the base64-encoded PDF data to the specified path. - Check File Existence: After writing, we check if the file exists using
RNFS.exists
. This ensures that the file was successfully written.
4. Sharing the File on iOS:
- Share.open: If the platform is iOS, we use
react-native-share
to open the file. This allows the user to select where to save, view or share the PDF directly from the app. If the user doesn’t select a file destination, an error occurs and the download process doesn’t complete.
iOS Permissions
Before we get too carried away, remember that iOS requires some permissions to save files. Add the following to your Info.plist
:
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need your permission to save PDFs to your photo library</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>We need access to save your account statements in the documents folder.</string>
Why Permissions?
Without these permissions, iOS won’t allow your app to save files, and we don’t want to frustrate our users with mysterious errors.
Building the UI
Finally, we build the UI using the imported components:
return (
<Container headerType="logo-only" style={styles.container}>
<View style={styles.contentContainer}>
<View style={styles.itemWrapper}>
<Text style={styles.title}>Generate Statement</Text>
<Text style={styles.subTitle}>
Select a plan to download the statement of your account
</Text>
</View>
<View>
<Text style={{ fontSize: 12 }}>Select plan</Text>
<RNPickerSelect
onValueChange={(value) => handlePlanSelection(value)}
items={plans.map(plan => ({ label: plan.planName, value: plan.planName }))}
placeholder={{ label: "Select...", value: null }}
value={planName}
/>
</View>
<View style={styles.itemWrapper}>
<Text style={{ fontSize: 12, marginBottom: 8 }}>From</Text>
<RNButton onPress={() => setShowFromDatePicker(true)} title="Select From Date" />
{showFromDatePicker && (
<DateTimePicker
value={fromRawDate}
mode="date"
display="default"
onChange={handleFromDateChange}
/>
)}
</View>
<View>
<Text style={{ fontSize: 12, marginBottom: 8 }}>To</Text>
<RNButton onPress={() => setShowToDatePicker(true)} title="Select To Date" />
{showToDatePicker && (
<DateTimePicker
value={toRawDate}
mode="date"
display="default"
onChange={handleToDateChange}
/>
)}
</View>
</View>
{error && (
<Text
style={{
fontSize: 12,
color: colors.redPrimary,
marginVertical: 10,
textAlign: 'center',
}}>
{error}
</Text>
)}
<View style={styles.buttonContainer}>
<Button
label="Download Account Statement"
onPress={completeDownloadStatement}
useFlex={false}
disabled={isLoading}
loading={isLoading}
/>
</View>
</Container>
);
Why This Layout?
- Container: Ensures consistent styling and layout.
- View, Text: Basic building blocks for layout and text.
- RNButton: For showing date pickers.
- Button: Custom button for downloading the statement.
- Conditional Rendering: To show/hide date pickers and display error messages.
And there you have it, folks! A simple and effective way to generate and download PDFs in a React Native app from a byte array response. Remember, every great app is built one line of code at a time. Keep coding, stay curious, don’t be scared to unlearn and never stop learning!