UniPass
Produto · App (Android + PWA)

App Estudante

Aplicativo (Expo/React Native) usado pelo estudante beneficiário do transporte universitário — Android nativo e PWA web, sem iOS (decisão deliberada). Voltagem plena da IDV Unipass: framboesa #e92c58 (identidade + ação) + roxo #831e91 (conquista/gamificação), mosaico multicolor em momentos de celebração, Nunito em display. Ver arquitetura do ecossistema.

Nota de método

Existe um UX-spec de 2026-07-04 que propôs um redesign completo (nav com carteirinha central, split framboesa/roxo, CTA sticky). Esta página descreve o que está de fato implementado hoje (verificado direto no código, não a proposta) — onde a proposta ainda não foi totalmente adotada, isso está sinalizado explicitamente como exceção.

1. Visão geral

O App Estudante é a superfície voltada ao beneficiário do transporte: confirmar presença semanal, acompanhar a rota e o ônibus, mostrar a carteirinha digital para o fiscal, e (opcionalmente) engajar com um sistema de gamificação. Estudantes não acessam o painel Gestão — este app é sua única superfície.

Plataformas

Android nativo (APK/AAB via EAS) + PWA web instalável. Sem build iOS.

Papel

Estudante — login por CPF, auto-provisionamento de conta no primeiro acesso

2. Conceitos específicos do domínio

Conceito — Confirmação Diária × Sinalização (não confundir)

Confirmação Diária é o fluxo formal e principal do app: o estudante confirma presença numa grade semanal, sujeita a cutoff, e alimenta o planejamento/manifesto da operação. Sinalização é um modelo mais simples e antigo ("vou usar o transporte nesta data") sem cutoff nem vínculo a manifesto — usado hoje apenas como evidência secundária para detectar walk-in nas estatísticas de disciplina. Os dois sistemas não têm nenhuma referência cruzada no código — são vestígios de dois momentos de design do produto que continuam ativos em paralelo. Para o tutorial de usuário final, documentar apenas Confirmação Diária como o fluxo real. (Existe uma tela de "sinalizar ponto" no código do app, mas ela não é exposta como uma das 5 tabs — não deveria ganhar destaque no tutorial.)

Conceito — walk-in vs. fora do manifesto

São duas coisas diferentes: walk-in é o embarque de um estudante sem Vínculo ativo com a rota (bloqueado por padrão, exige override do motorista com justificativa); fora do manifesto é uma confirmação criada manualmente por um operador depois que o manifesto da rota já fechou — nunca acontece pelo fluxo do próprio app estudante.

Vocabulário adicional: card_token (identificador único da carteirinha, formato SGTU-{16 hex}) · DeviceToken (token de push por usuário+plataforma) · GamificationState (XP/nível/streak/badges/missão semanal).

5 tabs, com a Carteirinha elevada ao centro (implementado, não só proposto — commit ddf84bf, 2026-07-04):

PosiçãoTabÍcone
1InícioHouse
2Minhas RotasRoute
3 (centro, destacada)CarteirinhaIdCard — 10% maior, realce de mosaico quando ativa
4ConfirmarCalendarCheck — badge de pendências
5PerfilUser

Notificações não é mais uma tab — foi migrada para um sino no header da tela Início, com badge de não-lidos.

4. Telas e fluxos

4.1 Início

Hub do dia: responde "o que é do meu transporte hoje?" e permite agir em 1 toque (confirmar pendência ou acompanhar viagem ativa). Ordem de leitura: header com sino de notificações → banner de penalidade (se ativo) → estado do dia → viagem ativa (se houver) → gamificação → ações rápidas.

Regra de negócio — proximidade da viagem ("modo GO")

Quando há viagem ativa, o banner mostra quantas paradas faltam até o ponto do estudante, cruzando os eventos de chegada/saída do veículo em cada ponto com a posição do estudante na rota. Se nenhum ponto foi concluído ainda, mostra a contagem total de paradas da rota.

Regra de negócio — cutoff que mira o dia seguinte

Quando a rota exige confirmação com antecedência de dias (cutoff_dias_antes > 0), o banner de cutoff na Início já mira a confirmação de amanhã, não hoje — porque a janela de hoje já fechou no dia anterior.

Exceção — sem empty state dedicado

Quando não há rota vinculada ou viagem ativa, a tela não mostra uma ilustração de estado vazio — apenas degrada o texto para "Sem rota hoje". Diferente de outras telas do app (Minhas Rotas, Notificações), que têm empty state ilustrado com o mosaico da marca.

4.2 Minhas Rotas

Tela de referência (leitura, não CRUD): cada card mostra nome da rota, badge "Ativa" (na primeira/rota corrente), direção/turno, dias da semana, e em destaque visual o ponto de embarque + horário — o dado que o estudante mais busca. Empty state com mosaico decorativo quando não há vínculo de rota.

4.3 Confirmar

O "batimento transacional" do app — grade semanal (segunda a sexta) com estados de célula: confirmado (verde), cancelado (X cinza), vazio (traço neutro, distinto de cancelado) e travado (após o cutoff, opacidade reduzida, preserva o que de fato aconteceu).

CTA "Confirmar todas"

Cor framboesa, sticky na base da tela (zona do polegar) — não mais no topo

Recompensa ao confirmar

Haptic + pulso de escala no botão + +10 XP flutuante; confirmação em lote da semana inteira dispara também um burst de mosaico

Caso de uso — confirmar a semana

Estudante abre a aba Confirmar no domingo à noite, vê a grade da próxima semana com o aviso de cutoff em destaque, toca "Confirmar todas" — recebe feedback tátil e visual imediato (XP, burst de mosaico, e no primeiro uso, o badge "Calourado"). Se um dia específico não for necessário, desmarca a célula individual antes do cutoff daquele dia.

Regra de negócio — cutoff é intransponível pelo estudante

Depois do cutoff, o estudante não pode mais criar, alterar ou cancelar a confirmação daquele dia/turno — só admin/gestor podem forçar uma alteração tardia (parâmetro force=true, nunca exposto ao app estudante).

4.4 Carteirinha

O artefato-herói do app — elevado à tab central, com o padrão visual mais alto de acabamento. Estrutura: zona superior em gradiente framboesa→roxo (foto, nome, curso) sobre um card branco de dados (matrícula, CPF completo, validade, situação, faixa co-branding com brasão/nome do município).

RNF — teatro anti-fraude (3 camadas)

L1 — relógio vivo: HH:MM:SS ao vivo, tanto no card quanto no modal de QR (screenshot congela o horário, denunciando a fraude). L2/L3 — faixa-checksum com shimmer: cores da faixa derivadas de um hash determinístico de cardToken + data (UTC) — muda por dia, reproduzível entre dispositivos, e praticamente sempre distinta entre estudantes diferentes no mesmo dia. QR sob demanda: só aparece dentro de um modal aberto por botão — nunca exposto por padrão na tela. O QR continua sendo a validação criptográfica real; as 3 camadas acima são dissuasão visual de primeiro olhar, não a segurança em si.

Regra de negócio — validade é um corte semestral fixo

A "validade" exibida não vem do calendário acadêmico real da instituição — é um corte hardcoded: 31 de julho (1º semestre) ou 31 de dezembro (2º semestre), conforme o mês da consulta. Não reflete o calendário específico de cada IES.

Regra de negócio — validação pública da carteirinha (minimização de PII)

Existe um endpoint público (sem autenticação) que valida uma carteirinha pelo seu card_token — é o que um fiscal/portaria consulta. O retorno é reduzido de propósito: nome, foto, curso, turno, IES, situação e validade — mas omite deliberadamente o CPF, para não expor PII numa URL pública. Contraste importante para o tutorial: na carteirinha exibida ao próprio estudante o CPF aparece completo (política de zero-masking), mas na superfície pública de validação o CPF nunca é retornado.

Reduced motion: shimmer e tick do relógio são omitidos (não acelerados) — substituídos por faixa estática e um timestamp fixo "Aberto em DD/MM HH:MM". Nenhuma capacidade de dissuasão anti-fraude é perdida, só a affordance de movimento.

4.5 Perfil

Header (avatar framboesa + dados) → atalho para Carteirinha → histórico de viagens → Conquistas (gamificação, sempre em roxo — nunca framboesa, separação de papel de cor deliberada e documentada no código) → dados pessoais/acadêmicos → preferências → logout.

4.6 Notificações

4 categorias: Todas / Não lidas / Alertas / Comunicados. Distinção visual por severidade real, não decorativa: crítico = vermelho + "Urgente"; aviso/categoria prioritária (penalidade, cutoff, cancelamento) = âmbar + "Obrigatório"; comunicado informativo comum = framboesa + "Comunicado" (única categoria que usa a cor de marca do estudante em vez de semântica de severidade).

4.7 Login e primeiro acesso

Tela de login permanece em azul institucional neutro (o papel do usuário ainda é desconhecido nesse ponto — decisão deliberada, não resíduo). O fluxo de onboarding gamificado (4 passos: avatar → perfil → tutorial de embarque → meta da primeira semana) é exibido apenas no primeiro acesso.

5. Gamificação

Atenção — motor client-authoritative, não server-authoritative

Todo o design de progressão (fórmula de XP por ação, catálogo de badges, contagem de streak) vive no app, não no backend. O servidor apenas persiste e sincroniza o estado: XP sobe por max(servidor, cliente) (nunca aceita um valor menor, protegendo contra regressão acidental), badges são a união dos dois lados, e streak/streak_frozen/missão semanal são sobrescritos pelo valor que o app enviar, sem validação de regra do servidor. Isso é relevante para o tutorial: "gamificação" não é uma feature de servidor com API própria de regras — é uma camada de engajamento cliente com um envelope de sincronização simples.

O que existe: XP acumulado, nível (derivado por faixas de XP: 0/500/2000/5000/10000), streak de confirmações, badges (lista livre de strings), missão semanal (progresso/meta). Cor: roxo puro em toda a seção de gamificação — separado deliberadamente da framboesa (identidade/ação).

Exceção — "escudo de sequência" sem regra de servidor

O campo streak_frozen existe no backend e é aceito na sincronização, mas não há nenhuma regra automática (nenhum job, nenhum signal) que decida quando congelar a sequência do estudante — a decisão, se existir, é inteiramente do cliente. Não documentar como uma política formal do produto até que uma regra de servidor exista.

6. Disciplina — perspectiva do estudante

O estudante enxerga automaticamente apenas suas próprias ocorrências e penalidades (escopo automático via API compartilhada — não é um endpoint separado). A única ação de escrita que o estudante executa é reconhecer uma penalidade (confirmar ciência) — nunca pode alterar o status dela.

Atenção — sem bloqueio funcional por penalidade ativa

Um estudante com suspensão temporária ou definitiva ativa continua conseguindo confirmar presença e embarcar normalmente — não há nenhum gate no backend que impeça isso hoje. Se o produto espera que uma suspensão bloqueie o uso do transporte, é um gap de implementação a resolver, não uma regra de negócio existente a documentar como se already funcionasse.

7. O que o estudante efetivamente recebe

Exceção importante — dois sistemas de comunicação não conectados

O módulo administrativo de Comunicação (templates/canais WhatsApp/SMS/e-mail configurados no Gestão) é hoje um stub — não entrega nada de fato. O que o estudante realmente recebe no inbox de notificações vem de um sistema totalmente separado (Alertas), disparado automaticamente por eventos operacionais de viagem — persistidos como alertas de tipo "operação" e também enviados por push: viagem confirmada/publicada, viagem iniciada/finalizada, motorista chegou ao ponto e aviso livre do motorista. As categorias de comunicação "institucionais" (atraso, cancelamento, mudança de ponto) existem como taxonomia administrativa, mas não chegam ao estudante por nenhum canal implementado hoje.

8. Autenticação e primeiro acesso

Regra de negócio — login unificado com auto-provisionamento

O endpoint de login por CPF tenta primeiro casar com um motorista; se não achar, tenta estudante. Se o estudante já tem conta mas nunca definiu senha própria, o sistema aceita CPF como senha e força a troca (self-heal). Se o estudante ainda não tem conta de usuário nenhuma, o sistema a cria automaticamente no primeiro login bem-sucedido com CPF. Qualquer falha (CPF inexistente, senha errada, conta bloqueada) retorna a mesma mensagem genérica — proteção deliberada contra enumeração de CPFs válidos.

Exceção — segundo fator obrigatório e fail-closed

Para definir a senha própria (fim do primeiro acesso), o estudante precisa informar a data de nascimento cadastrada. Se o cadastro não tiver data de nascimento, a troca self-service é recusada — o caminho vira reset assistido por um gestor, nunca um bypass silencioso.

9. Requisitos não-funcionais

9.1 Plataforma e offline

RNF — offline é parcial e assimétrico com o app motorista

Diferente do app motorista (fila offline completa para embarque/desembarque/ocorrências), o app estudante depende de rede para confirmar presença e demais ações — não há fila de escrita offline equivalente. A Carteirinha funciona sem rede, mas por um mecanismo indireto: os dados do estudante ficam em cache local por até 24h após a última busca bem-sucedida, não por um "modo offline" desenhado especificamente para o cartão.

9.2 LGPD e armazenamento local

Atenção — achado de segurança relevante para dispositivo compartilhado

No Android nativo, o cache de dados do estudante (nome, CPF, foto, card_token — dados de menores) é armazenado cifrado (AES-128, chave em keystore seguro do device). No PWA web, esse mesmo cache cai para um armazenamento não-cifrado — limitação da biblioteca de storage no navegador, não uma escolha deliberada. Isso é relevante especificamente para o cenário de tablet/quiosque compartilhado (ex.: portaria de instituição): em um navegador não higienizado entre sessões, dados do último estudante autenticado ficam recuperáveis por qualquer pessoa com acesso ao mesmo perfil do navegador, sem precisar de root/jailbreak. Token de autenticação em si não sofre disso — usa cookie HttpOnly via BFF, não localStorage.

9.3 Acessibilidade (WCAG 2.2 AA)

RNF — correção de contraste já aplicada, sistemática

Framboesa #e92c58 como texto é borderline (~4.0:1, abaixo do 4.5:1 exigido) — o app já usa consistentemente a variante escurecida accentDark para todo texto pequeno, reservando a cor crua para fills/backgrounds grandes. Cobertura ampla de accessibilityLabel/accessibilityRole e alvos de toque ≥48px nas telas principais.

9.4 Push notifications

Registro de DeviceToken por usuário+plataforma. O backend dispara push automaticamente por evento de viagem: a cada chegada do motorista a um ponto (e nas transições de viagem publicada/iniciada/finalizada, além de aviso do motorista), o servidor envia o push e persiste o alerta no inbox. É importante distinguir: isso é push por evento de chegada a ponto, não um cálculo contínuo de proximidade em tempo real ("faltam N paradas" calculado a cada segundo por GPS) — esse rastreamento contínuo não existe no backend (ver App Motorista). O app tem o formato de payload de proximidade preparado, mas o gatilho real hoje é discreto (por evento), não contínuo.

10. Limitações e exceções conhecidas

  • Resíduo de cor violeta hardcoded (fora do design system) em dois elementos do fluxo de onboarding gamificado — pendência pequena e concreta, não coberta pela revisão de paleta já feita
  • Sem regra de servidor para "congelar" a sequência de confirmações (gamificação é só o app decidindo)
  • Sem bloqueio de confirmação/embarque quando o estudante tem penalidade ativa
  • Armazenamento local não-cifrado no PWA web (ver RNF 9.2)
  • Módulo de Comunicação administrativo não entrega nada ao estudante (é stub) — o que chega de fato vem de Alertas
  • expo-image não é usado nas telas do estudante (convenção do projeto pede seu uso para imagens)
  • Validade da carteirinha usa corte semestral fixo (31/jul ou 31/dez), não o calendário acadêmico real da IES

11. Fora de escopo

  • Build e distribuição iOS
  • Fila de escrita offline (só o app motorista tem)
  • Entrega real de comunicados institucionais (atraso/cancelamento/mudança de ponto) — modelo existe, canal não
  • Regras de servidor para gamificação (XP por ação, streak-freeze automático, catálogo de badges)