Ir para o conteúdo

O SDK iOS do Hooky

A conversa do Hooky, nativa no seu app para iPhone, iPad ou Mac (Catalyst): escrita em SwiftUI, sem WebView, com as cores do seu site. Os seus usuários escrevem, a sua equipe responde na mesma caixa de entrada do balão da web.

Uma tela de app no iPhone com o balão do Hooky no canto inferior direito. A conversa do Hooky no modo escuro no iPhone: mensagens, uma foto e o campo de texto.
O balão em um app SwiftUI, e a conversa no modo escuro.

Requisitos

  • iOS 15 ou mais recente, e Mac Catalyst 15 ou mais recente.
  • Xcode 16 ou mais recente (o pacote é escrito em Swift 6). O seu app pode continuar em Swift 5.
  • Nenhuma dependência de terceiros: URLSession, WebSocket, PhotosUI e o Keychain, nada mais.
  • Uma conta Hooky e um site: a chave pública dele fica em Sites › Instalar, a mesma do data-site-key do balão na web.

Instalação

Só o Swift Package Manager é suportado. No Xcode: File › Add Package Dependencies…, cole o endereço do pacote, mantenha a regra Up to Next Major Version a partir da 1.0.0 e adicione o produto Hooky ao target do seu app.

https://github.com/Defdjamel/hooky-ios

Ou em um Package.swift:

dependencies: [
    .package(url: "https://github.com/Defdjamel/hooky-ios", from: "1.0.0"),
],
targets: [
    .target(name: "MeuApp", dependencies: [
        .product(name: "Hooky", package: "hooky-ios"),
    ]),
]

O pacote traz o próprio manifesto de privacidade (PrivacyInfo.xcprivacy), que o Xcode adiciona ao relatório de privacidade do seu app. O código-fonte está sob licença MIT: github.com/Defdjamel/hooky-ios.

Comece em 3 linhas

import Hooky

// 1. Na inicialização (App.init ou application(_:didFinishLaunchingWithOptions:))
Hooky.configure(siteKey: "SUA_CHAVE_PUBLICA")

// 2. O balão flutuante, em qualquer view SwiftUI
ContentView().hookyBubble()

// 3. Ou a conversa a partir do seu próprio botão
Hooky.open()

configure carrega o nome, a cor e a mensagem de boas-vindas do site, retoma a conversa salva no aparelho e abre a conexão ao vivo. Chame uma vez, antes de qualquer outra chamada.

@main
struct MeuApp: App {
    init() { Hooky.configure(siteKey: "SUA_CHAVE_PUBLICA") }

    var body: some Scene {
        WindowGroup { ContentView().hookyBubble() }
    }
}
ParâmetroFunção
siteKeyObrigatório. A chave pública do site.
languageOpcional. Força o idioma da conversa: "fr", "en", "es", "pt", "it" ou "de". Sem ele, vale o do aparelho (veja Idioma, modo escuro, acessibilidade).
translationOpcional, true por padrão. false remove a tradução das mensagens da equipe (veja Tradução das mensagens).
apiURLOpcional, para testar com o seu próprio servidor. Por padrão, https://api.heyhooky.com.

O balão

Um botão flutuante com a cor do seu site, o logo do Hooky e um contador vermelho para as respostas ainda não lidas. Um toque abre a conversa.

SwiftUI

ContentView()
    .hookyBubble()                         // no canto inferior, do lado definido para o site

ContentView()
    .hookyBubble(alignment: .bottomLeading, padding: 24, hidden: estaNoPagamento)

HookyBubble(size: 56)                      // só o botão, para você posicionar
HookyBubble { mostrarMinhaFolha = true }   // com a sua própria ação
  • alignment força o canto (senão, o lado esquerdo ou direito definido em Sites); padding, a distância das bordas (60 pontos de lado, 20 de margem por padrão); hidden o esconde em uma tela onde ele atrapalharia.
  • O balão some enquanto a conversa está aberta.

UIKit

// No canto inferior, respeitando a área segura (leading: true para a esquerda)
Hooky.showBubble(in: view)

// Ou uma UIView para você posicionar
let balao = HookyBubbleView(size: 60)

Abrir e fechar a conversa

Hooky.open()               // folha de altura total, por cima do controlador mais alto
Hooky.open(from: self)     // UIKit: apresentada por este controlador
Hooky.close()              // fecha a conversa

No iPhone, a conversa abre em uma folha de altura total; no iPad e no Mac, em uma folha centralizada. O seu usuário fecha com o botão × ou deslizando para baixo. Para apresentá-la você mesmo (em um .sheet, em uma navegação…), use a view:

.sheet(isPresented: $chat) { HookyConversationView() }

Mensagens não lidas

O balão já mostra o próprio contador. Para os seus botões (uma aba Ajuda, um ícone de menu), o número de respostas da equipe ainda não vistas pode ser acompanhado ao vivo. Ele volta a 0 quando a conversa é aberta.

// SwiftUI: Hooky.shared é um ObservableObject
@ObservedObject var hooky = Hooky.shared

AjudaView()
    .tabItem { Label("Ajuda", systemImage: "questionmark.bubble") }
    .badge(hooky.unreadCount)

// UIKit: um AsyncStream (o valor atual, depois cada mudança)
Task {
    for await n in Hooky.unreadCountUpdates() {
        tabBarItem.badgeValue = n > 0 ? "\(n)" : nil
    }
}

// Ou um closure, chamado na thread principal
Hooky.shared.onUnreadCountChange = { n in print(n) }

Hooky.shared também publica siteName, isOpen e isConfigured, para a sua interface. As respostas chegam ao vivo enquanto o app está em primeiro plano; quando ele volta ao primeiro plano, o SDK recupera o que perdeu. A versão 1.0.0 não envia notificações push aos seus usuários.

Identificar o usuário

O seu app sabe quem está logado: conte isso ao Hooky. Nos detalhes da conversa, a sua equipe vê o nome, o e-mail, o seu próprio identificador do usuário, o aparelho e os dados que você anexa. Com um e-mail, a conversa não pede mais nome nem e-mail.

// Depois do login
Hooky.identify(
    name: "Lila",
    email: "lila@example.com",
    userId: "u_1042",                                 // o seu próprio identificador
    attributes: ["plano": "Pro", "carrinho": 49.9, "beta": true]
)

// Mais tarde: mesclados com os anteriores
Hooky.setAttributes(["carrinho": 12, "ultima_tela": "Pagamento"])
Hooky.setAttributes(["beta": nil])                    // nil apaga uma chave

// No logout
Hooky.reset()

Os valores são HookyValue: um texto, um número ou um booleano. Os literais são escritos como estão; para uma variável, indique o tipo: ["plano": .string(user.plano), "carrinho": .number(total), "beta": .bool(beta)].

Como funciona

  • Ainda sem conversa: tudo fica na memória e é enviado com a primeira mensagem. Depois, cada mudança é enviada na hora, agrupada (um único envio para tudo o que muda no mesmo segundo). Sem conexão, ela vai junto com a próxima mensagem ou quando o app voltar.
  • name e email substituem os da conversa (nil os apaga no aparelho); userId: nil o deixa como está.
  • O userId é guardado com o token da conversa. Um userId diferente é outra conta no mesmo aparelho: a conversa é esquecida, como com reset(). O novo usuário não vê a conversa do anterior.
  • Hooky.reset() fecha a conversa e esquece o histórico, a identidade, os dados e o rascunho. Chame no logout.
  • Nenhuma dessas chamadas gera erro. Chame configure antes: senão, identify e setAttributes não fazem nada (e param em uma asserção no modo Debug).

Regras dos dados

CampoRegra
Dados livresSem aninhamento: 50 chaves no máximo, 8 KB no total. As chaves novas substituem as antigas, as outras ficam.
ChaveDe 1 a 64 caracteres: letras, algarismos, espaços, _, . e -. Senão, é ignorada.
ValorUm texto (cortado em 500 caracteres), um número ou um booleano. nil apaga a chave.
userId200 caracteres no máximo.
name, emailO nome: 100 caracteres no máximo. O e-mail: um endereço válido, senão é ignorado.

O Hooky limpa o que passar do limite, sem nunca recusar a mensagem do seu usuário.

Esses dados são declarativos: a chave do site é pública, então qualquer pessoa pode usá-la para enviar o nome, o e-mail ou o userId que quiser. Eles ajudam a sua equipe a situar a pessoa, não provam quem ela é. Nunca coloque um segredo neles (senha, token, número de cartão) e nunca use esses dados para liberar um acesso ou agir sobre uma conta: sempre confira do seu lado.

Estatísticas

Conte o que acontece no seu app, como com Hooky.track na web: os cards aparecem em Estatísticas, junto com os do site. Chame logo depois que a ação der certo.

Hooky.track("cadastro")
Hooky.track("pedido", value: 49.9, properties: ["plano": "pro", "assentos": 3, "teste": false])
  • value: um valor em dinheiro ou uma quantidade, somado no card. As propriedades não têm aninhamento (textos, números, booleanos).
  • Pode ser chamado de qualquer thread, e até antes de configure: os eventos ficam esperando (100 no máximo). Nunca gera erro; sem conexão, o evento se perde.
  • As mesmas regras da web (nome, 20 propriedades no máximo, limite): veja Estatísticas na página.

Nunca envie dados pessoais (e-mail, nome, telefone) em um evento. Conte ações, não pessoas.

O que a conversa faz

  • A primeira mensagem, com nome e e-mail opcionais (ou os do identify), depois uma conversa como a do balão na web: nomes da equipe, “… está digitando”, “Lida”, links clicáveis, emojis grandes, mensagens anteriores sob demanda.
  • Até 4 anexos por mensagem, 10 MB cada: fotos (JPEG, PNG, HEIC, WebP, GIF) ou PDF. As fotos da galeria passam pelo seletor do sistema (nenhuma permissão a pedir) e são reduzidas a 2048 px antes do envio; os arquivos vêm do app Arquivos.
  • Para oferecer a câmera, adicione NSCameraUsageDescription ao Info.plist do seu app. Sem ela, a opção simplesmente fica escondida.
  • As fotos recebidas abrem em tela cheia (zoom, deslizar, compartilhar), os arquivos no Visualização Rápida.
  • O rascunho sobrevive ao fechamento da conversa e à reabertura do app. O teclado nunca esconde o campo de texto.
  • Erros claros, no idioma do usuário: sem conexão, arquivo grande demais, arquivos demais, mensagens demais de uma vez.

Tradução das mensagens

O seu usuário e a sua equipe escrevem cada um no seu idioma, como com o balão na web (veja Tradução das mensagens).

  • Abaixo de uma mensagem da equipe escrita em outro idioma que o da conversa (o do aparelho, ou o definido por language), aparece um link discreto Traduzir. Traduzida, a mensagem aparece no idioma do usuário, com “Traduzido do inglês · Ver o original” para voltar ao texto original.
  • O botão de tradução do cabeçalho oferece Traduzir sempre: as mensagens da equipe passam a chegar já traduzidas. A escolha fica salva no aparelho, para cada chave de site.
  • Uma tradução que falha mostra um erro curto com Tentar de novo. As mensagens do usuário nunca são traduzidas do lado dele; a sua equipe tem o próprio botão Traduzir, no painel e no app Hooky.
  • Só o texto é traduzido, não os anexos. A tradução vem ativada por padrão; para removê-la do seu app: Hooky.configure(siteKey: "SUA_CHAVE_PUBLICA", translation: false).

O que o SDK envia

O que o balão da web envia, e nada mais. Sem identificador de publicidade, sem rastreamento entre apps, sem biblioteca de terceiros. Para preencher a seção Privacidade do app da sua página na App Store:

DadoQuandoPrivacidade do app (App Store)
Mensagens, fotos e PDFsQuando o usuário escreveConteúdo do usuário: Atendimento ao cliente, Fotos ou vídeos
Nome e e-mailSe ele os informar, ou se você os passar para identifyInformações de contato: Nome, Endereço de e-mail
O seu userIdSe você o passar para identifyIdentificadores: ID do usuário
Dados livresO que você passa para identify e setAttributesDepende do que você coloca (um carrinho: Compras › Histórico de compras…)
Eventos trackQuando você os envia, sem identificador de pessoaDados de uso: Interação com o produto (Análise), não vinculados ao usuário
Aparelho, idioma, fuso horário, appCom a conversaInformações técnicas para a sua equipe (veja abaixo)
  • Todos esses dados servem à Funcionalidade do app (o atendimento ao cliente), e as estatísticas à sua Análise. Nenhum deles serve ao rastreamento no sentido da Apple: nada de pedido do App Tracking Transparency.
  • O aparelho é legível: “iPhone 15 Pro · iOS 18.2”, “iPad Air 11-inch (M2) · iPadOS 18.1”, “Mac · macOS 15.1”. Cada requisição leva X-Hooky-Client: hooky-ios/1.0.0 e X-Hooky-App: <bundle id> <version> (<build>): a sua equipe vê que a conversa vem de “iOS · com.example.app 2.3 (45)”.
  • No aparelho: o token da conversa (a única chave do usuário) e o userId no Keychain (só neste aparelho, depois do primeiro desbloqueio), por chave de site; o rascunho, a última mensagem vista e a escolha “Traduzir sempre” em UserDefaults. Uma tradução só envia o identificador da mensagem e o idioma da conversa.
  • O Hooky trata esses dados em seu nome (como operador, nos termos do RGPD e da LGPD): veja a política de privacidade. A sua página na loja continua sendo responsabilidade sua: declare o que o seu app realmente envia.

Idioma, modo escuro, acessibilidade

Idioma
A interface existe em francês, inglês, espanhol, português, italiano e alemão. Ela segue os idiomas preferidos do aparelho, com inglês por padrão; Hooky.configure(siteKey:language:) força um deles. O idioma vai com a primeira mensagem, para que a sua equipe responda no idioma certo e na hora certa (com o fuso horário).
Modo escuro
A conversa segue o modo claro ou escuro do aparelho, com a cor do seu site. Nada a configurar.
Acessibilidade
Cada controle tem o seu rótulo do VoiceOver (o balão: “Abrir o chat com …”, e o número de não lidas); Dynamic Type em todo lugar, e o teclado nunca esconde o campo de texto.

Solução de problemas

O meu site cadastrou os endereços dele: o app é recusado?
Não. Um app não tem endereço: ele se apresenta pelo cabeçalho X-Hooky-Client e funciona mesmo quando o site cadastrou os domínios dele. Não adicione nada em Sites.
O balão fica com as cores padrão, ou nada chega
Confira a chave: é a chave pública do site (não a chave secreta hks_…, nem uma chave de API hka_…). Com uma chave desconhecida, o console do Xcode mostra “Hooky: the site couldn’t be loaded (HTTP 404…)”.
Sem conexão
A mensagem mostra “Sem conexão” e fica no campo; identify e setAttributes são enviados mais tarde. A aparência do site é recarregada ao abrir a conversa.
A opção Câmera não aparece
Adicione NSCameraUsageDescription ao seu Info.plist.
Outro usuário vê a conversa antiga
Chame Hooky.reset() no logout e passe um userId para identify: um identificador diferente começa uma conversa do zero.

Histórico de versões

1.0.0
Primeira versão: configure, o balão SwiftUI e UIKit, open e close, identify (com userId e dados livres), setAttributes, track, reset, as não lidas ao vivo, fotos e PDFs, a tradução das mensagens da equipe, seis idiomas, modo escuro, VoiceOver e Dynamic Type.