A collection of Expo compatible hooks to use with Pinata
pnpm i pinata-expo-hooks@latest
pinata-expo-hooks
is MIT open sourced and can be accessed on GitHub with the link below
import { useUpload } from "pinata-expo-hooks";
const {
upload, // Method to upload a file using a presigned URL
progress, // Progress state as integer
loading, // Boolean uploading state
uploadResponse, // File ID used to fetch the file info server side
error, // Error state
pause, // Pause upload method
resume, // Resume upload method
cancel, // Cancel current upload method
} = useUpload();
useUpload
export type UseUploadReturn = {
progress: number;
loading: boolean;
error: Error | null;
uploadResponse: string | null;
upload: (
fileUri: string,
network: "public" | "private",
url: string,
options?: UploadOptions,
) => Promise<void>;
pause: () => void;
resume: () => void;
cancel: () => void;
};
upload
method like so.
await upload(fileUri, "public", "presigned_URL", {
name: fileName || "Upload from Expo",
keyvalues: {
app: "Pinata Expo Demo",
timestamp: Date.now().toString(),
},
});
UploadOptions
that can be passed into upload
export type UploadOptions = {
customHeaders?: Record<string, string>;
name?: string;
keyvalues?: Record<string, string>;
groupId?: string;
chunkSize?: number; // Defaults to 1014 * 256 * 20 * 10 (~52MB)
};
uploadResponse
will contain the CID for the file.
Below is a full example of implementing the useUpload
hook with progress and abilities to pause and resume the upload.
import { Image } from "expo-image";
import { View, Text, Button, StyleSheet } from "react-native";
import * as ImagePicker from "expo-image-picker";
import * as DocumentPicker from "expo-document-picker";
import { useState } from "react";
import ParallaxScrollView from "@/components/ParallaxScrollView";
import { useUpload } from "pinata-expo-hooks";
const SERVER_URL = "http://localhost:8787"; // Server that returns a presigned_url
export default function HomeScreen() {
const {
upload, // Method to upload a file using a presigned URL
progress, // Progress state as integer
loading, // Boolean uploading state
uploadResponse, // CID for the uploaded file
error, // Error state
pause, // Pause upload method
resume, // Resume upload method
cancel, // Cancel current upload method
} = useUpload();
const [fileUri, setFileUri] = useState<string | null>(null);
const [fileName, setFileName] = useState<string | null>(null);
const pickImage = async () => {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
alert("Sorry, we need camera roll permissions to make this work!");
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: false,
quality: 1,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
setFileUri(result.assets[0].uri);
setFileName(result.assets[0].uri.split("/").pop() || "image");
}
};
// Pick document from device
const pickDocument = async () => {
try {
const result = await DocumentPicker.getDocumentAsync();
if (result.canceled === false) {
setFileUri(result.assets[0].uri);
setFileName(result.assets[0].name);
}
} catch (err) {
console.error("Error picking document", err);
}
};
// Start the upload process
const startUpload = async () => {
if (!fileUri) {
alert("Please select a file first");
return;
}
try {
const urlRes = await fetch(`${SERVER_URL}/presigned_url`);
if (!urlRes.ok) {
console.log(urlRes.status);
}
const urlData = await urlRes.json();
await upload(
fileUri,
"public", // or 'private' if you want a private upload
urlData.url,
{
name: fileName || "Upload from Expo",
keyvalues: {
app: "Pinata Expo Demo",
timestamp: Date.now().toString(),
},
},
);
} catch (err) {
console.error("Failed to start upload:", err);
alert("Failed to start upload");
}
};
// Progress bar component
const ProgressBar = ({ value }: { value: number }) => {
return (
<View style={styles.progressBarContainer}>
<View style={[styles.progressBar, { width: `${value}%` }]} />
</View>
);
};
return (
<ParallaxScrollView
headerBackgroundColor={{ light: "#A1CEDC", dark: "#1D3D47" }}
headerImage={
<Image
source={require("@/assets/images/partial-react-logo.png")}
style={styles.reactLogo}
/>
}
>
<View style={styles.container}>
<Text style={styles.title}>Pinata IPFS Upload</Text>
<View style={styles.buttonContainer}>
<Button title="Pick Image" onPress={pickImage} />
<Button title="Pick Document" onPress={pickDocument} />
</View>
{fileUri && (
<View style={styles.fileInfoContainer}>
<Text style={styles.fileInfo}>Selected: {fileName}</Text>
<Button
title="Upload to IPFS"
onPress={startUpload}
disabled={loading}
color="#FF6AC1" // Pinata pink
/>
</View>
)}
{loading && (
<View style={styles.uploadStatusContainer}>
<Text style={styles.uploadStatusText}>
Uploading... {Math.round(progress)}%
</Text>
<ProgressBar value={progress} />
<View style={styles.uploadControlsContainer}>
{progress < 100 && (
<>
<Button title="Pause" onPress={pause} color="#FFA15C" />
<Button title="Resume" onPress={resume} color="#5CB8FF" />
<Button title="Cancel" onPress={cancel} color="#FF5C5C" />
</>
)}
</View>
</View>
)}
{error && (
<View style={styles.errorContainer}>
<Text style={styles.errorTitle}>Upload Error</Text>
<Text style={styles.errorText}>{error.message}</Text>
</View>
)}
{uploadResponse && (
<View style={styles.successContainer}>
<Text style={styles.successTitle}>Upload Complete!</Text>
<Text style={styles.successText}>File CID: {uploadResponse}</Text>
</View>
)}
</View>
</ParallaxScrollView>
);
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: "absolute",
},
container: {
flex: 1,
padding: 20,
backgroundColor: "#f5f5f5",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 20,
textAlign: "center",
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 20,
},
fileInfoContainer: {
backgroundColor: "#fff",
padding: 15,
borderRadius: 8,
marginBottom: 20,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
fileInfo: {
marginBottom: 10,
},
uploadStatusContainer: {
backgroundColor: "#fff",
padding: 15,
borderRadius: 8,
marginBottom: 20,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.2,
shadowRadius: 2,
},
uploadStatusText: {
marginBottom: 10,
fontWeight: "bold",
},
progressBarContainer: {
height: 10,
backgroundColor: "#e0e0e0",
borderRadius: 5,
overflow: "hidden",
marginBottom: 15,
},
progressBar: {
height: "100%",
backgroundColor: "#FF6AC1", // Pinata pink
},
uploadControlsContainer: {
flexDirection: "row",
justifyContent: "space-around",
},
errorContainer: {
backgroundColor: "#FFEBEE",
padding: 15,
borderRadius: 8,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: "#FF5C5C",
},
errorTitle: {
fontWeight: "bold",
color: "#D32F2F",
marginBottom: 5,
},
errorText: {
color: "#D32F2F",
},
successContainer: {
backgroundColor: "#E8F5E9",
padding: 15,
borderRadius: 8,
marginBottom: 20,
borderLeftWidth: 4,
borderLeftColor: "#4CAF50",
},
successTitle: {
fontWeight: "bold",
color: "#2E7D32",
marginBottom: 10,
},
successText: {
color: "#2E7D32",
marginBottom: 5,
},
pinningContainer: {
flexDirection: "row",
alignItems: "center",
marginBottom: 5,
},
pinningText: {
color: "#FF6AC1",
marginRight: 10,
},
});