FINANÇAS COMPORTAMENTAIS · PSICOLOGIA ECONÔMICA

Pitch: TianTian, flashcards de chinês (HSK) com áudio offline num PWA: Service Worker, Range requests do Safari e outras pegadinhas

Fizemos o TianTian (天天, “todo dia”), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência…

Fizemos o TianTian (天天, “todo dia”), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência em chinês. Cada palavra tem foto, caractere, pinyin e áudio nativo, e tudo se responde só com toques. Ele roda como PWA: abre no navegador do celular e pode ser instalado na tela inicial.

Este post é um pitch, mas a ideia é contar o que deu trabalho. Áudio offline em PWA, principalmente no iPhone, tem umas pegadinhas que quase não aparecem em tutorial.

Por que PWA, e não loja de apps

O público inicial é brasileiro estudando chinês. Pesou na decisão:

  • Celular com pouco espaço. Muita gente não instala “mais um app” de 150 MB. O PWA baixa o shell (HTML, CSS, JS e o JSON das palavras grátis) e o resto vem sob demanda.
  • Um link resolve. Dá para mandar o link num fórum ou grupo de estudo e a pessoa já está usando em 5 segundos, sem cadastro no plano grátis.
  • Atualização na hora. Corrigiu um tom errado num áudio? Sobe o arquivo e pronto, sem esperar revisão de loja.
  • Pagamento direto. Sem loja no meio, a assinatura vai pelo Stripe Checkout. O lado ruim: por enquanto só cartão (sem PIX), o que no Brasil é uma limitação real que ainda estamos avaliando.

O preço: PWA no iOS ainda tem limitações (armazenamento separado do Safari, sem notificação confiável em versões antigas), e “instalar” não é óbvio para quem nunca fez. Esse é um ponto que ainda queremos melhorar no próprio app (hoje ele não tem um tutorial de instalação).

A estratégia de cache: três caches, três regras

O Service Worker separa três tipos de conteúdo:

const SHELL = `tiantian-shell-${VERSION}`; // HTML/CSS/JS + JSON grátis: precache no installconst MEDIA = "tt-media-v2"; // imagens e áudios grátis: cache preguiçosoconst PREMIUM = "tt-premium"; // /premium/*: NUNCA precache, só em runtime
  • Shell e dados grátis: precache no install + stale-while-revalidate. O app abre instantâneo, mesmo offline, e se atualiza em segundo plano. Para forçar atualização nas cópias instaladas, basta mudar o VERSION, porque o activate apaga os caches com nome antigo.
  • Áudio grátis: cache-first, salvo no primeiro play. Quem ouviu uma palavra uma vez já tem ela offline.
  • Imagens: stale-while-revalidate, para que uma foto trocada no servidor apareça sem quebrar o offline.
  • Conteúdo pago: só entra no cache se a resposta for um 200 completo, sem redirect. Se o servidor responder 401/403 (sessão expirada ou assinatura cancelada), o cache premium inteiro é apagado. Não existe “desbloqueio” no cliente: o app só espelha o que o servidor diz.
const cacheable = res => !!res && res.status === 200 && res.type === "basic" && !res.redirected;

Pegadinha 1: o Safari pede áudio com Range, e o Cache API não guarda 206

No iPhone, o elemento <audio> não pede o arquivo inteiro. Ele manda Range: bytes=0-1 e depois vai pedindo pedaços. A resposta é 206 Partial Content, e o Cache API recusa respostas parciais. Resultado: o áudio “funcionava” online e sumia offline.

A solução tem duas partes:

  1. No SW, buscar sempre sem o header Range, para receber o 200 completo e poder guardar.
  2. Quando o navegador pedir um Range, montar o 206 a partir do arquivo inteiro que está no cache:
async function rangeResponse(req, res){ const buf = await res.arrayBuffer(), size = buf.byteLength; const m = /bytes=(d*)-(d*)/.exec(req.headers.get("range") || ""); let start = m && m[1] ? +m[1] : 0, end = m && m[2] ? +m[2] : size - 1; if (m && !m[1] && m[2]) { start = size - +m[2]; end = size - 1; } // sufixo: "bytes=-500" end = Math.min(end, size - 1); return new Response(buf.slice(start, end + 1), { status: 206, headers: { "Content-Type": res.headers.get("Content-Type") || "audio/mpeg", "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": String(end - start + 1), "Accept-Ranges": "bytes" } });}

Os MP3 das palavras são pequenos, então carregar o arquivo inteiro em memória para fatiar não pesa.

Pegadinha 2: o áudio precisa tocar dentro do gesto do usuário

O iOS bloqueia play() fora de um toque. Num flashcard, o natural é tocar o áudio depois de conferir a resposta, e foi aí que travou. Duas medidas resolveram:

  • Chamar play() dentro do handler do toque, antes de qualquer setTimeout ou animação.
  • Pré-carregar os áudios da rodada como Blob URLs (fetch → blob() → URL.createObjectURL). Na hora do toque, o src já está em memória e toca instantâneo, inclusive offline.

Se o arquivo falhar mesmo assim, há um fallback para a voz do sistema (speechSynthesis com lang = "zh-CN" e rate = .8). Não é tão boa quanto o áudio nativo, mas é melhor que silêncio.

Repetição espaçada simples de propósito

Nada de SM-2 completo. São 6 níveis com intervalos fixos:

const INTERVALS = [0, 10*60e3, 1*DAY, 3*DAY, 7*DAY, 16*DAY, 35*DAY]; // 10 min ... ~5 semanas
  • Acertou: sobe um nível. Errou: desce um nível, e a palavra volta 3 cartões depois na mesma rodada.
  • Para quem está no HSK 1, isso basta, e o comportamento fica previsível (“vi ontem, volta em 3 dias”).
  • O progresso fica no localStorage do aparelho, sem conta.
  • O ponto fraco é conhecido: trocar de celular perde o progresso do plano grátis.

Pegadinha 3: medir de onde vêm os usuários num PWA

Duas coisas que quase nos pegaram:

  • O start_url do manifest não pode ter UTM. Se tiver, toda abertura do app instalado conta como visita da campanha.
  • No iPhone, o PWA instalado tem armazenamento separado do Safari. O que ficou gravado no Safari (inclusive a origem da visita) não existe dentro do app instalado. Então a atribuição precisa ir ao servidor logo na primeira visita, e não ficar só no cliente.

O que existe hoje (sendo honestos)

  • HSK 1 completo, com a lista clássica de 150 palavras (HSK 2.0).
  • O HSK 3.0 tem prova oficial mundial marcada para 13/12/2026 e lista maior (300 palavras no nível 1). Ainda não cobrimos a lista nova inteira. O HSK 2 em diante está em construção.
  • O app também tem 18 lições curtas de gramática do HSK 1, montador de frases (tocar nas palavras na ordem certa), exercícios de escuta e textos curtos com toque para ouvir.
  • Plano grátis: 20 palavras por nível, as primeiras lições e alguns textos, com anúncio, sem cadastro.
  • Premium: desbloqueia tudo e tira o anúncio. Tem também o “Baixar tudo” para estudar 100% offline (no metrô, no avião). R$14,90/mês ou R$79,90/ano, e um plano de fundador de R$59,90/ano para os 100 primeiros.
  • Não é dicionário. O Pleco continua sendo o melhor para isso.

Perguntas para a comunidade

  1. Alguém já resolveu de forma mais elegante o Range + Cache API no Safari? (Pensamos em guardar em IndexedDB, mas o Cache API ficou mais simples.)
  2. Para PWA no Brasil: vocês já mediram quanta gente instala de fato na tela inicial ou só usa no navegador?
  3. Vale a pena oferecer PIX numa assinatura recorrente, ou o cartão resolve para esse público?

Quem quiser testar (roda no navegador do celular, sem instalar nada):
https://tiantianhsk.com/?utm_source=tabnews&utm_medium=post&utm_campaign=launch&utm_content=pitch

Respondemos a todos os comentários por aqui.

Perguntas frequentes

Respostas institucionais sobre esta cobertura, fontes e limites editoriais.

Sobre o que trata “Pitch: TianTian, flashcards de chinês (HSK) com áudio offline num…”?
Fizemos o TianTian (天天, "todo dia"), um app de flashcards para o vocabulário do HSK, a prova oficial de proficiência em chinês. Cada palavra tem foto, caractere, pinyin e áudio nativo, e tudo se responde só…
Por que Psicologia econômica importa agora?
O tema impacta decisões e debates cotidianos ligados a Psicologia econômica. A redação destaca fatos, datas e implicações práticas sem substituir orientação profissional personalizada.
Quem edita o conteúdo do Estrato Mente?
A cobertura é produzida e revisada pela equipe editorial do Estrato Mente, com editor-chefe responsável pela linha editorial. Conheça a redação em https://mente.estrato.cc/equipe/.
Quais fontes o Estrato prioriza?
Priorizamos fontes primárias (órgãos oficiais, balanços, estudos revisados, documentos públicos) e cruzamos informações antes da publicação, conforme a política editorial.
Como solicitar correção de uma matéria?
Envie o pedido pela página de Correções (https://mente.estrato.cc/correcoes/) ou Contato (https://mente.estrato.cc/contato/). Erros materiais são corrigidos com registro da atualização.
O conteúdo constitui aconselhamento profissional?
Não. As matérias têm caráter informativo e jornalístico. Decisões financeiras, de saúde, jurídicas ou educacionais devem considerar profissionais habilitados e a sua situação particular.
Onde acompanhar a política editorial do Estrato Mente?
Metodologia, ética e transparência estão em https://mente.estrato.cc/metodologia/, https://mente.estrato.cc/etica-editorial/ e https://mente.estrato.cc/politica-editorial/.
Como o Estrato trata temas sensíveis (YMYL)?
Temas que afetam dinheiro, saúde e bem-estar recebem revisão editorial reforçada, disclaimer visível e links para páginas institucionais de metodologia e correções. Portal: https://mente.estrato.cc/
WhatsApp X LinkedIn
Compartilhar: LinkedIn
Nina Barros

Nina Barros

Nina Barros integra a equipe editorial do Estrato Mente, vertical da rede Estrato, na função de Repórter de Finanças comportamentais. O escopo permanente de cobertura inclui vieses e decisões, com atenção ao leitor brasileiro que busca contexto factual, datas oficiais e implicações práticas. Na produção diária, prioriza fontes primárias (órgãos públicos, balanços, papers e documentos oficiais), cruza pelo menos duas referências independentes quando o tema é contestado e evita manchetes que prometam certeza onde há incerteza. Critérios de qualidade: lead com o fato principal, atribuição clara de autoria da equipe Estrato, atualização com selo quando há mudança material e linguagem acessível sem simplificar demais o risco ou o contexto. Trabalha sob revisão da mesa editorial e do editor-chefe do portal. Matérias YMYL recebem segunda leitura antes da publicação e podem ser atualizadas quando surgem novos dados oficiais. Limites: este perfil descreve o papel editorial na marca Estrato. O conteúdo publicado é informativo e não constitui aconselhamento médico, financeiro, jurídico ou profissional personalizado. Transparência ao leitor: metodologia em /metodologia/, ética em /etica-editorial/, correções em /correcoes/, equipe em /equipe/ e canal em /contato/ ([email protected]). Na página-mãe da rede, a bio ampliada e o arquivo de matérias ficam em estrato.cc/blog/, com link para o arquivo do autor em cada portal.

Deixe um comentário