In my Expo (react-native) application, I want to do the upload task even if the application is in the background or killed.
the upload should be done to firebase storage, so we don't have a REST API.
checked out the Expo task manager library, but I could not figure out how it should be done. is it even possible to achieve this goal with Expo? is the TaskManager the correct package for this task?
there are only some Expo packages that could be registered as a task (e.g. backgroundFetch), and it is not possible to register a custom function (in this case uploadFile method).
I even got more confused as we should enable add UIBackgroundModes key for iOS but it only has audio,location,voip,external-accessory,bluetooth-central,bluetooth-peripheral,fetch,remote-notification,processing as possible values.
I would appreciate it if you can at least guide me on where to start or what to search for, to be able to upload the file even if the app is in the background is killed/terminated.
import { getStorage, ref, uploadBytes } from "firebase/storage";
const storage = getStorage();
const storageRef = ref(storage, 'videos');
const uploadFile = async (file)=>{
// the file is Blob object
await uploadBytes(storageRef, file);
}
I have already reviewed react-native-background-fetch, react-native-background-upload, react-native-background-job . upload should eject Expo, job does not support iOS, and fetch is a fetching task designed for doing task in intervals.
if there is a way to use mentioned libraries for my purpose, please guide me :)
to my understanding, the Firebase Cloud JSON API does not accept files, does it ? if so please give me an example. If I can make storage json API work with file upload, then I can use Expo asyncUpload probably without ejecting.
I have done something similar like you want, you can use expo-task-manager and expo-background-fetch. Here is the code as I used it. I Hope this would be useful for you.
import * as BackgroundFetch from 'expo-background-fetch';
import * as TaskManager from 'expo-task-manager';
const BACKGROUND_FETCH_TASK = 'background-fetch';
const [isRegistered, setIsRegistered] = useState(false);
const [status, setStatus] = useState(null);
//Valor para que se ejecute en IOS
BackgroundFetch.setMinimumIntervalAsync(60 * 15);
// Define the task to execute
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
const now = Date.now();
console.log(`Got background fetch call at date: ${new Date(now).toISOString()}`);
// Your function or instructions you want
return BackgroundFetch.Result.NewData;
});
// Register the task in BACKGROUND_FETCH_TASK
async function registerBackgroundFetchAsync() {
return BackgroundFetch.registerTaskAsync(BACKGROUND_FETCH_TASK, {
minimumInterval: 60 * 15, // 1 minutes
stopOnTerminate: false, // android only,
startOnBoot: true, // android only
});
}
// Task Status
const checkStatusAsync = async () => {
const status = await BackgroundFetch.getStatusAsync();
const isRegistered = await TaskManager.isTaskRegisteredAsync(
BACKGROUND_FETCH_TASK
);
setStatus(status);
setIsRegistered(isRegistered);
};
// Check if the task is already register
const toggleFetchTask = async () => {
if (isRegistered) {
console.log('Task ready');
} else {
await registerBackgroundFetchAsync();
console.log('Task registered');
}
checkStatusAsync();
};
useEffect(() => {
toggleFetchTask();
}, []);
Hope this isn't too late to be helpful.
I've been dealing with a variety of expo <-> firebase storage integrations recently, and here's some info that might be helpful.
First, I'd recommend not using the uploadBytes / uploadBytesResumable methods from Firebase. This Thread has a long ongoing discussion about it, but basically it's broken in v9. Maybe in the future the Firebase team will solve the issues, but it's pretty broken with Expo right now.
Instead, I'd recommend either going down the route of writing a small Firebase function that either gives a signed-upload-url or handles the upload itself.
Basically, if you can get storage uploads to work via an http endpoint, you can get any kind of upload mechanism working. (e.g. the FileSystem.uploadAsync() method you're probably looking for here, like #brentvatne pointed out, or fetch, or axios. I'll show a basic wiring at the end).
Server Side
Option 1: Signed URL Upload.
Basically, have a small firebase function that returns a signed url. Your app calls a cloud function like /get-signed-upload-url , which returns the url, which you then use. Check out: https://cloud.google.com/storage/docs/access-control/signed-urls for how you'd go about this.
This might work well for your use case. It can be configured just like any httpsCallable function, so it's not much work to set up, compared to option 2.
However, this doesn't work for the firebase storage / functions emulator! For this reason, I don't use this method, because I like to intensively use the emulators, and they only offer a subset of all the functionalities.
Option 2: Upload the file entirely through a function
This is a little hairier, but gives you a lot more fidelity over your uploads, and will work on an emulator! I like this too because it allows doing upload process within the endpoint execution, instead of as a side effect.
For example, you can have a photo-upload endpoint generate thumbnails, and if the endpoint 201's, then you're good! Rather than the traditional Firebase approach of having a listener to cloud storage which would generate thumbnails as a side effect, which then has all kinds of bad race conditions (checking for processing completion via exponentiational backoff? Gross!)
Here are three resources I'd recommend to go about this approach:
https://cloud.google.com/functions/docs/writing/http#multipart_data
https://github.com/firebase/firebase-js-sdk/issues/5848
https://github.com/mscdex/busboy
Basically, if you can make a Firebase cloud endpoint that accepts a File within formdata, you can have busboy parse it, and then you can do anything you want with it... like upload it to Cloud Storage!
an outline of this:
import * as functions from "firebase-functions";
import * as busboy from "busboy";
import * as os from "os";
import * as path from "path";
import * as fs from "fs";
type FieldMap = {
[fieldKey: string]: string;
};
type Upload = {
filepath: string;
mimeType: string;
};
type UploadMap = {
[fileName: string]: Upload;
};
const MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB
export const uploadPhoto = functions.https.onRequest(async (req, res) => {
verifyRequest(req); // Verify parameters, auth, etc. Better yet, use a middleware system for this like express.
// This object will accumulate all the fields, keyed by their name
const fields: FieldMap = {};
// This object will accumulate all the uploaded files, keyed by their name.
const uploads: UploadMap = {};
// This will accumulator errors during the busboy process, allowing us to end early.
const errors: string[] = [];
const tmpdir = os.tmpdir();
const fileWrites: Promise<unknown>[] = [];
function cleanup() {
Object.entries(uploads).forEach(([filename, { filepath }]) => {
console.log(`unlinking: ${filename} from ${path}`);
fs.unlinkSync(filepath);
});
}
const bb = busboy({
headers: req.headers,
limits: {
files: 1,
fields: 1,
fileSize: MAX_FILE_SIZE,
},
});
bb.on("file", (name, file, info) => {
verifyFile(name, file, info); // Verify your mimeType / filename, etc.
file.on("limit", () => {
console.log("too big of file!");
});
const { filename, mimeType } = info;
// Note: os.tmpdir() points to an in-memory file system on GCF
// Thus, any files in it must fit in the instance's memory.
console.log(`Processed file ${filename}`);
const filepath = path.join(tmpdir, filename);
uploads[filename] = {
filepath,
mimeType,
};
const writeStream = fs.createWriteStream(filepath);
file.pipe(writeStream);
// File was processed by Busboy; wait for it to be written.
// Note: GCF may not persist saved files across invocations.
// Persistent files must be kept in other locations
// (such as Cloud Storage buckets).
const promise = new Promise((resolve, reject) => {
file.on("end", () => {
writeStream.end();
});
writeStream.on("finish", resolve);
writeStream.on("error", reject);
});
fileWrites.push(promise);
});
bb.on("close", async () => {
await Promise.all(fileWrites);
// Fail if errors:
if (errors.length > 0) {
functions.logger.error("Upload failed", errors);
res.status(400).send(errors.join());
} else {
try {
const upload = Object.values(uploads)[0];
if (!upload) {
functions.logger.debug("No upload found");
res.status(400).send("No file uploaded");
return;
}
const { uploadId } = await processUpload(upload, userId);
cleanup();
res.status(201).send({
uploadId,
});
} catch (error) {
cleanup();
functions.logger.error("Error processing file", error);
res.status(500).send("Error processing file");
}
}
});
bb.end(req.rawBody);
});
Then, that processUpload function can do anything you want with the file, like upload it to cloud storage:
async function processUpload({ filepath, mimeType }: Upload, userId: string) {
const fileId = uuidv4();
const bucket = admin.storage().bucket();
await bucket.upload(filepath, {
destination: `users/${userId}/${fileId}`,
{
contentType: mimeType,
},
});
return { fileId };
}
Mobile Side
Then, on the mobile side, you can interact with it like this:
async function uploadFile(uri: string) {
function getFunctionsUrl(): string {
if (USE_EMULATOR) {
const origin =
Constants?.manifest?.debuggerHost?.split(":").shift() || "localhost";
const functionsPort = 5001;
const functionsHost = `http://${origin}:${functionsPort}/{PROJECT_NAME}/${PROJECT_LOCATION}`;
return functionsHost;
} else {
return `https://{PROJECT_LOCATION}-{PROJECT_NAME}.cloudfunctions.net`;
}
}
// The url of your endpoint. Make this as smart as you want.
const url = `${getFunctionsUrl()}/uploadPhoto`;
await FileSystem.uploadAsync(uploadUrl, uri, {
httpMethod: "POST",
uploadType: FileSystem.FileSystemUploadType.MULTIPART,
fieldName: "file", // Important! make sure this matches however you want bussboy to validate the "name" field on file.
mimeType,
headers: {
"content-type": "multipart/form-data",
Authorization: `${idToken}`,
},
});
});
TLDR
Wrap Cloud Storage in your own endpoint, treat it like a normal http upload, everything plays nice.
This is a strange one, but here's the situation.
I'm using Next.js with the Next-auth package to handle authentication.
I'm not using Server-Side rendering, it's an admin area, so there is no need for SSR, and in order to authenticate users, I've created a HOC to wrap basically all components except for the "/sign-in" route.
This HOC all does is check if there's a session and then adds the "access token" to the Axios instance in order to use it for all async calls, and if there is no session, it redirects the user to the "sign-in" page like this ...
const AllowAuthenticated = (Component: any) => {
const AuthenticatedComponent = () => {
const { data: session, status }: any = useSession();
const router = useRouter();
useEffect(() => {
if (status !== "loading" && status === "unauthenticated") {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
router.push("/signin");
} else if (session) {
axiosInstance.defaults.headers.common["Authorization"] = `Bearer ${session.accessToken.accessToken}`;
}
}, [session, status]);
if (status === "loading" || status === "unauthenticated") {
return <LoadingSpinner />;
} else {
return <Component />;
}
};
return AuthenticatedComponent;
};
export default AllowAuthenticated;
And in the Axios instance, I'm checking if the response is "401", then I log out the user and send him to the "sign-in" screen, like this ...
axiosInstance.interceptors.response.use(
response => response,
error => {
const { status } = error.response;
if (status === 401) {
axiosInstance.defaults.headers.common["Authorization"] = null;
signOut({ redirect: false });
return Promise.reject(error);
}
return Promise.reject(error);
},
);
Very simple stuff, and it works like a charm until I decided to upgrade my project to use "react 18.1.0" and "react-dom 18.1.0", then all of a sudden, my API calls doesn't get the "Authorization" header and they return "401" and the user gets logged out :(
If I tried to make an API call inside the HOC right after I set the Auth headers it works, sot I DO get the "token" from the session, but all the async dispatch calls inside the wrapped component return 401.
I forgot to mention, that this issue happens on page refresh, if I didn't refresh the page after I sign in, everything works great, but once I refresh the page the inner async dispatch calls return 401.
I Updated all the packages in my project including Axios and next-auth, but it didn't help.
I eventually had to downgrade back to "react 17.0.2" and everything works again.
Any help is much appreciated.
For those of you who might come across the same issue.
I managed to solve this by not including the logic for adding the token to the "Authorization" header inside the HOC, instead, I used a solution by #kamal-choudhary on a post on Github talking about how to add "JWT" to every axios call using next-auth.
Using #jaketoolson help at that Github post, he was able to attach the token to every "Axios" call.
The solution is basically to create an Axios instance and add an interceptor like I was doing above, but not just for the response, but also for request.
You'll add an interceptor for every request and check if there's a session, and then attach the JWT to the Authorization header.
That managed to solve my issue, and now next-auth works nicely with react 18.
Here's the code he's using ...
import axios from 'axios';
import { getSession } from 'next-auth/react';
const baseURL = process.env.SOME_API_URL || 'http://localhost:1337';
const ApiClient = () => {
const defaultOptions = {
baseURL,
};
const instance = axios.create(defaultOptions);
instance.interceptors.request.use(async (request) => {
const session = await getSession();
if (session) {
request.headers.Authorization = `Bearer ${session.jwt}`;
}
return request;
});
instance.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.log(`error`, error);
},
);
return instance;
};
export default ApiClient();
Don't forget to give them a thumbs up for their help if it works for you ...
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1993281
https://github.com/nextauthjs/next-auth/discussions/3550#discussioncomment-1898233
I am building out a webpage which needs to make a call to the Google Geocoder api.
In order to hide the api key from public view, I am trying to set up server middleware to act as a REST api endpoint.
I have checked through all of the documentation and copied all of it, but the response is always the same. I receive the entirety of the html body back from the axios request rather than anything else I send back via express.
In my component I have the following code:
computed: {
normalizedAddress() {
return `${this.member.address.street} ${this.member.address.city}, ${this.member.address.state} ${this.member.address.zip}`.replace(
/\s/g,
'+'
)
}
},
methods: {
async getLocation() {
try {
const res = await axios.get(
`/api/geocode/${this.normalizedAddress}`
)
console.log(res)
} catch (err) {
console.log(err)
}
}
},
In nuxt.config.js I have this setup
serverMiddleware: ['~/api/geocode.js'],
In the root of my project I have an api folder with geocode.js stored there.
geocode.js is below
import express from 'express';
import axios from "axios";
let GEO_API = "MY_API_KEY"
const app = express();
app.use(express.json());
app.get("/", async (req, res) => {
const uri = `https://maps.googleapis.com/maps/api/geocode/json?address=${req.params.address}&key=${GEO_API}`
try {
const code = await axios.get(uri);
if (code.status !== "OK") {
return res.status(500).send(code.status)
}
return res.status(200).send(code);
} catch (err) {
return res.status(500).send(err);
}
});
export default {
path: "/api/geocode/:address",
handler: app
}
Again. The response always has the entire html document from the website sent back (some 100 pages of code).
Even when I set the response to fixed text, that is not sent.
The only detail I can think of that might be interrupting it is that I have my own custom routing setup using the #nuxtjs/router build module in use.
Really hoping someone can help me.
What I am trying to do: Call a REST API for a json and resolve a Angular 2 promise.
ServerAPI running Node.js/ExpressJS/Lodash
Server.js file:
var express = require('express');
var app = express();
var bodyParser = require("body-parser");
var data = require('./data.json');
var _ = require('lodash');
var cors = require('cors');
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());
app.get('/GetData', function (req, resp) {
if (req.query.search != null) {
var result = _.find(data, function (o) {
return o.value === req.query.search.toLowerCase().trim()
});
return resp.send(result)
}
});
app.listen(1337, function () {
console.log('Listening at Port 1337');
});
Ran as tested http://localhost:1337/GetData?search=colorado and returns a vaild json object.
ClientAPI
Service file calling HTTP request:
import {Injectable} from "#angular/core";
import {Http} from "#angular/http";
import {Config} from "../config";
import {SearchResult} from "../models/search-result.model";
import {MockSearchData} from "../mock/mock-search-results";
import {Observable} from 'rxjs/Rx';
import 'rxjs/add/operator/map';
#Injectable()
export class ApiDataService {
constructor(private http:Http) {
}
public performSearchRequest(searchTerm:string,queryType:string):Promise<SearchResult[]> {
return new Promise<SearchResult[]>((resolve, reject) => {
let url = Config.apiBaseUrl + Config.searchApi;
url += "?search=" + searchTerm;
console.log("Your query to be: " + url);
if (searchTerm != "") {
if (queryType == 'mock') {
resolve(MockSearchData);
} else if (queryType == 'api') {
let data = [];
this.http.get(url)
.map(resp => resp.json())
.subscribe(getData => data = getData);
resolve(data);
} else {
reject("No query type found.");
}
} else {
reject("Please enter a search term.");
};
});
}
}
The resolve of the mock data which is a local json file within the ClientAPI works perfectly. I need to get the if function for the api query type to work.
The Angular app starts with no issue and runs the http.get without error. I checked the network tab under the dev tools and can see that a HTTP request was done and returns its response is the valid JSON object I want resolved e.g there is data being returned. Yet the table i am resolving this into is blank.
WHAT AM I DOING WRONG!
The issue occurs here:
this.http.get(url)
.map(resp => resp.json())
.subscribe(getData => data = getData);
resolve(data);
You subscribe to the observable, but it isn’t “resolved” yet when you call resolve directly afterwards. That means you’re really just calling resolve([]).
Instead, do something like this:
this.http.get()./*...*/.subscribe(result => resolve(result));
You also might want to look into the toPromise method on Observables as well as the way to construct a resolved promise directly.
I went through FBSDK Sharing documentation here, but I can't find a simple example where one can just post a simple message to timeline (not a link, not a photo, not a video) using FBShareDialog.
I know I can do it running a web request which essentially does this:
POST graph.facebook.com/me/feed?
message="My new post message!"&
access_token={your-access-token}
as described in Graph API docs, but again - I want to use ShareDialog to have consistent UI.
How do I do it? Thank you.
Note: All user lower case "post" refers to the act of posting to a users wall. All upper case "POST" refers to HTTP request method.
Facebooks offical react native SDK is located here https://developers.facebook.com/docs/react-native/
Note there are three different component:
Login
Sharing
Graph API.
The first two are self explanatory and the examples are provided on the site.
The Graph API is the primary way to get data in and out of Facebook's
social graph. It's a low-level HTTP-based API that is used to query
data, post new stories, upload photos and a variety of other tasks
that an app might need to do.
Facebook Graph API is just a REST API which lets you interact with the fb data via HTTP methods( GET, POST, DELETE etc). react-native-fbsdk just layer on top of it which makes it easier to make these request.
There are two prerequisites to posting to a user time.
Ensuring your fb app is correctly setup: https://developers.facebook.com/apps/
Obtaining a user access token with publish_actions permission can be used to publish new posts.
Once you have obtained these you can post a message using the react native GRAPH API.
But first lets have a look at how you would do this simply using HTTP rather then the RN-SDK:
https://developers.facebook.com/docs/graph-api/reference/v2.7/user/feed
POST /v2.7/me/feed HTTP/1.1
Host: graph.facebook.com
message=This+is+a+test+message
According to this we need to make a POST request to the location /v2.7/me/feed with the message defined as a paramater.
To finally answer your question how to we post to the users timeline using the react native sdk (). Lets have a look at the rnsdk docs: https://developers.facebook.com/docs/react-native/graph-api
It seems like we need two objects GraphRequest (to create a request) and GraphRequestManager (to send the request)
const FBSDK = require('react-native-fbsdk');
const {
FBGraphRequest,
FBGraphRequestManager,
} = FBSDK;
Since there is no example provided on how to post to the user wall using these two objects we need to look into the source code:
https://github.com/facebook/react-native-fbsdk/blob/master/js/FBGraphRequest.js
We can see from the constructor it takes three parameters:
/**
* Constructs a new Graph API request.
*/
constructor(
graphPath: string,
config: ?GraphRequestConfig,
callback: ?GraphRequestCallback,
)
We know the graphPath = "/me/feed" from the Graph API docs. The callback will just be a function called upon return of the request. This leaves us with the config object, which is defined in the source as:
type GraphRequestConfig = {
/**
* The httpMethod to use for the request, for example "GET" or "POST".
*/
httpMethod?: string,
/**
* The Graph API version to use (e.g., "v2.0")
*/
version?: string,
/**
* The request parameters.
*/
parameters?: GraphRequestParameters,
/**
* The access token used by the request.
*/
accessToken?: string
};
So our config object will look something like this:
const postRequestParams = {
fields: {
message: 'Hello World!'
}
}
const postRequestConfig = {
httpMethod: 'POST',
version: 'v2.7',
parameters: postRequestParams,
accessToken: token.toString() //pre-obtained access token
}
Putting it altogether:
const FBSDK = require('react-native-fbsdk');
const {
FBGraphRequest,
FBGraphRequestManager,
} = FBSDK;
_responseInfoCallback(error: ?Object, result: ?Object) {
if (error) {
alert('Error fetching data: ' + error.toString());
} else {
alert('Success fetching data: ' + result.toString());
}
}
const postRequestParams = {
fields: {
message: 'Hello World!'
}
}
const postRequestConfig = {
httpMethod: 'POST',
version: 'v2.7',
parameters: postRequestParams,
accessToken: token.toString()
}
const infoRequest = new GraphRequest(
'/me/feed',
postRequestConfig,
this._responseInfoCallback,
);
new FBGraphRequestManager().addRequest(infoRequest).start();
I posted to facebook with react native 0.43 by above code but i changed on postRequestParams
const postRequestParams = {
message: {
string: 'Hello World!'
}
}
Here is all of me.
const FBSDK = require('react-native-fbsdk');
const {
GraphRequest,
GraphRequestManager,
AccessToken
} = FBSDK;
class PostScreen extends React.Component {
postToFacebook = () => {
AccessToken.getCurrentAccessToken().then(
(data) => {
let tempAccesstoken = data.accessToken;
const _responseInfoCallback = (error, result) => {
console.log(result);
}
const postRequestParams = {
message: {
string: "Hello world!"
}
}
const postRequestConfig = {
httpMethod: "POST",
version: "v2.9",
parameters: postRequestParams,
accessToken: tempAccesstoken
}
console.log(postRequestConfig);
const infoRequest = new GraphRequest(
"/me/feed",
postRequestConfig,
_responseInfoCallback,
);
console.log("infoRequest");
console.log(infoRequest);
new GraphRequestManager().addRequest(infoRequest).start();
});
}
}