# Integrar a avaliação +1 / −1 da TBBC pela API (iOS, Android, nativo, híbrido, bots, servidores)

Você é um agente de código. Sua tarefa é adicionar a avaliação +1 / −1 da TBBC a este projeto **pela API HTTP**. Você constrói a interface com os componentes do próprio projeto e envia os votos para https://tbbc.app. Não precisa de SDK, biblioteca nem backend próprio.

Use este guia quando o projeto **não roda num navegador**: app iOS (Swift, SwiftUI, UIKit), Android (Kotlin, Java, Jetpack Compose), React Native, Flutter, .NET MAUI, Electron sem páginas web, apps de desktop, bots (WhatsApp, Telegram, Slack), totens, e-mails pós-venda e servidores.

Se o projeto é um site ou app web, ou um app híbrido cuja tela é uma página web (Capacitor, Ionic, Cordova, WebView carregando HTML), use o outro guia, que é mais simples: https://tbbc.app/rating-widget/instrucoes-ia.md

## Antes de começar

1. Descubra a linguagem e o framework de interface do projeto.
2. Se a pessoa não disse em quais telas ou momentos quer a avaliação, pergunte. Bons lugares: fim de um pedido ou entrega, depois de usar um recurso pela primeira vez, fim de um atendimento no bot, tela de ajuda.
3. Escolha um nome curto para o app (`app`), em minúsculas e com hífens, por exemplo `meu-app-ios`.
4. Peça à pessoa a chave do site (`site`, começa com `k_`). **Ela é obrigatória:** sem uma chave válida a TBBC recusa os votos. A pessoa cria a chave de graça em https://tbbc.app/feedbacks, num time dela, e as respostas vão para o painel desse time. Se ela não tiver a chave agora, use `SUA_CHAVE` numa constante e avise que os votos só são aceitos depois da troca.

## A interface (igual à do widget web)

Construa com os componentes nativos do projeto, seguindo o estilo dele:

1. Uma linha com a pergunta **"Esta tela foi útil?"** e dois botões: **+1** (polegar para cima) e **−1** (polegar para baixo). Em inglês: "Was this helpful?".
2. Ao tocar num botão: desabilite os dois, marque o escolhido e **abra o formulário na hora**. Envie o voto (`POST /api/rating`) em segundo plano: a pessoa nunca espera a rede.
3. O formulário é curto (sheet, modal ou tela) e todo opcional:
   - título: **"Que bom! O que você gostou?"** (+1) ou **"O que podemos melhorar?"** (−1);
   - comentário: texto livre, até 1.000 caracteres, placeholder "Conte em poucas palavras (opcional)";
   - nome: até 80 caracteres, "Seu nome (opcional)";
   - contato: "WhatsApp ou e-mail (opcional)", com a nota "Só para responder você, se precisar.";
   - botões **Enviar** e **Agora não**.
4. Em **Enviar**: mostre **"Recebido, obrigado!"** na hora e, se algo foi preenchido, envie `POST /api/rating/{id}` em segundo plano (assim que o voto tiver `id`). Junto do obrigado, ofereça **"Quer dizer mais alguma coisa?"**, que reabre o formulário vazio: cada novo envio é acrescentado ao mesmo voto.
5. Em **Agora não**, feche o formulário, mostre "Obrigado pelo voto!" e deixe um link **"Deixar um comentário"** que reabre o formulário. Nunca deixe a pessoa sem um jeito de falar.
6. **Nunca mostre erro de rede.** Guarde o voto e cada comentário numa fila local (UserDefaults no iOS, SharedPreferences ou DataStore no Android, AsyncStorage no React Native, `shared_preferences` no Flutter, arquivo ou banco num servidor) e tente de novo até a API aceitar, inclusive depois que o app for reaberto. Descarte um item só quando a API responder `4xx` (dado errado ou chave inválida: repetir não adianta).

Num bot de conversa, a mesma ideia vira mensagens: "Este atendimento te ajudou? Responda 1 para sim ou 2 para não.", depois "Quer contar mais? É só escrever. Se não, responda 0." e, depois de cada comentário, "Recebido, obrigado! Se quiser dizer mais alguma coisa, é só escrever."

## A API

Endereço base: `https://tbbc.app`. Corpo em JSON, com o header `Content-Type: application/json`. Não há outro login: a chave diz para qual site e time vai a avaliação. A chave é pública (só permite **enviar**), então pode ficar no app.

### 1. `POST /api/rating`: registra o voto

| Campo | Tipo | O que é |
|---|---|---|
| `site` | texto, obrigatório | A chave, começa com `k_`. |
| `vote` | número, obrigatório | `1` ou `-1`. Número, não texto. |
| `section` | texto | Parte do app: `checkout`, `pedido/entregue`, `bot/atendimento`. Letras, números e `- _ . /`. Sem ele: `default`. |
| `app` | texto | Nome do app, até 80 caracteres. |
| `page` | texto | A tela, como um caminho: `/pedido/entregue`. Parâmetros depois de `?` são descartados. |
| `lang` | texto | Idioma da pessoa, como `pt-BR`. |

Não envie `origin` (é só para web) nem `website` (é uma armadilha para robôs: a chamada é recusada se ele vier preenchido).

Sucesso, `201`: `{ "ok": true, "id": "<uuid>" }`

| Resposta | O que fazer |
|---|---|
| `201` | Guarde o `id` (em memória, junto da tela ou da conversa) para enviar o comentário. Ele vale por 24 horas. |
| `400 site_key_is_required` | Erro de código: envie `site`. Não repita sem corrigir. |
| `400 unknown_site_key` | A chave não existe. Confira no painel. Nada foi gravado. |
| `400 vote_must_be_1_or_-1` | Corrija o valor de `vote`. |
| `500` ou falha de rede | Mantenha na fila local e tente de novo, esperando cada vez mais (1 s, 2 s, 4 s... até 1 minuto), e na hora quando a rede voltar. Se a resposta se perdeu depois de gravar, a nova tentativa pode duplicar o voto: aceitável. |

### 2. `POST /api/rating/{id}`: comentário, nome e contato

Pode chamar mais de uma vez, em até 24 horas depois do voto: cada comentário é acrescentado aos anteriores, e nome e contato são atualizados quando vierem. Envie pelo menos um destes campos:

| Campo | Tipo | O que é |
|---|---|---|
| `comment` | texto | Até 1.000 caracteres. |
| `name` | texto | Até 80 caracteres. |
| `contact` | texto | WhatsApp com DDD (10 a 15 dígitos, com ou sem `+55`, espaços e traços são aceitos) ou e-mail. |
| `nonce` | texto | Um id único por envio (um UUID, por exemplo), até 64 caracteres. Repetir a chamada com o mesmo `nonce` não duplica o comentário: use sempre, para poder tentar de novo sem medo. |

Sucesso, `200`: `{ "ok": true, "id": "<uuid>" }`

| Resposta | O que fazer |
|---|---|
| `200` | Tire o item da fila. Guarde o `id` enquanto a tela estiver aberta, para o "Quer dizer mais alguma coisa?". |
| `400 contact_is_invalid` | Mostre "Confira o WhatsApp ou o e-mail." e deixe a pessoa corrigir ou apagar o contato. |
| `400 comment_too_long` / `name_too_long` | Limite os campos na interface (1.000 e 80 caracteres). Somados, os comentários de um voto vão até 5.000. |
| `400 nothing_to_save` | Não chame quando tudo estiver vazio: só feche o formulário. |
| `404 not_found` | O `id` está errado. Use exatamente o que o voto devolveu. |
| `409 too_many_follow_ups` | Este voto já recebeu 20 envios. Descarte: repetir não adianta. |
| `409 too_late` | Passaram 24 horas. Se ainda quiser registrar, envie um voto novo e o comentário em seguida. |
| `500` ou falha de rede | Mantenha na fila e tente de novo, com o **mesmo** `nonce`: se já tiver gravado, a resposta é `200` e nada é duplicado. |

## Exemplos

### Swift (iOS, macOS)

```swift
import Foundation

enum TbbcRating {
    static let base = URL(string: "https://tbbc.app/api/rating")!
    static let site = "SUA_CHAVE"

    struct Reply: Decodable { let ok: Bool?; let id: String?; let error: String? }

    private static func post(_ url: URL, _ body: [String: Any]) async throws -> (Int, Reply) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try JSONSerialization.data(withJSONObject: body)
        let (data, response) = try await URLSession.shared.data(for: request)
        let status = (response as? HTTPURLResponse)?.statusCode ?? 0
        return (status, (try? JSONDecoder().decode(Reply.self, from: data)) ?? Reply(ok: nil, id: nil, error: nil))
    }

    /// Returns the rating id (keep it to send the comment).
    static func vote(_ value: Int, section: String) async throws -> String {
        let (status, reply) = try await post(base, ["site": site, "vote": value, "section": section, "app": "meu-app-ios"])
        guard status == 201, let id = reply.id else { throw URLError(.badServerResponse) }
        return id
    }

    /// Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
    /// Throws on errors; "contact_is_invalid" asks the person to fix it.
    static func comment(id: String, nonce: String = UUID().uuidString, comment: String?, name: String?, contact: String?) async throws -> Bool {
        var body: [String: Any] = ["comment": comment, "name": name, "contact": contact].compactMapValues { $0 }
        body["nonce"] = nonce
        let (status, reply) = try await post(base.appendingPathComponent(id), body)
        if status == 200 { return true }
        throw NSError(domain: "TbbcRating", code: status, userInfo: [NSLocalizedDescriptionKey: reply.error ?? "HTTP \(status)"])
    }
}
```

### Kotlin (Android)

Sem bibliotecas extras. Chame fora da thread principal (por exemplo, `withContext(Dispatchers.IO)`), e garanta a permissão `INTERNET` no manifest.

```kotlin
import org.json.JSONObject
import java.net.HttpURLConnection
import java.net.URL

object TbbcRating {
    private const val BASE = "https://tbbc.app/api/rating"
    private const val SITE = "SUA_CHAVE"

    private fun post(url: String, body: JSONObject): Pair<Int, JSONObject> {
        val conn = URL(url).openConnection() as HttpURLConnection
        conn.requestMethod = "POST"
        conn.setRequestProperty("Content-Type", "application/json")
        conn.connectTimeout = 10_000
        conn.readTimeout = 10_000
        conn.doOutput = true
        conn.outputStream.use { it.write(body.toString().toByteArray()) }
        val status = conn.responseCode
        val text = (if (status < 400) conn.inputStream else conn.errorStream)?.bufferedReader()?.readText().orEmpty()
        conn.disconnect()
        return status to (runCatching { JSONObject(text) }.getOrDefault(JSONObject()))
    }

    /** Returns the rating id (keep it to send the comment). */
    fun vote(value: Int, section: String): String {
        val (status, reply) = post(BASE, JSONObject().put("site", SITE).put("vote", value).put("section", section).put("app", "meu-app-android"))
        check(status == 201) { reply.optString("error", "HTTP $status") }
        return reply.getString("id")
    }

    /** Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
     *  Throws with the error code otherwise ("contact_is_invalid": ask to fix). */
    fun comment(id: String, comment: String?, name: String?, contact: String?, nonce: String = java.util.UUID.randomUUID().toString()): Boolean {
        val body = JSONObject().put("nonce", nonce)
        comment?.let { body.put("comment", it) }
        name?.let { body.put("name", it) }
        contact?.let { body.put("contact", it) }
        val (status, reply) = post("$BASE/$id", body)
        if (status == 200) return true
        error(reply.optString("error", "HTTP $status"))
    }
}
```

### React Native, Expo, Node, Deno, Bun

```js
const TBBC = 'https://tbbc.app/api/rating'
const SITE = 'SUA_CHAVE'

async function post(url, body) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  })
  const data = await res.json().catch(() => ({}))
  return { status: res.status, ...data }
}

export async function vote(value, section) {
  const r = await post(TBBC, { site: SITE, vote: value, section, app: 'meu-app' })
  if (r.status === 201) return r.id // keep it to send the comment
  throw new Error(r.error || `HTTP ${r.status}`)
}

// Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
export async function comment(id, { comment, name, contact, nonce = Date.now().toString(36) + Math.random().toString(36).slice(2) }) {
  const r = await post(`${TBBC}/${id}`, { comment, name, contact, nonce })
  if (r.status === 200) return true
  throw new Error(r.error || `HTTP ${r.status}`) // 'contact_is_invalid': ask to fix
}
```

### Flutter (Dart, pacote `http`)

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;

class TbbcRating {
  static const _base = 'https://tbbc.app/api/rating';
  static const _site = 'SUA_CHAVE';

  static Future<(int, Map<String, dynamic>)> _post(String url, Map<String, dynamic> body) async {
    final res = await http.post(Uri.parse(url),
        headers: {'Content-Type': 'application/json'}, body: jsonEncode(body));
    Map<String, dynamic> data = {};
    try { data = jsonDecode(res.body) as Map<String, dynamic>; } catch (_) {}
    return (res.statusCode, data);
  }

  /// Returns the rating id (keep it to send the comment).
  static Future<String> vote(int value, String section) async {
    final (status, data) = await _post(_base, {'site': _site, 'vote': value, 'section': section, 'app': 'meu-app'});
    if (status == 201) return data['id'] as String;
    throw Exception(data['error'] ?? 'HTTP $status');
  }

  /// Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
  static Future<bool> comment(String id, {String? comment, String? name, String? contact, String? nonce}) async {
    final body = {'comment': comment, 'name': name, 'contact': contact, 'nonce': nonce ?? DateTime.now().microsecondsSinceEpoch.toString()}
      ..removeWhere((_, v) => v == null);
    final (status, data) = await _post('$_base/$id', body);
    if (status == 200) return true;
    throw Exception(data['error'] ?? 'HTTP $status'); // 'contact_is_invalid': ask to fix
  }
}
```

### Python (servidor, bot, script)

```python
import uuid
import requests

TBBC = "https://tbbc.app/api/rating"
SITE = "SUA_CHAVE"

def vote(value, section):
    r = requests.post(TBBC, json={"site": SITE, "vote": value, "section": section, "app": "meu-bot"}, timeout=10)
    if r.status_code == 201:
        return r.json()["id"]  # keep it to send the comment
    r.raise_for_status()

def comment(rating_id, comment=None, name=None, contact=None, nonce=None):
    # Appends a comment to the vote. Retry with the SAME nonce: the server ignores repeats.
    body = {k: v for k, v in {"comment": comment, "name": name, "contact": contact}.items() if v}
    body["nonce"] = nonce or uuid.uuid4().hex
    r = requests.post(f"{TBBC}/{rating_id}", json=body, timeout=10)
    if r.status_code == 200:
        return True
    r.raise_for_status()  # "contact_is_invalid": ask to fix
```

## Como verificar

1. Toque em +1 numa tela com a avaliação. A chamada `POST https://tbbc.app/api/rating` deve responder `201` com um `id`, e o formulário deve abrir.
2. Escreva um comentário e envie. `POST https://tbbc.app/api/rating/<id>` deve responder `200`.
3. No painel https://tbbc.app/feedbacks, o voto aparece na seção escolhida, marcado como "via API".
4. Faça poucos testes: eles também são gravados.

## Não faça

- Não invente campos nem envie `origin` ou `website`.
- Não coloque dados pessoais em `section`, `app` ou `page` (nada de e-mail, telefone, número de pedido ou id de usuário). Nome e contato só vão em `name` e `contact`, quando a pessoa preencher.
- Não pergunte a avaliação toda hora: uma vez por tela ou momento já basta.
- Não repita sem esperar: aumente o intervalo entre tentativas (até 1 minuto) e use sempre o mesmo `nonce` ao repetir um comentário.

## Ao terminar

Diga à pessoa onde a avaliação foi colocada (arquivos e telas), qual `app` e quais `section` foram usados, e que as respostas aparecem no painel: https://tbbc.app/feedbacks (o mesmo login onde ela criou a chave). Se ficou `SUA_CHAVE` no código, lembre que os votos só são aceitos depois que ela trocar pela chave real.

Documentação completa: https://tbbc.app/rating-widget/#api
Índice dos guias para agentes: https://tbbc.app/rating-widget/llms.txt
