📚 Módulo 11: Mobile - Armazenamento Seguro de Tokens, Catálogo Nativo e Carrinho

Com a base e a navegação configuradas no Módulo 10, desenvolveremos as principais interfaces de negócio da TecLoja 03 utilizando componentes nativos do React Native.

Neste módulo, implementaremos:

  1. O catálogo de produtos utilizando o componente nativo FlatList (otimizado para renderização eficiente de grandes volumes de dados em smartphones).
  2. Armazenamento persistente seguro para credenciais de autenticação (JWT) usando o módulo nativo expo-secure-store.
  3. Um carrinho de compras reativo utilizando a React Context API adaptada para a renderização móvel.

🗺️ 1. Arquitetura de Fluxo de Dados de Compra Nativa

O diagrama abaixo descreve a comunicação assíncrona entre o gerenciamento de estados no dispositivo móvel e as rotas protegidas no NestJS:

flowchart TD
    %% Styling
    classDef react fill:#61DAFB,stroke:#20232A,stroke-width:2px,color:#000;
    classDef secure fill:#FF5722,stroke:#D84315,stroke-width:2px,color:#fff;
    classDef nest fill:#E0234E,stroke:#9F1239,stroke-width:2px,color:#fff;

    A[Catálogo: FlatList]:::react -->|Adicionar| B[Cart Context State]:::react
    B -->|Checkout Assinado| C{Token JWT Local?}:::secure
    C -->|Sim| D[SecureStore: Obter Token]:::secure
    C -->|Não| E[Navegar para Tela de Login]:::react
    D -->|Post Request / Headers Bearer| F[NestJS Endpoint /api/pedidos]:::nest

🔒 2. Persistência Protegida com expo-secure-store

Armazenar tokens JWT sensíveis de alunos em texto claro no AsyncStorage comum equivale a expor dados sigilosos a ataques locais. Em aplicativos profissionais, utilizamos o expo-secure-store, que criptografa os dados em disco utilizando o Keychain (iOS) ou a Keystore (Android).

Instalação

No console do repositório mobile, instale o pacote nativo:

npx expo install expo-secure-store

Implementação do Serviço de Token (services/tokenService.ts)

import * as SecureStore from 'expo-secure-store';

const KEY_TOKEN = 'jwt_auth_token';

export async function saveAuthToken(token: string): Promise<void> {
  await SecureStore.setItemAsync(KEY_TOKEN, token);
}

export async function getAuthToken(): Promise<string | null> {
  return await SecureStore.getItemAsync(KEY_TOKEN);
}

export async function deleteAuthToken(): Promise<void> {
  await SecureStore.deleteItemAsync(KEY_TOKEN);
}

📦 3. Renderização Eficiente de Catálogo com FlatList

Ao contrário de navegadores web onde podemos fazer scroll em milhares de tags div sem travamentos imediatos, dispositivos móveis possuem limitação de processamento gráfico. O componente FlatList resolve isso reciclando elementos fora da área útil de visualização da tela.

Implementação do Catálogo (app/(tabs)/index.tsx)

Substitua o conteúdo da tela de produtos para carregar dinamicamente os itens da API NestJS:

import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View, FlatList, Image, TouchableOpacity, ActivityIndicator } from 'react-native';
import { api } from '../../services/api';

interface Produto {
  id: number;
  nome: string;
  preco: number;
  estoque: number;
  imagemUrl?: string;
}

export default function CatalogoScreen() {
  const [produtos, setProdutos] = useState<Produto[]>([]);
  const [carregando, setCarregando] = useState(true);

  useEffect(() => {
    carregarProdutos();
  }, []);

  const carregarProdutos = async () => {
    try {
      const response = await api.get('/api/produtos');
      setProdutos(response.data);
    } catch (error) {
      console.error("Erro ao buscar produtos:", error);
    } finally {
      setCarregando(false);
    }
  };

  const renderItemProduto = ({ item }: { item: Produto }) => (
    <View style={styles.card}>
      <Image 
        source={{ uri: item.imagemUrl || 'https://via.placeholder.com/150' }} 
        style={styles.imagem} 
      />
      <Text style={styles.nome}>{item.nome}</Text>
      <Text style={styles.preco}>R$ {item.preco.toFixed(2)}</Text>
      
      <TouchableOpacity style={styles.botao}>
        <Text style={styles.botaoTexto}>Adicionar ao Carrinho</Text>
      </TouchableOpacity>
    </View>
  );

  if (carregando) {
    return (
      <View style={styles.loadingContainer}>
        <ActivityIndicator size="large" color="#38bdf8" />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Text style={styles.headerTitle}>🛒 TecLoja 03</Text>
      <FlatList
        data={produtos}
        keyExtractor={(item) => item.id.toString()}
        renderItem={renderItemProduto}
        numColumns={2}
        columnWrapperStyle={styles.row}
        contentContainerStyle={styles.listContent}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#0f172a',
    paddingTop: 16,
  },
  headerTitle: {
    fontSize: 22,
    fontWeight: 'bold',
    color: '#38bdf8',
    textAlign: 'center',
    marginBottom: 16,
  },
  loadingContainer: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#0f172a',
  },
  listContent: {
    paddingHorizontal: 8,
  },
  row: {
    justifyContent: 'space-between',
  },
  card: {
    backgroundColor: '#1e293b',
    flex: 0.48,
    borderRadius: 12,
    padding: 12,
    marginBottom: 16,
    borderWidth: 1,
    borderColor: '#334155',
  },
  imagem: {
    width: '100%',
    height: 120,
    borderRadius: 8,
    backgroundColor: '#0f172a',
    resizeMode: 'contain',
  },
  nome: {
    color: '#f8fafc',
    fontSize: 14,
    fontWeight: '600',
    marginTop: 8,
  },
  preco: {
    color: '#38bdf8',
    fontSize: 16,
    fontWeight: '700',
    marginTop: 4,
  },
  botao: {
    backgroundColor: '#38bdf8',
    borderRadius: 6,
    paddingVertical: 8,
    marginTop: 12,
    alignItems: 'center',
  },
  botaoTexto: {
    color: '#0f172a',
    fontWeight: '700',
    fontSize: 12,
  },
});

🛒 4. Estado Global do Carrinho de Compras

Para controlar a reatividade do carrinho entre as telas do app, implementaremos um Provedor de Contexto do React.

Crie o arquivo /context/CartContext.tsx:

import React, { createContext, useContext, useState } from 'react';

interface CartItem {
  id: number;
  nome: string;
  preco: number;
  quantidade: number;
}

interface CartContextData {
  items: CartItem[];
  adicionarAoCarrinho: (item: Omit<CartItem, 'quantidade'>) => void;
  limparCarrinho: () => void;
  valorTotal: number;
}

const CartContext = createContext<CartContextData>({} as CartContextData);

export function CartProvider({ children }: { children: React.ReactNode }) {
  const [items, setItems] = useState<CartItem[]>([]);

  const adicionarAoCarrinho = (novoItem: Omit<CartItem, 'quantidade'>) => {
    setItems((prevItems) => {
      const itemExistente = prevItems.find((i) => i.id === novoItem.id);
      if (itemExistente) {
        return prevItems.map((i) =>
          i.id === novoItem.id ? { ...i, quantidade: i.quantidade + 1 } : i
        );
      }
      return [...prevItems, { ...novoItem, quantidade: 1 }];
    });
  };

  const limparCarrinho = () => setItems([]);

  const valorTotal = items.reduce((acc, item) => acc + item.preco * item.quantidade, 0);

  return (
    <CartContext.Provider value={{ items, adicionarAoCarrinho, limparCarrinho, valorTotal }}>
      {children}
    </CartContext.Provider>
  );
}

export function useCart() {
  return useContext(CartContext);
}

Não se esqueça de envelopar o componente raiz do seu app em /app/_layout.tsx com o <CartProvider> para disponibilizar o estado global.


✅ Pré-Requisitos deste Módulo

Antes de testar as implementações, verifique se:


🤔 Por que fizemos assim?


🔍 Checkpoint

  1. Carregamento de API: Acesse a tela de catálogo no celular e confirme se o catálogo é renderizado dinamicamente carregando dados do NestJS (com o indicador de loading visual ativo durante a transição).
  2. Criptografia Local: Adicione uma verificação na inicialização para recuperar o token utilizando getAuthToken() e confirme se a aplicação não dispara erros de acesso a chaves do sistema operacional Android.

⚠️ Erros Comuns

Erro Causa Solução
TypeError: Cannot read property 'getItemAsync' of null Falha ao carregar o módulo nativo do Expo Secure Store no emulador local. Reinicie o build do Expo executando npx expo start --clear para limpar caches de compilação do compilador Metro.
Imagens dos produtos não aparecem no FlatList URLs de imagens externas salvas como localhost:3000 no banco não conseguem ser resolvidas pelo emulador. Mude as URLs de imagem para endereços públicos de internet (como Unsplash/Placehold) ou use o IP físico correto do computador.
O catálogo de produtos demora a atualizar ou exibe itens sobrepostos O componente FlatList exige uma propriedade chave estável identificadora de tipo texto. Certifique-se de que a propriedade keyExtractor converte o ID numérico do banco de dados para string via .toString().

Voltar para o Sumário