4

I'm trying to upload an image file to firebase storage, save the download URL, and load it after the upload is completed. When I run the app with debug js remotely on it works fine. When I turn off debug mode it stops working with the invalid format exception. The same happens when I run in a real device (both iOS and Android)

The base64 response data from React Native Image Picker seems to be correct

Here's my code

...
import * as ImagePicker from 'react-native-image-picker'; //0.26.10
import firebase from 'firebase'; //4.9.1
...

handleImagePicker = () => {
    const { me } = this.props;
    const options = {
      title: 'Select pic',
      storageOptions: {
        skipBackup: true,
        path: 'images'
      },
      mediaType: 'photo',
      quality: 0.5,
    };
    ImagePicker.showImagePicker(options, async (response) => {
        const storageRef = firebase.storage().ref(`/profile-images/user_${me.id}.jpg`);

        const metadata = {
          contentType: 'image/jpeg',
        };

        const task = storageRef.putString(response.data, 'base64', metadata);
        return new Promise((resolve, reject) => {
          task.on(
            'state_changed',
            (snapshot) => {
              var progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
              console.log('Upload is ' + progress + '% done');
            },
            (error) =>
              console.log(error),
            () => {
              this.onChangeProfileImage();
            }
          );
        });
     }
}

onChangeProfileImage = async () => {
    const { me } = this.props;

    const storageRef = firebase.storage().ref(`/profile-images/user_${me.id}.jpg`);

    const profileImageUrl = await new Promise((resolve, reject) => {
      storageRef.getDownloadURL()
      .then((url) => {
        resolve(url);
      })
      .catch((error) => {
        console.log(error);
      });
    });


  // some more logic to store profileImageUrl in the database

  }

Any idea how to solve this?

Thanks in advance.

soutot
  • 3,531
  • 1
  • 18
  • 22

2 Answers2

5

After some research and debug I found the cause of the issue and a solution for it.

Why does it happen?

Firebase uses atob method to decode the base64 string sent by putstring method. However, since JavaScriptCore doesn't have a default support to atob and btoa, the base64 string can't be converted, so this exception is triggered.

When we run the app in debug javascript remotely mode, all javascript code is run under chrome environment, where atob and btoa are supported. That's why the code works when debug is on and doesn't when its off.

How to solve?

To handle atob and btoa in React Native, we should either write our own encode/decode method, or install a lib to handle it for us.

In my case I preferred to install base-64 lib

But here's an example of a encode/decode script:

const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
const Base64 = {
  btoa: (input:string = '')  => {
    let str = input;
    let output = '';

    for (let block = 0, charCode, i = 0, map = chars;
    str.charAt(i | 0) || (map = '=', i % 1);
    output += map.charAt(63 & block >> 8 - i % 1 * 8)) {

      charCode = str.charCodeAt(i += 3/4);

      if (charCode > 0xFF) {
        throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.");
      }

      block = block << 8 | charCode;
    }

    return output;
  },

  atob: (input:string = '') => {
    let str = input.replace(/=+$/, '');
    let output = '';

    if (str.length % 4 == 1) {
      throw new Error("'atob' failed: The string to be decoded is not correctly encoded.");
    }
    for (let bc = 0, bs = 0, buffer, i = 0;
      buffer = str.charAt(i++);

      ~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
        bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
    ) {
      buffer = chars.indexOf(buffer);
    }

    return output;
  }
};

export default Base64;

Usage:

import Base64 from '[path to your script]';

const stringToEncode = 'xxxx';
Base64.btoa(scriptToEncode);

const stringToDecode = 'xxxx';
Base64.atob(stringToDecode);

After choosing either to use the custom script or the lib, now we must add the following code to the index.js file:

import { decode, encode } from 'base-64';

if (!global.btoa) {
    global.btoa = encode;
}

if (!global.atob) {
    global.atob = decode;
}

AppRegistry.registerComponent(appName, () => App);

This will declare atob and btoa globally. So whenever in the app those functions are called, React Native will use the global scope to handle it, and then trigger the encode and decode methods from base-64 lib.

So this is the solution for Base64 issue.

However, after this is solved, I found another issue Firebase Storage: Max retry time for operation exceed. Please try again when trying to upload larger images. It seems that firebase has some limitation on support to React Native uploads, as this issue suggests.

I believe that react-native-firebase may not struggle on this since it's already prepared to run natively, instead of using the web environment as firebase does. I didn't test it yet to confirm, but it looks like this will be the best approach to handle it.

Hope this can be helpful for someone else.

Community
  • 1
  • 1
soutot
  • 3,531
  • 1
  • 18
  • 22
  • BTW, @soutot, did you use fetch() for reading the file from cache? It didn't work for me, so I am trying to use expo's FileSystem.readAsStringAsync instead. – Yossi Nov 13 '18 at 21:32
  • I started working on other projects and didn't finish this one, so I haven't implemented cache reading. My idea at that time was to download the image and store in the local storage. Then read from it if there's any valid image stored, otherwise fetch again. I think it might work. – soutot Nov 13 '18 at 23:52
  • 1
    Well, good luck :) fetch seemed to work for me for a while, and then stopped working. https://github.com/expo/firebase-storage-upload-example/issues/14 – Yossi Nov 14 '18 at 05:39
  • but you are encoding a string into a base64 string, what would the solution be for an image? – Cristian G Mar 31 '21 at 14:04
3

The problem is now solved using fetch() API. The promise returned can be converted to blob which you can upload to firebase/storage

Here is an example

 let storageRef = storage().ref();
 let imageName = data.name + "image";
 let imagesRef = storageRef.child(`images/${imageName}`);

 const response = await fetch(image); 
 const blob = await response.blob(); // Here is the trick

 imagesRef
      .put(blob)
      .then((snapshot) => {
        console.log("uploaded an image.");
      })
      .catch((err) => console.log(err));
Ayush Kumar
  • 494
  • 1
  • 6
  • 21