React native e firebase

O que você vai fazer

O que você vai aprender

O que você precisa

O Firebase vai funcionar como back-end da aplicação. Vamos começar criando um projeto através do console do Firebase.

Insira um nome de projeto e pressione para continuar.

Agora você será perguntado se deseja habilitar o Google Analytics para seu projeto. Não habilite e continue.

Clique em Criar Projeto. Agora o Firebase alocará recursos para o projeto.

Quando terminar, pressione Continuar para prosseguir para o painel do projeto. Agora, vamos configurar a autenticação.

Na barra de navegação lateral clique em Criação/Autenticação.

e será apresentada a seguinte tela e você deverá clicar em Primeiros passos. Isso habilitará o módulo de autenticação:

Vamos utilizar a autenticação do Google. Clique em Google e forneça o seu email. Veja abaixo como ficará a tela:

Você configurou com sucesso a autenticação do Google em seu projeto do Firebase.

O Cloud Firestore é um banco de dados não relacional (NoSQL), assim como o MongoDB. Para ativar o Cloud Firestore, clique em Firestore Database na barra lateral:

Clique em Criar banco de dados e você verá a seguinte tela:

Vamos iniciar o banco de dados do Firestore no modo de teste. Isso ocorre porque não queremos nos preocupar com um ambiente de produção e regras de segurança, para nos concentrarmos mais no lado do desenvolvimento. No entanto, você pode alterá-lo para o modo de produção depois de concluir o projeto.

Clique em Avançar e escolha um local geográfico onde ficará o seu banco de dados (pode ser qualquer um) e pressione Criar. Isso criará seu banco de dados do Cloud Firestore.

Em uma etapa posterior vamos precisar da configuração associada a um app criado no console do Firebase. Clique no ícone de engrenagem na barra lateral e vá para as configurações do projeto.

Role a tela até aparecer o seguinte:

Clique no segundo ícone, que representa uma aplicação Android. Forneça um nome para o aplicativo e clique em Registrar aplicativo.

Após você deverá fazer o download do arquivo de configuração:

O seu arquivo google-services.json será parecido com este:

{
  "project_info": {
    "project_number": "184594998406",
    "project_id": "lista-de-tarefas-d0615",
    "storage_bucket": "lista-de-tarefas-d0615.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:184594998406:android:9df3ac0ca5d0ff788a9d07",
        "android_client_info": {
          "package_name": "com.professorangoti.listadetarefas"
        }
      },
      "oauth_client": [
        {
          "client_id": "184594998406-d9fmffc8tj8dgcev6qlm29q5e9issrtc.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "AIzaSyBCsJYBXCRdm35_JpeKFHAUdP5yGIKcVJ8"
        }
      ],
      "services": {
        "appinvite_service": {
          "other_platform_oauth_client": [
            {
              "client_id": "184594998406-d9fmffc8tj8dgcev6qlm29q5e9issrtc.apps.googleusercontent.com",
              "client_type": 3
            }
          ]
        }
      }
    }
  ],
  "configuration_version": "1"
}

Ignore todas as outras etapas, faremos isso manualmente.

O Firebase Storage será usado para armazenar arquivos da aplicação. Clique em primeiros passos para começar.

Na primeira etapa use as configurações mostradas abaixo:

Escolha um local do servidor e aperte o botão Concluir para finalizar a configuração.

Verifique as regras de segurança do Storage e compare a regra mostrada abaixo. Modifique sua regra se estiver diferente do mostrado abaixo.

Vamos criar uma aplicação React native. Primeiro abra um terminal e use os seguintes comandos:

npx react-native init <nome da aplicação>

Precisamos renomear o pacote dentro da aplicação Android. Use o mesmo nome do pacote do Android informado no passo 2. Execute o seguinte comando dentro da pasta raiz do projeto:

cd <nome da aplicação>
npx react-native-rename "nome da aplicação" -b <nome do pacote>

O último comando abrirá o Visual Studio Code nesse diretório. Vamos limpar o código do arquivo App.js, que ficará assim:

import React from 'react';
import {Header} from 'react-native/Libraries/NewAppScreen';

function App() {
  return <Header />;
}

export default App;

Vamos criar agora um arquivo na raiz do projeto com o nome jsconfig.json, com o seguinte código:

{
  "compilerOptions": {
    "checkJs": true,
    "jsx": "react-native",    
  },
  "exclude": ["node_modules", "**/node_modules/*"]
}

Usando Hermes

Hermes é uma máquina virtual Javascript otimizada para React Native. Para usar Hermes você precisa abrir o arquivo android/app/build.gradle e fazer a alteração mostrada abaixo:

project.ext.react = [
      entryFile: "index.js",
      enableHermes: true  
]

Em seguida, execute o seguinte comando para iniciar o aplicativo:

npx react-native run-android

O seu aplicativo exibirá a tela abaixo. Observe no canto superior direito da tela a mensagem de ativação do Hermes.

  1. O arquivo de configuração google-services.json que obtivemos no passo 4 deve ser copiado para dentro de seu projeto no seguinte local: /android/app/.

  1. Abra o arquivo /android/app/build.gradle e adicione o seguinte código:
dependencies {
    ...
    
    // For animated GIF support
    implementation 'com.facebook.fresco:animated-gif:2.6.0'
    
    ...
}

// Adicione esta linha no fim do arquivo
apply plugin: 'com.google.gms.google-services' // <--- this should be the last line
  1. Adicione o plugin google-services como uma dependência dentro do arquivo /android/build.gradle:
buildscript {
    ext {
        buildToolsVersion = "..."
        minSdkVersion = ...
        compileSdkVersion = ...
        targetSdkVersion = ...
        supportLibVersion = "..."
        googlePlayServicesAuthVersion = "19.2.0" // <--- use this version or newer
    }
        
    dependencies {
        ...
        classpath('com.google.gms:google-services:4.3.10') // <--- use this version or newer
        ...
    }
  1. Vamos gerar chaves para configurar o app no Firebase console.
cd android && ./gradlew signingReport

Isso gera duas chaves. Você precisa copiar as chaves 'SHA1' e 'SHA-256' que pertencem à variante 'debugAndroidTest' e ‘Store' indicando o diretório de sua aplicação.

Em seguida, você pode adicionar essas chaves às "impressões digitais do certificado SHA" em seu aplicativo no Firebase console. Veja sequencia de imagens a seguir:

Role a página até aparecer o seu aplicativo Android

Copie as chaves geradas no campo Impressão digital do certificado.

yarn add @react-native-firebase/app @react-native-firebase/auth @react-native-firebase/firestore @react-native-firebase/storage @react-native-firebase/messaging
yarn add @react-native-google-signin/google-signin
yarn add @react-navigation/native react-native-screens react-native-safe-area-context @react-navigation/native-stack @react-navigation/bottom-tabs
yarn add react-native-paper
yarn add react-native-vector-icons

Edite o arquivo android/app/build.gradle ( Não é android/build.gradle ) e adicione o seguinte:

apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
yarn add react-native-image-picker
yarn add react-native-encrypted-storage
yarn add @react-native-firebase/messaging

Todos módulos em um só comando:

yarn add @react-native-firebase/app @react-native-firebase/auth @react-native-firebase/firestore @react-native-firebase/storage @react-native-firebase/messaging @react-native-google-signin/google-signin @react-navigation/native react-native-screens @react-navigation/bottom-tabs react-native-safe-area-context @react-navigation/native-stack react-native-paper react-native-vector-icons react-native-image-picker react-native-encrypted-storage @react-native-firebase/messaging

Vamos crias as seguintes pastas a partir da raiz do projeto:

E depois teremos a seguinte estrutura no nosso projeto

import React, { createContext, useEffect, useMemo, useReducer } from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import HomeScreen from './src/screens/HomeScreen';
import SignInScreen, { logOut } from './src/screens/SignInScreen';
import SplashScreen from './src/screens/SplashScreen';
import { Text, TouchableOpacity } from 'react-native';
import { styles } from './src/util/styles';

const Stack = createNativeStackNavigator();

// @ts-ignore
export const AuthContext = createContext();

function App() {
  const [state, dispatch] = useReducer(
    (prevState, action) => {
      switch (action.type) {
        case 'RESTORE_TOKEN':
          return {
            ...prevState,
            isLoading: false,
          };
        case 'SIGN_IN':
          return {
            ...prevState,
            isSignout: false,
            userToken: action.token,
          };
        case 'SIGN_OUT':
          return {
            ...prevState,
            isSignout: true,
            userToken: null,
          };
      }
    },
    {
      isLoading: true,
      isSignout: false,
      userToken: null,
    },
  );

  // temporizador para exibir a splashScreen
  useEffect(() => {
    const sleep = async (ms) => {
      await new Promise((resolve) => setTimeout(resolve, ms));
      // @ts-ignore
      dispatch({ type: 'RESTORE_TOKEN' });
    };
    sleep(500);
  }, []);

  const authContext = useMemo(
    () => ({
      signIn: async (data) => {
        // @ts-ignore
        dispatch({ type: 'SIGN_IN', token: data });
      },
      // @ts-ignore
      signOut: () => {
        logOut();
        // @ts-ignore
        dispatch({ type: 'SIGN_OUT' });
      },
      usuario: state.userToken,
    }),
    [state.userToken],
  );

  if (state.isLoading) {
    return <SplashScreen />;
  }
  return (
    <AuthContext.Provider value={authContext}>
      <NavigationContainer>
        <Stack.Navigator>
          {state.userToken == null ? (
            // No token found, user isn't signed in
            <Stack.Screen
              name="SignIn"
              component={SignInScreen}
              options={{
                title: 'Sign in',
                headerStyle: {
                  backgroundColor: '#2b008f',
                },
                headerTintColor: '#fff',
                headerTitleStyle: {
                  fontWeight: 'bold',
                },
                // When logging out, a pop animation feels intuitive
                // You can remove this if you want the default 'push' animation
                animationTypeForReplace: state.isSignout ? 'pop' : 'push',
              }}
            />
          ) : (
            // User is signed in
            <Stack.Screen
              name="Home"
              component={HomeScreen}
              options={{
                title: state.userToken.displayName,
                headerStyle: {
                  backgroundColor: '#f4511e',
                },
                headerTintColor: '#fff',
                headerTitleStyle: {
                  fontWeight: 'bold',
                },
                headerTitleAlign: 'left',
                headerRight: () => (
                  <TouchableOpacity
                    onPress={() => {
                      //authContext.usuario.delete();
                      authContext.signOut();
                    }}>
                    <Text style={styles.logoutButton}>Sair</Text>
                  </TouchableOpacity>
                ),
              }}
            />
          )}
        </Stack.Navigator>
      </NavigationContainer>
    </AuthContext.Provider>
  );
}

export default App;
import React, { useContext, useEffect, useState } from 'react';
import { FlatList, View } from 'react-native';
import { styles } from '../util/styles';
import firestore from '@react-native-firebase/firestore';
import { Button, TextInput } from 'react-native-paper';
import Mensagem from '../components/Mensagem';
import { AuthContext } from '../../App';
import { launchImageLibrary } from 'react-native-image-picker';
import { saveImageMessage } from '../util/firebase';

const HomeScreen = () => {
  const ref = firestore().collection('mensagens');
  const [mensagem, setMensagem] = useState('');
  // const [loading, setLoading] = useState(true);
  const [mensagens, setMensagens] = useState([]);
  const { usuario } = useContext(AuthContext);

  async function enviarMensagem() {
    await ref.add({
      texto: mensagem,
      usuario: usuario.displayName,
      foto: usuario.photoURL,
      timeStamp: firestore.FieldValue.serverTimestamp(),
    });
    setMensagem('');
  }

  function enviarImagem() {
    console.log('escolhendo imagem');
    launchImageLibrary({
      mediaType: 'photo',
    }).then((result) => {
      saveImageMessage(result.assets[0], usuario);
    })
    .catch(error => {
        console.log('-------------------------> ' + error);
    });
  }

  useEffect(() => {
    console.log('--------------- useEffect <<<<<<<<<<<<');
    return ref.orderBy('timeStamp', 'desc').onSnapshot((querySnapshot) => {
      const list = [];
      querySnapshot.forEach((doc) => {
        list.push({
          ...doc.data(),
          id: doc.id,
        });
      });
      setMensagens(list);
      // if (loading) {
      //   setLoading(false);
      // }
    });
  }, []);

  const Separador = () => {
    return <View style={styles.separador} />;
  };

  // if (loading) {
  //   return null;
  // }
  return (
    <>
      <FlatList
        style={styles.mensagens}
        data={mensagens}
        keyExtractor={(item) => item.id}
        renderItem={({ item }) => {
          return <Mensagem {...item} />;
        }}
        ItemSeparatorComponent={Separador}
      />
      <View style={styles.containerTextInput}>
        <TextInput
          label={'Nova mensagem'}
          value={mensagem}
          onChangeText={setMensagem}
          style={styles.textInputStyle}
        />
        <Button
          icon="send"
          onPress={() => enviarMensagem()}
          style={styles.buttonTextInput}
        />
        <Button
          icon="camera"
          onPress={() => enviarImagem()}
          style={styles.buttonTextInput}
        />
      </View>
    </>
  );
};

export default HomeScreen;
import auth from '@react-native-firebase/auth';
import {
  GoogleSignin,
  GoogleSigninButton,
} from '@react-native-google-signin/google-signin';
import React, { useContext, useState } from 'react';
import { View } from 'react-native';
import { AuthContext } from '../../App';
import { styles } from '../util/styles';

export async function logOut() {
  try {
    await GoogleSignin.signOut();
  } catch (error) {
    console.error(error);
  }
}

function SignInScreen() {
  const { signIn } = useContext(AuthContext);
  const [isSigninInProgress, setIsSigninInProgress] = useState(false);

  GoogleSignin.configure({
    webClientId:
      '439977024970-hl040j52rem8m896hh3ta4r1fqejakkd.apps.googleusercontent.com',
  });

  async function onGoogleButtonPress() {
    setIsSigninInProgress(true);
    // Get the users ID token
    const { idToken } = await GoogleSignin.signIn();

    // Create a Google credential with the token
    const googleCredential = auth.GoogleAuthProvider.credential(idToken);

    // Sign-in the user with the credential
    return auth().signInWithCredential(googleCredential);
  }

  return (
    <View style={styles.container}>
      <GoogleSigninButton
        style={styles.button}
        size={GoogleSigninButton.Size.Wide}
        color={GoogleSigninButton.Color.Light}
        onPress={() => onGoogleButtonPress().then((user) => signIn(user.user))}
        disabled={isSigninInProgress}
      />
    </View>
  );
}

export default SignInScreen;
import React from 'react';
import { View, Text, Image } from 'react-native';
import { styles } from '../util/styles';

const SplashScreen = () => {
  const LOADING_IMAGE_URL = 'https://www.google.com/images/spin-32.gif?a';
  return (
    <View style={styles.container}>
      <Text style={styles.titulo}>Projeto X</Text>
      <Image source={{ uri: LOADING_IMAGE_URL }} style={styles.splashLogo} />
    </View>
  );
};

export default SplashScreen;
import React from 'react';
import { View, Image } from 'react-native';
import { Avatar, List } from 'react-native-paper';
import { styles } from '../util/styles';

function Mensagem({ texto, usuario, foto, imageUrl }) {
  return (
    <View>
      <List.Item
        title={usuario + ' falou'}
        titleStyle={{ fontSize: 10 }}
        description={texto}
        descriptionStyle={{ fontSize: 16 }}
        left={() => (
          <Avatar.Image
            size={44}
            source={{
              uri: foto,
            }}
          />
        )}
      />
      {imageUrl ? (
        <Image
          source={{ uri: imageUrl }}
          style={styles.imageMessage}
          resizeMode="contain"
        />
      ) : (
        <></>
      )}
    </View>
  );
}

export default React.memo(Mensagem);
import firestore from '@react-native-firebase/firestore';
import storage from '@react-native-firebase/storage';

var LOADING_IMAGE_URL = 'https://www.google.com/images/spin-32.gif?a';

export async function saveImageMessage(file, usuario) {
  const ref = firestore().collection('mensagens');

  try {
    // 1 - We add a message with a loading icon that will get updated with the shared image.
    const messageRef = await ref.add({
      usuario: usuario.displayName,
      foto: usuario.photoURL,
      timeStamp: firestore.FieldValue.serverTimestamp(),
      imageUrl: LOADING_IMAGE_URL,
    });

    // 2 - Upload the image to Cloud Storage.
    // const filePath = `${usuario.uid}/${messageRef.id}/${file.fileName}`;
    const filePath = `${file.fileName}`;
    const newImageRef = storage().ref(filePath);
    const task = newImageRef.putFile(file.uri);

    task.on('state_changed', (taskSnapshot) => {
      console.log(
        `${taskSnapshot.bytesTransferred} transferred out of ${taskSnapshot.totalBytes}`,
      );
    });

    task.then(() => {
      console.log('Image uploaded to the bucket!');
      // 3 - Generate a public URL for the file.
      newImageRef.getDownloadURL().then((publicImageUrl) => {
        console.log('------------> url da imagem: ' + publicImageUrl);
        // 4 - Update the chat message placeholder with the image's URL.
        messageRef.update({
          imageUrl: publicImageUrl,
        });
      });
    });
  } catch (error) {
    console.error(
      '-----------------> There was an error uploading a file to Cloud Storage:',
      error,
    );
  }
}
import { StyleSheet } from 'react-native';

export const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#18073f',
  },
  navbar: { backgroundColor: '#f4511e' },
  titulo: {
    fontSize: 20,
    fontWeight: 'bold',
    padding: 8,
    color: '#0fa',
  },
  button: {
    // width: 192,
    // height: 48,
    alignSelf: 'center',
  },
  logoutButton: {
    color: '#fff',
  },
  mensagens: {},
  card: { borderColor: '#f00', borderWidth: 2 },
  separador: {
    height: 1,
    width: '100%',
    backgroundColor: '#aaa',
    marginBottom: 4,
  },
  splashLogo: {
    padding: 8,
    width: 32,
    height: 32,
  },
  imageMessage: {
    margin: 16,
    width: '100%',
    height: 200,
  },
  textInputStyle: { flexGrow: 1 },
  containerTextInput: {
    flexDirection: 'row',
    alignItems: 'center',
  },
  buttonTextInput: {},
});