Relacionamento Digitalcom o Cidadão
Parte V — Tecnologia
Parte V — TecnologiaCapítulo 33

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:

DadoArmazenamentoLifetimeCriptografia
Token de sessãoKeychain/EncryptedSharedPreferencesAté logout ou expiraçãoNativa do SO
Refresh token (se adotado)Keychain/EncryptedSharedPreferencesConforme políticaNativa do SO
Preferências de usuárioAsyncStorage comumIndefinidoSem requisito
Cache de catálogo (conteúdo público)AsyncStorage comumTTL 24hNão exigido
Dados pessoais sensíveisNunca 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>
  );
};

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>
  );
};
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

RiscoConsequênciaMitigação
Token armazenado em AsyncStorage comumRoubo por aplicativo maliciosoArmazenar em Keychain/EncryptedSharedPreferences
Secret embutido no bundle (chave de API)Exposição por descompilaçãoNenhum secret no bundle; apenas URLs públicas
WebView executando JavaScript não confiávelXSS, acesso a APIs nativasBrowser in-app do SO para fluxos críticos; WebView apenas com origem confiável
Falta de validação em deep linkRedirecionamento a tela interna com parâmetros maliciososValidação de URL e parâmetros antes de rotear
Screenshot de tela sensívelExposição de token ou dado pessoalFLAG_SECURE no Android, blur em background no iOS
Biometria como único fatorAcesso por impressão digital não autorizadaToken de sessão sempre re-validado no backend
Versão antiga do app sem correçõesExploração de vulnerabilidadeVersão mínima suportada + 426 Upgrade Required
Push token inativo acumulandoFalha silenciosa de entregaRemoção de token quando APNS/FCM retorna erro permanente
Conflito de merge em fluxos assíncronosEstado inconsistenteImutabilidade + comparação de versão otimista
Não detectar perda de conexãoExibição de dados obsoletos como atuaisIndicador 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

ADRTema
ADR-313React Native + TypeScript como stack do aplicativo mobile
ADR-314Redux Toolkit para estado compartilhado
ADR-315NativeWind para estilização baseada em Tailwind
ADR-316React Navigation com deep linking configurado
ADR-317Keychain/EncryptedSharedPreferences para tokens
ADR-318PKCE com state para autenticação GOV.BR
ADR-319Biometria local como conveniência (não fator de autenticação)
ADR-320Notificações push via FCM (Android) e APNS (iOS)
ADR-321Scanner de documento com correção de perspectiva
ADR-322Certificate pinning para APIs críticas
ADR-323Compatibilidade 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

CampoValor
DocumentoDocumento Mestre — Plataforma de Relacionamento Digital com o Cidadão
Capítulo33 — Aplicativo Mobile: React Native
Versão1.0
SituaçãoConcluído
Última atualização16/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.

Nesta página

33.1 Objetivo do Capítulo33.2 Contexto e Posicionamento33.3 Stack Tecnológica33.3.1 Dependências Base33.3.2 Estilização e Design System33.3.3 Navegação33.3.4 Gerenciamento de Estado33.3.5 HTTP e Integração Backend33.3.6 Autenticação e Segurança33.3.7 Câmera e Scanner33.3.8 Notificações Push33.3.9 Autenticação Biométrica33.3.10 Observabilidade33.4 Arquitetura de Componentes33.4.1 Estrutura de Diretórios33.4.2 Princípios de Componentes33.4.3 Exemplo de Componente33.5 Navegação33.5.1 Estrutura de Navegação33.5.2 Deep Linking33.6 Gerenciamento de Estado33.6.1 Estrutura Redux com Slices33.6.2 Persistência de Estado33.6.3 Selectors33.7 Autenticação33.7.1 Fluxo de Login Local33.7.2 Autenticação com GOV.BR (PKCE)33.7.3 Biometria Local33.8 Camada de Persistência Local33.8.1 Armazenamento Seguro33.8.2 Sincronização de Cache33.9 Notificações Push33.9.1 Integração com APNS e FCM33.9.2 Tratamento de Notificações33.10 Captura de Documentos via Câmera33.10.1 Scanner com Correção de Perspectiva33.11 Navegação e Deep Links33.11.1 Configuração de Deep Links33.11.2 Validação de Deep Links33.12 Performance e Otimizações33.12.1 Renderização Eficiente33.12.2 Lazy Loading de Imagens33.12.3 Monitoramento de Performance33.13 Testes no Aplicativo Mobile33.13.1 Testes Unitários e de Integração33.13.2 Testes E2E33.14 Segurança Avançada33.14.1 Certificate Pinning33.14.2 Proteção de Telas Sensíveis33.15 Observabilidade e Crash Reporting33.15.1 Integração com Crash Reporting33.15.2 Métricas de Experiência33.16 Acessibilidade (VoiceOver / TalkBack)33.16.1 Configuração de Acessibilidade33.16.2 Dynamic Type e Font Scaling33.17 Build e Distribuição33.17.1 Configuração de Build33.17.2 Compatibilidade de Versão33.18 Riscos e Mitigações33.19 Benefícios do Aplicativo Mobile em React Native33.20 Decisões Arquiteturais33.21 Considerações Finais33.22 Controle de Versão33.23 Rastreabilidade PRODEMGE33.8 Armazenamento Seguro33.8.1 Estratégia de Armazenamento