📚 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:
- O catálogo de produtos utilizando o componente nativo
FlatList(otimizado para renderização eficiente de grandes volumes de dados em smartphones). - Armazenamento persistente seguro para credenciais de autenticação (JWT) usando o módulo nativo
expo-secure-store. - 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:
- As rotas de autenticação REST no backend NestJS estão validadas e em funcionamento.
- Você gerou dados fictícios (seeds) para a tabela de produtos utilizando o Prisma no Módulo 02.
🤔 Por que fizemos assim?
- Por que não usar o
AsyncStoragecomum para guardar o JWT? OAsyncStorageescreve arquivos no dispositivo móvel em texto limpo (formato JSON sem criptografia). Qualquer pessoa mal-intencionada ou vírus local com acesso root ao aparelho Android conseguiria copiar o token JWT, sequestrando a sessão do usuário. OSecureStorese comunica com o enclave de segurança criptográfico físico integrado na CPU do smartphone, garantindo segurança de nível bancário para chaves e tokens de acesso. - Por que o
FlatListé melhor que fazer mapeamento manual comScrollView? Quando usamos.map()dentro de uma tagScrollView, o React Native instancia de uma vez só todos os componentes na memória RAM do celular, mesmo que o usuário nunca faça rolagem para visualizá-los. Se o banco possuir 500 produtos cadastrados, o aplicativo travará instantaneamente. OFlatListinstancia dinamicamente apenas os componentes visíveis no tamanho da tela, reduzindo drasticamente o consumo de CPU e RAM.
🔍 Checkpoint
- 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).
- 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(). |