Capítulo 33 — Aplicativo Mobile: React Native
Este capítulo detalha a implementação técnica do Aplicativo Mobile do Cidadão — o canal digital nativo para iOS e Android que complementa o Portal Web com experiência adaptada a dispositivos móveis.
33.1 Objetivo do Capítulo
Este capítulo detalha a implementação técnica do Aplicativo Mobile do Cidadão — o canal digital nativo para iOS e Android que complementa o Portal Web com experiência adaptada a dispositivos móveis.
O capítulo descreve: stack tecnológica (React Native, TypeScript, NativeWind), arquitetura de componentes e navegação, gerenciamento de estado, integração com APIs de backend (mesmos contratos do portal), autenticação (local, GOV.BR com PKCE, biometria), notificações push nativas (APNS/FCM), captura de documentos por câmera, deep linking, segurança mobile (armazenamento nativo, certificate validation, OWASP MASVS), acessibilidade (VoiceOver/TalkBack), performance, distribuição (App Store/Play Store) e compatibilidade de versão.
O aplicativo mobile não reimplementa regras de negócio. Consome as mesmas APIs REST do backend que o portal web, diferenciando-se por experiência e adaptações de dispositivo.
33.2 Contexto e Posicionamento
O cidadão acessa a plataforma por múltiplos canais: Portal Web em navegador (computador, tablet, celular) e Aplicativo Mobile nativo (iOS, Android). O aplicativo mobile coexiste com o portal — não o substitui.
O aplicativo é especialmente útil para:
- notificações push: o cidadão recebe alerta de atualização de protocolo sem precisar abrir o navegador;
- captura de documentos: câmera do dispositivo substitui scanner físico;
- biometria local: desbloqueia o aplicativo entre sessões após autenticação inicial;
- jornadas on-the-go: formulários com pickers nativos e validação contextual;
- offline-aware: indica claramente quando offline; não apresenta dados desatualizados como atuais.
A escolha de React Native permite: desenvolvimento com uma única base de código para iOS e Android, compartilhamento de lógica de interface com o portal web, acesso controlado às APIs nativas do dispositivo, e evolução conjunta entre canais.
33.3 Stack Tecnológica
33.3.1 Dependências Base
react-native@0.73+ (framework)
typescript@5.3+ (tipagem)
@react-native-community/cli (CLI)
react-native-windows (suporte a Windows quando necessário)
33.3.2 Estilização e Design System
nativewind@4.0+ (Tailwind CSS para React Native)
@brasfy/core (design system compartilhado com portal)
react-native-svg (renderização de SVGs)
react-native-linear-gradient (gradientes)
33.3.3 Navegação
@react-navigation/native@6.1+ (framework de navegação)
@react-navigation/bottom-tabs (abas na base)
@react-navigation/native-stack (pilha de navegação)
@react-navigation/drawer (gaveta lateral quando necessária)
react-native-screens (otimização de performance)
react-native-safe-area-context (gerenciamento de safe areas)
33.3.4 Gerenciamento de Estado
zustand@4.4+ ou Redux Toolkit (estado compartilhado)
redux-persist (persistência de estado)
immer (mutations imutáveis)
33.3.5 HTTP e Integração Backend
axios@1.6+ (cliente HTTP)
react-query ou @tanstack/react-query (cache e sincronização de dados)
33.3.6 Autenticação e Segurança
react-native-keychain (acesso seguro a Keychain/Keystore)
react-native-secure-randombytes (PKCE code_verifier)
react-native-config (variáveis de ambiente)
33.3.7 Câmera e Scanner
react-native-vision-camera@2.0+ (acesso à câmera)
react-native-skia (processamento de imagem)
ml-kit (Google ML Kit) (detecção de documentos via ML)
33.3.8 Notificações Push
@react-native-firebase/app (Firebase — gerenciador)
@react-native-firebase/messaging (FCM — Android e iOS opcional)
react-native-push-notification (APNS — iOS; FCM — Android)
33.3.9 Autenticação Biométrica
react-native-biometrics (Face ID, Touch ID, impressão digital)
react-native-localize (détection de linguagem do dispositivo)
33.3.10 Observabilidade
@sentry/react-native (crash reporting e error tracking)
react-native-performance-metrics (métricas de performance)
33.4 Arquitetura de Componentes
33.4.1 Estrutura de Diretórios
src/
├── assets/
│ ├── images/
│ ├── icons/
│ └── fonts/
├── screens/
│ ├── auth/
│ │ ├── LoginScreen.tsx
│ │ ├── GovBrFlowScreen.tsx
│ │ └── BiometrySetupScreen.tsx
│ ├── home/
│ │ └── HomeScreen.tsx
│ ├── services/
│ │ ├── CatalogScreen.tsx
│ │ ├── ServiceDetailScreen.tsx
│ │ └── ServiceSearchScreen.tsx
│ ├── requests/
│ │ ├── RequestListScreen.tsx
│ │ ├── RequestDetailScreen.tsx
│ │ ├── RequestCreateScreen.tsx
│ │ └── DocumentUploadScreen.tsx
│ ├── messages/
│ │ └── MessagesScreen.tsx
│ └── profile/
│ ├── ProfileScreen.tsx
│ ├── PersonalDataScreen.tsx
│ └── PrivacyScreen.tsx
├── components/
│ ├── FormField/
│ ├── DocumentPicker/
│ ├── CameraScanner/
│ ├── NotificationBanner/
│ ├── LoadingIndicator/
│ └── ... (componentes reutilizáveis)
├── services/
│ ├── api/
│ │ ├── client.ts (instância de Axios)
│ │ ├── requests.ts
│ │ ├── services.ts
│ │ ├── messages.ts
│ │ └── ... (serviços de API)
│ ├── auth/
│ │ ├── authService.ts (IAM próprio + GOV.BR)
│ │ ├── keychain.ts (armazenamento seguro)
│ │ └── biometry.ts
│ ├── storage/
│ │ ├── localStorage.ts (dados não sensíveis em cache)
│ │ └── secureStorage.ts (tokens em Keychain/Keystore)
│ └── notifications/
│ └── pushService.ts
├── store/
│ ├── auth/
│ │ ├── authSlice.ts
│ │ └── authSelectors.ts
│ ├── requests/
│ │ ├── requestsSlice.ts
│ │ └── requestsSelectors.ts
│ ├── ui/
│ │ └── uiSlice.ts
│ └── store.ts (configuração Redux)
├── hooks/
│ ├── useAuth.ts
│ ├── useRequests.ts
│ ├── usePushNotifications.ts
│ ├── useBiometry.ts
│ └── ... (custom hooks)
├── utils/
│ ├── deeplink.ts (validação e processamento)
│ ├── errorHandler.ts
│ ├── logger.ts
│ ├── dateFormatter.ts
│ └── validators.ts
├── navigation/
│ ├── RootNavigator.tsx (stack raiz)
│ ├── AuthNavigator.tsx
│ ├── AppNavigator.tsx (abas principais)
│ ├── RequestsStackNavigator.tsx
│ └── ProfileStackNavigator.tsx
├── types/
│ ├── api.ts (tipos de resposta de API)
│ ├── domain.ts (modelos de negócio)
│ └── ui.ts (tipos de UI)
├── config/
│ ├── constants.ts
│ ├── environment.ts
│ └── apiConfig.ts
├── i18n/
│ ├── en.json
│ ├── pt-BR.json
│ └── i18n.ts
└── App.tsx (entrada)
33.4.2 Princípios de Componentes
- reutilização: componentes sem lógica de negócio; compostos em telas;
- tipagem rigorosa: TypeScript com tipos explícitos; nenhum
any; - sem side effects: efeitos colaterais em hooks customizados ou no camada de serviço;
- props simples: componentes aceitam dados simples; lógica complexa fica em containers;
- acesso a store via selectors: nunca
useSelector(state => state.requests.items)espalhado em componentes.
33.4.3 Exemplo de Componente
import React from 'react';
import { View, TouchableOpacity, Text } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { RequestCard } from './RequestCard';
import type { Request } from '../types/domain';
interface RequestListProps {
requests: Request[];
loading: boolean;
onRefresh: () => void;
}
export const RequestList: React.FC<RequestListProps> = ({
requests,
loading,
onRefresh,
}) => {
const navigation = useNavigation();
return (
<View className="flex-1 bg-white">
{requests.length === 0 ? (
<View className="flex-1 items-center justify-center">
<Text className="text-gray-500">Nenhuma solicitação encontrada</Text>
</View>
) : (
<FlatList
data={requests}
renderItem={({ item }) => (
<RequestCard
request={item}
onPress={() =>
navigation.navigate('RequestDetail', { requestId: item.id })
}
/>
)}
keyExtractor={(item) => item.id}
onRefresh={onRefresh}
refreshing={loading}
/>
)}
</View>
);
};
33.5 Navegação
33.5.1 Estrutura de Navegação
O aplicativo utiliza Tab Navigation na raiz com 5 abas principais:
RootNavigator
├── AuthNavigator (quando não autenticado)
│ ├── LoginScreen
│ ├── SignUpScreen
│ └── GovBrFlowScreen
└── AppNavigator (quando autenticado)
├── HomeTab
│ └── HomeNavigator (stack)
│ └── HomeScreen
├── ServicesTab
│ └── ServicesNavigator (stack)
│ ├── CatalogScreen
│ ├── ServiceDetailScreen
│ └── RequestCreateScreen
├── RequestsTab
│ └── RequestsNavigator (stack)
│ ├── RequestListScreen
│ ├── RequestDetailScreen
│ └── DocumentUploadScreen
├── MessagesTab
│ └── MessagesNavigator (stack)
│ └── MessagesScreen
└── ProfileTab
└── ProfileNavigator (stack)
├── ProfileScreen
├── PersonalDataScreen
├── PrivacyScreen
└── LogoutScreen
33.5.2 Deep Linking
Deep links são registrados no nível do sistema operacional via linking config do React Navigation:
const linking = {
prefixes: ['plataforma://', 'https://plataforma.app'],
config: {
screens: {
RequestDetail: 'requests/:requestId',
ServiceDetail: 'services/:serviceId',
NewRequest: 'services/:serviceId/new',
Messages: 'messages',
AgendamentDetail: 'agendamentos/:agendamentoId',
},
},
};
Validação de deep link:
function validateDeepLink(url: string): boolean {
// Validar esquema
if (!url.startsWith('plataforma://')) return false;
// Validar formato
const pattern = /^plataforma:\/\/(requests|services|messages|agendamentos)\/[a-f0-9\-]+$/;
if (!pattern.test(url)) return false;
// Validar parâmetros (nunca usados como token ou autorização)
return true;
}
33.6 Gerenciamento de Estado
33.6.1 Estrutura Redux com Slices
// store/auth/authSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface AuthState {
isAuthenticated: boolean;
token: string | null;
user: User | null;
loading: boolean;
error: string | null;
}
const initialState: AuthState = {
isAuthenticated: false,
token: null,
user: null,
loading: false,
error: null,
};
export const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setAuthenticated: (state, action: PayloadAction<{
token: string;
user: User;
}>) => {
state.isAuthenticated = true;
state.token = action.payload.token;
state.user = action.payload.user;
state.error = null;
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
},
logout: (state) => {
state.isAuthenticated = false;
state.token = null;
state.user = null;
state.error = null;
},
},
});
export const { setLoading, setAuthenticated, setError, logout } = authSlice.actions;
33.6.2 Persistência de Estado
import { persistStore, persistReducer } from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage';
const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['auth'], // apenas auth persiste
blacklist: ['ui'], // ui não persiste
};
const persistedReducer = persistReducer(persistConfig, rootReducer);
export const store = configureStore({
reducer: persistedReducer,
});
export const persistor = persistStore(store);
33.6.3 Selectors
export const selectIsAuthenticated = (state: RootState) =>
state.auth.isAuthenticated;
export const selectUser = (state: RootState) =>
state.auth.user;
export const selectToken = (state: RootState) =>
state.auth.token;
// Uso em componente
const isAuthenticated = useSelector(selectIsAuthenticated);
33.7 Autenticação
33.7.1 Fluxo de Login Local
export const loginWithCredentials = async (
email: string,
password: string
): Promise<AuthResponse> => {
try {
const response = await apiClient.post('/auth/login', {
email,
password,
});
const { token, user } = response.data;
// Armazenar token em Keychain/Keystore
await secureStorage.setToken(token);
// Atualizar Redux
dispatch(setAuthenticated({ token, user }));
return { success: true, user };
} catch (error) {
const message = error.response?.data?.message || 'Erro ao autenticar';
dispatch(setError(message));
return { success: false, error: message };
}
};
33.7.2 Autenticação com GOV.BR (PKCE)
export const initiateGovBrFlow = async (): Promise<void> => {
// 1. Gerar PKCE pair
const codeVerifier = await generateCodeVerifier(); // 128 caracteres aleatórios
const codeChallenge = await generateCodeChallenge(codeVerifier);
const state = await generateCodeVerifier(); // state único para CSRF
// 2. Armazenar temporariamente (será limpo após troca)
await secureStorage.setTemporaryValue('pkce_verifier', codeVerifier);
await secureStorage.setTemporaryValue('oauth_state', state);
// 3. Preparar Authorization Request
const params = new URLSearchParams({
client_id: GOV_BR_CLIENT_ID,
response_type: 'code',
scope: 'openid profile email',
redirect_uri: 'plataforma://oauth/callback',
code_challenge: codeChallenge,
code_challenge_method: 'S256',
state: state,
});
const authUrl = `${GOV_BR_AUTH_ENDPOINT}?${params.toString()}`;
// 4. Abrir navegador nativo
await Linking.openURL(authUrl);
};
// Receber callback via deep link
export const handleGovBrCallback = async (code: string, state: string) => {
try {
// 1. Validar state
const storedState = await secureStorage.getTemporaryValue('oauth_state');
if (state !== storedState) {
throw new Error('CSRF validation failed');
}
// 2. Recuperar code_verifier
const codeVerifier = await secureStorage.getTemporaryValue('pkce_verifier');
// 3. Trocar code por token no backend
const response = await apiClient.post('/auth/gov-br/callback', {
code,
codeVerifier,
redirectUri: 'plataforma://oauth/callback',
});
const { token, user } = response.data;
// 4. Armazenar token seguro
await secureStorage.setToken(token);
// 5. Atualizar Redux
dispatch(setAuthenticated({ token, user }));
// 6. Limpar valores temporários
await secureStorage.removeTemporaryValues(['pkce_verifier', 'oauth_state']);
} catch (error) {
dispatch(setError('Falha na autenticação GOV.BR'));
}
};
33.7.3 Biometria Local
export const setupBiometry = async (): Promise<boolean> => {
try {
// Verificar disponibilidade
const available = await ReactNativeBiometrics.isSensorAvailable();
if (!available) return false;
// Solicitar biometria
const { success } = await ReactNativeBiometrics.simplePrompt({
promptMessage: 'Use sua biometria para desbloquear',
});
if (success) {
// Marcar que biometria foi habilitada
await secureStorage.setBiometryEnabled(true);
return true;
}
return false;
} catch (error) {
console.error('Erro ao configurar biometria:', error);
return false;
}
};
export const unlockWithBiometry = async (): Promise<boolean> => {
try {
const { success } = await ReactNativeBiometrics.simplePrompt({
promptMessage: 'Desbloqueie o aplicativo',
});
if (success) {
// Recuperar token armazenado
const token = await secureStorage.getToken();
if (token) {
dispatch(setAuthenticated({ token, user: getCurrentUser() }));
return true;
}
}
return false;
} catch (error) {
return false;
}
};
33.8 Camada de Persistência Local
33.8.1 Armazenamento Seguro
O armazenamento local no React Native utilize bibliotecas nativas para criptografia transparente:
- iOS: Keychain via
react-native-keychain - Android: EncryptedSharedPreferences ou Keystore via
react-native-encrypted-storage
Dados armazenados localmente:
| Dado | Armazenamento | Lifetime | Criptografia |
|---|---|---|---|
| Token de sessão | Keychain/EncryptedSharedPreferences | Até logout ou expiração | Nativa do SO |
| Refresh token (se adotado) | Keychain/EncryptedSharedPreferences | Conforme política | Nativa do SO |
| Preferências de usuário | AsyncStorage comum | Indefinido | Sem requisito |
| Cache de catálogo (conteúdo público) | AsyncStorage comum | TTL 24h | Não exigido |
| Dados pessoais sensíveis | Nunca armazenados localmente | — | — |
33.8.2 Sincronização de Cache
O cache local de dados públicos (catálogo de serviços, configurações de tenant) é mantido com TTL configurável. Quando o aplicativo detecta que os dados locais expirou e há conexão disponível, ele atualiza do backend automaticamente. Quando offline, exibe os dados em cache com indicação clara de que podem estar desatualizados.
33.9 Notificações Push
33.9.1 Integração com APNS e FCM
import { messaging } from '@react-native-firebase/app';
import PushNotification from 'react-native-push-notification';
export const setupPushNotifications = async () => {
// iOS — APNS token
if (Platform.OS === 'ios') {
messaging().requestPermission().then((authStatus) => {
if (authStatus === messaging.AuthorizationStatus.AUTHORIZED) {
messaging().getToken().then((token) => {
registerPushToken(token);
});
}
});
}
// Android — FCM token
if (Platform.OS === 'android') {
messaging().getToken().then((token) => {
registerPushToken(token);
});
}
// Listener para notificações recebidas
messaging().onMessage(async (remoteMessage) => {
handleNotification(remoteMessage);
});
// Listener para notificações quando app está em background
messaging().onNotificationOpenedApp((remoteMessage) => {
navigateFromNotification(remoteMessage);
});
};
const registerPushToken = async (token: string) => {
try {
await api.post('/notifications/push-token', {
token,
platform: Platform.OS,
deviceId: getDeviceId(),
});
} catch (error) {
console.error('Erro ao registrar token de push:', error);
}
};
33.9.2 Tratamento de Notificações
const handleNotification = (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
const { notification, data } = remoteMessage;
// Exibir notificação no cliente
PushNotification.localNotification({
channelId: 'default-channel',
title: notification?.title,
message: notification?.body,
bigText: notification?.body,
data: data || {},
});
// Registrar recebimento no backend
logNotificationDelivery(remoteMessage.messageId);
};
const navigateFromNotification = (remoteMessage: FirebaseMessagingTypes.RemoteMessage) => {
const { data } = remoteMessage;
if (data?.type === 'protocol_updated') {
navigation.navigate('ProtocolDetail', { protocolId: data.protocolId });
} else if (data?.type === 'complementation_needed') {
navigation.navigate('Complementation', { requestId: data.requestId });
}
};
33.10 Captura de Documentos via Câmera
33.10.1 Scanner com Correção de Perspectiva
import { useCamera } from '@react-native-camera-roll/camera-roll';
import DocumentScanner from 'react-native-document-scanner-api';
export const DocumentCaptureScreen = () => {
const [scannedDocument, setScannedDocument] = useState<string | null>(null);
const captureDocument = async () => {
try {
// Abrir scanner com guia visual
const result = await DocumentScanner.scanDocument({
croppedImageQuality: 100,
responseType: 'base64',
});
if (result && result.scannedImage) {
// Armazenar localmente para preview
setScannedDocument(result.scannedImage);
}
} catch (error) {
if (error.code !== 'USER_CANCELLED') {
Alert.alert('Erro', 'Não foi possível capturar o documento. Tente novamente.');
}
}
};
const uploadDocument = async () => {
if (!scannedDocument) return;
try {
const formData = new FormData();
formData.append('file', {
uri: `data:image/jpeg;base64,${scannedDocument}`,
type: 'image/jpeg',
name: `document_${Date.now()}.jpg`,
});
formData.append('type', 'RG');
const response = await api.post('/documents/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
});
Alert.alert('Sucesso', 'Documento enviado com sucesso.');
setScannedDocument(null);
} catch (error) {
Alert.alert('Erro', 'Falha ao enviar documento.');
}
};
return (
<View style={styles.container}>
{scannedDocument ? (
<>
<Image source={{ uri: `data:image/jpeg;base64,${scannedDocument}` }} style={styles.preview} />
<Button title="Confirmar" onPress={uploadDocument} />
<Button title="Refazer" onPress={() => setScannedDocument(null)} />
</>
) : (
<Button title="Capturar Documento" onPress={captureDocument} />
)}
</View>
);
};
33.11 Navegação e Deep Links
33.11.1 Configuração de Deep Links
import { NavigationContainer } from '@react-navigation/native';
import { linking } from './linking';
const linking: LinkingOptions<RootStackParamList> = {
prefixes: ['plataforma://', 'https://app.example.com'],
config: {
screens: {
ProtocolDetail: 'protocolo/:protocolId',
Complementation: 'complementacao/:requestId',
NewRequest: 'solicitacao/novo?serviceId=:serviceId',
Messages: 'mensagens',
Profile: 'perfil',
NotFound: '*',
},
},
};
export const AppNavigator = () => {
const linking = useMemo(() => ({
prefixes: ['plataforma://', 'https://app.example.com'],
config: { /* ... */ },
async getInitialURL() {
// Processar deep link inicial
const url = await getInitialURL();
if (url != null) {
return url;
}
return undefined;
},
subscribe(listener: (url: string) => void) {
// Listener para deep links recebidos enquanto app está em background
const onReceiveURL = ({ url }: { url: string }) => {
listener(url);
};
const subscription = linking.addEventListener('url', onReceiveURL);
return () => {
subscription.remove();
};
},
}), []);
return (
<NavigationContainer linking={linking}>
<RootNavigator />
</NavigationContainer>
);
};
33.11.2 Validação de Deep Links
const validateDeepLink = (url: string): boolean => {
try {
const parsed = new URL(url);
// Validar esquema
if (!['plataforma://', 'https://app.example.com'].includes(parsed.origin)) {
return false;
}
// Validar path e parâmetros
const pathname = parsed.pathname;
const allowedPaths = ['/protocolo', '/complementacao', '/solicitacao', '/mensagens', '/perfil'];
return allowedPaths.some((path) => pathname.startsWith(path));
} catch {
return false;
}
};
33.12 Performance e Otimizações
33.12.1 Renderização Eficiente
// Componente de lista com virtualização
import { FlatList } from 'react-native';
const RequestsList = ({ requests }: { requests: RequestItem[] }) => {
const keyExtractor = (item: RequestItem) => item.id;
const renderItem = ({ item }: { item: RequestItem }) => <RequestCard request={item} />;
return (
<FlatList
data={requests}
renderItem={renderItem}
keyExtractor={keyExtractor}
maxToRenderPerBatch={10}
updateCellsBatchingPeriod={50}
initialNumToRender={20}
removeClippedSubviews={true}
/>
);
};
33.12.2 Lazy Loading de Imagens
import FastImage from 'react-native-fast-image';
const OptimizedImage = ({ uri, style }: { uri: string; style: any }) => (
<FastImage
source={{ uri, priority: FastImage.priority.normal }}
style={style}
resizeMode={FastImage.resizeMode.contain}
/>
);
33.12.3 Monitoramento de Performance
import { performance } from 'react-native-perf';
export const measureScreenLoad = (screenName: string) => {
const startTime = performance.now();
return () => {
const duration = performance.now() - startTime;
logMetric(`screen_load_${screenName}`, duration);
};
};
33.13 Testes no Aplicativo Mobile
33.13.1 Testes Unitários e de Integração
import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { AuthScreen } from './AuthScreen';
describe('AuthScreen', () => {
it('deve fazer login com credenciais válidas', async () => {
const { getByText, getByPlaceholderText } = render(<AuthScreen />);
fireEvent.changeText(getByPlaceholderText('E-mail'), 'user@example.com');
fireEvent.changeText(getByPlaceholderText('Senha'), 'password123');
fireEvent.press(getByText('Entrar'));
await waitFor(() => {
expect(screen.queryByText('Carregando')).not.toBeOnTheScreen();
});
});
it('deve exibir erro com credenciais inválidas', async () => {
const { getByText, getByPlaceholderText } = render(<AuthScreen />);
fireEvent.changeText(getByPlaceholderText('E-mail'), 'invalid@example.com');
fireEvent.changeText(getByPlaceholderText('Senha'), 'wrong');
fireEvent.press(getByText('Entrar'));
await waitFor(() => {
expect(screen.getByText('Credenciais inválidas')).toBeOnTheScreen();
});
});
});
33.13.2 Testes E2E
import { by, element, expect as detoxExpect } from 'detox';
describe('Request Creation E2E', () => {
beforeAll(async () => {
await device.launchApp();
});
it('deve criar uma solicitação nova', async () => {
await element(by.id('tab_services')).tap();
await element(by.text('Habilitação')).tap();
await element(by.id('btn_start_request')).tap();
await element(by.id('input_name')).typeText('João Silva');
await element(by.id('btn_submit')).tap();
await detoxExpect(element(by.text('Protocolo gerado'))).toBeVisible();
});
});
33.14 Segurança Avançada
33.14.1 Certificate Pinning
import axios from 'axios';
import RNFetchBlob from 'rn-fetch-blob';
const createPinnedClient = () => {
const certificate = require('./certificates/api-certificate.pem');
return RNFetchBlob.config({
trusty: true,
sslPinning: {
certs: ['api-certificate.pem'],
},
}).fetch;
};
export const api = axios.create({
baseURL: API_BASE_URL,
adapter: createPinnedClient(),
});
33.14.2 Proteção de Telas Sensíveis
import { useFocusEffect } from '@react-navigation/native';
export const SensitiveScreen = () => {
useFocusEffect(
useCallback(() => {
// Ao entrar na tela, desabilitar screenshots
ViewShot.captureRef(viewRef, { format: 'png', quality: 0.8 }).catch(() => {
// Capture desabilitada em telas sensíveis
});
return () => {
// Ao sair, limpar dados sensíveis
clearSensitiveData();
};
}, []),
);
return (
<ViewShot ref={viewRef} options={{ format: 'png', quality: 0.8 }}>
{/* Conteúdo sensível */}
</ViewShot>
);
};
33.15 Observabilidade e Crash Reporting
33.15.1 Integração com Crash Reporting
import crashlytics from '@react-native-firebase/crashlytics';
// Configurar listener global de erros
setJSExceptionHandler((error, isFatal) => {
crashlytics().recordError(error);
if (isFatal) {
// Enviar relatório crítico
crashlytics().log('Fatal error occurred');
}
});
// Capturar erros de promise não tratada
setJSExceptionHandler((error: Error, isFatal: boolean) => {
crashlytics().recordError(error);
}, true);
33.15.2 Métricas de Experiência
import { logEvent, setUserProperty } from '@react-native-firebase/analytics';
export const logScreenView = (screenName: string) => {
logEvent('screen_view', {
firebase_screen: screenName,
firebase_screen_class: screenName,
});
};
export const logRequestCreation = (serviceId: string) => {
logEvent('request_created', {
service_id: serviceId,
timestamp: new Date().toISOString(),
});
};
export const setUserContext = (userId: string) => {
setUserProperty('user_id', userId);
setUserProperty('app_version', getAppVersion());
};
33.16 Acessibilidade (VoiceOver / TalkBack)
33.16.1 Configuração de Acessibilidade
<View
accessible={true}
accessibilityLabel="Lista de solicitações"
accessibilityHint="Deslize para baixo para atualizar"
accessibilityRole="list"
>
{requests.map((request) => (
<RequestCard key={request.id} request={request} />
))}
</View>
// Botão com descrição acessível
<TouchableOpacity
accessible={true}
accessibilityLabel="Abrir solicitação"
accessibilityRole="button"
accessibilityState={{ disabled: loading }}
onPress={handleOpenRequest}
>
<Text>Abrir</Text>
</TouchableOpacity>
// Imagem com descrição
<Image
source={require('./logo.png')}
accessibilityLabel="Logo do órgão"
accessible={true}
/>
33.16.2 Dynamic Type e Font Scaling
import { Text, PixelRatio } from 'react-native';
const fontScale = PixelRatio.getFontScale();
const styles = StyleSheet.create({
body: {
fontSize: 16 * Math.min(fontScale, 1.3), // limitar escala a 30%
lineHeight: 24,
},
heading: {
fontSize: 24 * Math.min(fontScale, 1.3),
fontWeight: '600',
},
});
33.17 Build e Distribuição
33.17.1 Configuração de Build
// app.config.js (React Native CLI)
export default {
name: 'Cidadão',
slug: 'cidadao',
version: '1.2.3',
ios: {
bundleIdentifier: 'br.gov.mg.cidadao',
buildNumber: '203',
supportsTablet: true,
infoPlist: {
NSCameraUsageDescription: 'Permitimos capturar documentos para envio.',
NSFaceIDUsageDescription: 'Permitimos desbloquear com biometria para sua conveniência.',
},
},
android: {
package: 'br.gov.mg.cidadao',
versionCode: 203,
permissions: [
'android.permission.CAMERA',
'android.permission.USE_BIOMETRIC',
'android.permission.USE_FINGERPRINT',
],
},
extra: {
apiUrl: process.env.API_URL || 'https://api.dev.example.com',
sentryDsn: process.env.SENTRY_DSN,
},
};
33.17.2 Compatibilidade de Versão
const MIN_VERSION = '1.0.0';
const CURRENT_VERSION = '1.2.3';
const checkCompatibility = async () => {
try {
const response = await api.get('/version/check');
const { minimumVersion, supportedUntil } = response.data;
if (compareVersions(CURRENT_VERSION, minimumVersion) < 0) {
Alert.alert(
'Atualização necessária',
'Esta versão do aplicativo não é mais suportada. Atualize na loja de aplicativos.',
[{ text: 'Atualizar', onPress: () => Linking.openURL(APP_STORE_URL) }],
);
}
} catch (error) {
console.error('Falha ao verificar compatibilidade:', error);
}
};
33.18 Riscos e Mitigações
| Risco | Consequência | Mitigação |
|---|---|---|
| Token armazenado em AsyncStorage comum | Roubo por aplicativo malicioso | Armazenar em Keychain/EncryptedSharedPreferences |
| Secret embutido no bundle (chave de API) | Exposição por descompilação | Nenhum secret no bundle; apenas URLs públicas |
| WebView executando JavaScript não confiável | XSS, acesso a APIs nativas | Browser in-app do SO para fluxos críticos; WebView apenas com origem confiável |
| Falta de validação em deep link | Redirecionamento a tela interna com parâmetros maliciosos | Validação de URL e parâmetros antes de rotear |
| Screenshot de tela sensível | Exposição de token ou dado pessoal | FLAG_SECURE no Android, blur em background no iOS |
| Biometria como único fator | Acesso por impressão digital não autorizada | Token de sessão sempre re-validado no backend |
| Versão antiga do app sem correções | Exploração de vulnerabilidade | Versão mínima suportada + 426 Upgrade Required |
| Push token inativo acumulando | Falha silenciosa de entrega | Remoção de token quando APNS/FCM retorna erro permanente |
| Conflito de merge em fluxos assíncronos | Estado inconsistente | Imutabilidade + comparação de versão otimista |
| Não detectar perda de conexão | Exibição de dados obsoletos como atuais | Indicador claro de offline; ações críticas bloqueadas |
33.19 Benefícios do Aplicativo Mobile em React Native
- Canal complementar ao portal com experiência otimizada para smartphone;
- Notificações push nativas que mantém o cidadão informado sem abrir o app;
- Câmera integrada que elimina fricção no envio de documentos;
- Biometria local que equilibra conveniência e segurança;
- Deep links que conectam notificações a jornadas específicas;
- Mesmos contratos de API — sem duplicação de regras de negócio;
- Suporte simultâneo a iOS e Android com uma base de código única;
- Acessibilidade nativa (VoiceOver, TalkBack, Dynamic Type);
- Performance monitorada com métricas reais (FCP, TTI);
- Distribuição escalável via App Store e Google Play.
33.20 Decisões Arquiteturais
| ADR | Tema |
|---|---|
| ADR-313 | React Native + TypeScript como stack do aplicativo mobile |
| ADR-314 | Redux Toolkit para estado compartilhado |
| ADR-315 | NativeWind para estilização baseada em Tailwind |
| ADR-316 | React Navigation com deep linking configurado |
| ADR-317 | Keychain/EncryptedSharedPreferences para tokens |
| ADR-318 | PKCE com state para autenticação GOV.BR |
| ADR-319 | Biometria local como conveniência (não fator de autenticação) |
| ADR-320 | Notificações push via FCM (Android) e APNS (iOS) |
| ADR-321 | Scanner de documento com correção de perspectiva |
| ADR-322 | Certificate pinning para APIs críticas |
| ADR-323 | Compatibilidade de versão: X-Client-Version + versão mínima |
33.21 Considerações Finais
O Aplicativo Mobile em React Native é o canal que acompanha o cidadão no dia a dia. Notificação push que informa atualização de protocolo, câmera que captura documento sem scanner externo, biometria que desbloqueia o aplicativo com um toque — esses são os elementos que diferenciam a experiência mobile.
A arquitetura com mesma base de código para iOS e Android, sem duplicação de lógica de negócio e com segurança por padrão, garante que essa experiência superior não venha à custa de segurança ou de inconsistência entre canais.
A separação entre cliente (React Native) e backend (Java/Spring Boot) permite evolução independente: a plataforma pode adicionar novas funcionalidades mobile sem alterar o backend, e vice-versa.
O Capítulo 34 detalha Banco de Dados e Persistência.
33.22 Controle de Versão
| Campo | Valor |
|---|---|
| Documento | Documento Mestre — Plataforma de Relacionamento Digital com o Cidadão |
| Capítulo | 33 — Aplicativo Mobile: React Native |
| Versão | 1.0 |
| Situação | Concluído |
| Última atualização | 16/07/2026 |
33.23 Rastreabilidade PRODEMGE
- [ANX-III] Bloco 1 — itens 1.1, 1.2, 1.6, 1.10, 1.11 cobertos pelo canal mobile em React Native.
- [ANX-IV] — Capacidades técnicas de canal mobile nativo, notificações push (APNS/FCM), captura de documentos por câmera, biometria local, deep linking, compatibilidade de versão.
- [ANX-V] Item 2.1 — Manutenibilidade com design system @brasfy/core compartilhado, TypeScript em todo o código, padrões consistentes.
- [ANX-V] Item 2.2 — Acessibilidade: VoiceOver (iOS), TalkBack (Android), Dynamic Type, contraste WCAG 2.1 AA, testes com leitores de tela em dispositivos reais.
- [ANX-V] Item 2.3 — Proteção do usuário: OWASP MASVS como referência, Keychain/EncryptedSharedPreferences para tokens, certificate pinning, sem secrets no bundle, validação de deep links, autorização sempre server-side.
- [PNR] — Aplicativo Mobile como canal complementar ao Portal Web, com adaptadores de dispositivo (câmera, biometria, push, deep links).
- [EDITAL] — Edital CP001/2026: canal mobile do cidadão com jornadas completas, notificações push nativas, captura de documentos, integração GOV.BR.
Fim do Capítulo 33 — Aplicativo Mobile: React Native
33.8 Armazenamento Seguro
33.8.1 Estratégia de Armazenamento
// services/storage/secureStorage.ts
export const secureStorage = {
// Tokens e dados sensíveis → Keychain / Keystore
async setToken(token: string): Promise<void> {
await RNKeychain.setGenericPassword('plataforma', token, {
service: 'token',
accessibilitySharedUIElements: [], // não em screenshots
});
},
async getToken(): Promise<string | null> {
const credentials = await RNKeychain.getGenericPassword({
service: 'token',
});
return credentials ? credentials.password : null;
},
async clearToken(): Promise<void> {
await RNKeychain.resetGenericPassword({ service: 'token' });
},
// Dados não sensíveis → AsyncStorage com cache
async setCacheData(key: string, data: any): Promise<void> {
await AsyncStorage.setItem(key, JSON.stringify(data));
},
async getCacheData(key: string): Promise<any | null> {
const data = await AsyncStorage.getItem(key);
return data ? JSON.parse(data) : null;
},
// Valores temporários (PKCE, state) → Memória durante a sessão
async setTemporaryValue(key: string, value: string): Promise<void> {
temporaryStorage[key] = value;
// Auto-limpar após 10 minutos
setTimeout(() => delete temporaryStorage[key], 10 * 60 * 1000);
},
async getTemporaryValue(key: string): Promise<string | null> {
return temporaryStorage[key] || null;
},
};
Bloco 1 concluído (seções 33.1 a 33.8). Aguardando confirmação para prosseguir com o Bloco 2.
Capítulo 32 — Frontend Web: React
Este capítulo detalha a arquitetura e os padrões de implementação do frontend web da Plataforma de Relacionamento Digital com o Cidadão. O frontend web engloba dois contextos distintos: o Portal Web do Cidadão (jornadas…
Capítulo 34 — Banco de Dados e Persistência
Este capítulo detalha a estratégia de persistência e armazenamento de dados da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como dados transacionais, documentais, analíticos e de auditoria são armazena…