Le SDK iOS de Hooky
La conversation Hooky, native dans ton app iPhone, iPad ou Mac (Catalyst) : écrite en SwiftUI, sans WebView, aux couleurs de ton site. Tes utilisateurs écrivent, ton équipe répond depuis la même boîte que pour la bulle web.
Prérequis
- iOS 15 ou plus, et Mac Catalyst 15 ou plus.
- Xcode 16 ou plus (le paquet est écrit en Swift 6). Ton app peut rester en Swift 5.
- Aucune dépendance tierce : URLSession, WebSocket, PhotosUI et le Trousseau, rien d’autre.
- Un compte Hooky et un site : sa clé publique est dans Sites › Installer, la même que le
data-site-keyde la bulle web.
Installation
Seul Swift Package Manager est pris en charge. Dans Xcode : File › Add Package Dependencies…, colle l’adresse du paquet, garde la règle Up to Next Major Version à partir de 1.0.0, puis ajoute le produit Hooky à la cible de ton app.
https://github.com/Defdjamel/hooky-iosOu dans un Package.swift :
dependencies: [
.package(url: "https://github.com/Defdjamel/hooky-ios", from: "1.0.0"),
],
targets: [
.target(name: "MonApp", dependencies: [
.product(name: "Hooky", package: "hooky-ios"),
]),
]Le paquet embarque son manifeste de confidentialité (PrivacyInfo.xcprivacy), qu’Xcode ajoute au rapport de confidentialité de ton app. Le code source est sous licence MIT : github.com/Defdjamel/hooky-ios.
Démarrer en 3 lignes
import Hooky
// 1. Au lancement (App.init ou application(_:didFinishLaunchingWithOptions:))
Hooky.configure(siteKey: "TA_CLE_PUBLIQUE")
// 2. La bulle flottante, sur n’importe quelle vue SwiftUI
ContentView().hookyBubble()
// 3. Ou la conversation depuis ton propre bouton
Hooky.open()configure charge le nom, la couleur et le message d’accueil du site, reprend la conversation enregistrée sur l’appareil et ouvre la connexion en direct. Appelle-le une fois, avant tout autre appel.
@main
struct MonApp: App {
init() { Hooky.configure(siteKey: "TA_CLE_PUBLIQUE") }
var body: some Scene {
WindowGroup { ContentView().hookyBubble() }
}
}| Paramètre | Rôle |
|---|---|
siteKey | Obligatoire. La clé publique du site. |
language | Facultatif. Impose la langue de la conversation : "fr", "en", "es", "pt", "it" ou "de". Sans lui, celle de l’appareil (voir Langue, mode sombre, accessibilité). |
translation | Facultatif, true par défaut. false retire la traduction des messages de l’équipe (voir Traduction des messages). |
apiURL | Facultatif, pour tester contre ton propre serveur. Par défaut https://api.heyhooky.com. |
La bulle
Un bouton flottant à la couleur de ton site, avec le logo Hooky et une pastille rouge pour les réponses pas encore lues. Le toucher ouvre la conversation.
SwiftUI
ContentView()
.hookyBubble() // dans le coin du bas, du côté réglé pour le site
ContentView()
.hookyBubble(alignment: .bottomLeading, padding: 24, hidden: estAuPaiement)
HookyBubble(size: 56) // le bouton seul, à placer toi-même
HookyBubble { afficherMaFeuille = true } // avec ta propre actionalignmentimpose le coin (sinon, le côté gauche ou droit réglé dans Sites) ;paddingla distance aux bords (60 points de côté, 20 de marge par défaut) ;hiddenla cache sur un écran où elle gênerait.- La bulle disparaît pendant que la conversation est ouverte.
UIKit
// Dans le coin du bas, zone sûre respectée (leading: true pour la gauche)
Hooky.showBubble(in: view)
// Ou une UIView à placer toi-même
let bulle = HookyBubbleView(size: 60)Ouvrir et fermer la conversation
Hooky.open() // feuille pleine hauteur, par-dessus le contrôleur le plus haut
Hooky.open(from: self) // UIKit : présentée par ce contrôleur
Hooky.close() // la refermeSur iPhone, la conversation s’ouvre en feuille pleine hauteur ; sur iPad et Mac, en feuille centrée. Ton utilisateur la ferme avec le bouton × ou en la faisant glisser vers le bas. Pour la présenter toi-même (dans une .sheet, une navigation…), utilise la vue :
.sheet(isPresented: $chat) { HookyConversationView() }Messages non lus
La bulle porte déjà sa pastille. Pour tes propres boutons (un onglet Aide, une icône de menu), le nombre de réponses de l’équipe pas encore vues se suit en direct. Il retombe à 0 quand la conversation s’ouvre.
// SwiftUI : Hooky.shared est un ObservableObject
@ObservedObject var hooky = Hooky.shared
AideView()
.tabItem { Label("Aide", systemImage: "questionmark.bubble") }
.badge(hooky.unreadCount)
// UIKit : un AsyncStream (la valeur actuelle, puis chaque changement)
Task {
for await n in Hooky.unreadCountUpdates() {
tabBarItem.badgeValue = n > 0 ? "\(n)" : nil
}
}
// Ou une fermeture, appelée sur le fil principal
Hooky.shared.onUnreadCountChange = { n in print(n) }Hooky.shared publie aussi siteName, isOpen et isConfigured, pour ton interface. Les réponses arrivent en direct tant que l’app est au premier plan ; au retour au premier plan, le SDK rattrape ce qu’il a manqué. La version 1.0.0 n’envoie pas de notification push à tes utilisateurs.
Identifier l’utilisateur
Ton app sait qui est connecté : dis-le à Hooky. Ton équipe lit, dans la fiche de la conversation, le nom, l’e-mail, ton identifiant de l’utilisateur, l’appareil et les données que tu y joins. Avec un e-mail, la conversation ne demande plus ni prénom ni e-mail.
// Après la connexion
Hooky.identify(
name: "Lila",
email: "lila@exemple.fr",
userId: "u_1042", // ton identifiant à toi
attributes: ["offre": "Pro", "panier": 49.9, "beta": true]
)
// Plus tard : fusionnées avec les précédentes
Hooky.setAttributes(["panier": 12, "dernier_ecran": "Paiement"])
Hooky.setAttributes(["beta": nil]) // nil efface une clé
// À la déconnexion
Hooky.reset()Les valeurs sont des HookyValue : un texte, un nombre ou un booléen. Les littéraux s’écrivent tels quels ; pour une variable, précise le type : ["offre": .string(user.offre), "panier": .number(total), "beta": .bool(beta)].
Comment ça marche
- Pas encore de conversation : tout reste en mémoire et part avec le premier message. Ensuite, chaque changement part aussitôt, regroupé (un seul envoi pour tout ce qui change dans la même seconde). Hors ligne, il repart au message suivant ou au retour de l’app.
nameetemailremplacent ceux de la conversation (nilles efface sur l’appareil) ;userId: nille laisse tel quel.- Le
userIdest gardé avec le jeton de la conversation. UnuserIddifférent, c’est un autre compte sur le même appareil : la conversation est oubliée, comme avecreset(). Le nouvel utilisateur ne voit pas le fil du précédent. Hooky.reset()referme la conversation et oublie le fil, l’identité, les données et le brouillon. Appelle-le à la déconnexion.- Aucun de ces appels ne lève d’erreur. Appelle
configureavant : sinon,identifyetsetAttributesne font rien (et s’arrêtent sur une assertion en Debug).
Les règles des données
| Champ | Règle |
|---|---|
| Données libres | À plat : 50 clés au plus, 8 Ko en tout. Les nouvelles clés remplacent les anciennes, les autres restent. |
| Clé | 1 à 64 caractères : lettres, chiffres, espaces, _, . et -. Sinon, elle est ignorée. |
| Valeur | Un texte (coupé à 500 caractères), un nombre ou un booléen. nil efface la clé. |
userId | 200 caractères au plus. |
name, email | Le nom : 100 caractères au plus. L’e-mail : une adresse valide, sinon il est ignoré. |
Hooky nettoie ce qui dépasse, sans jamais refuser le message de ton utilisateur.
Ces données sont déclaratives : la clé du site est publique, et n’importe qui peut l’utiliser pour envoyer un nom, un e-mail ou un userId de son choix. Elles aident ton équipe à situer la personne, elles ne prouvent pas qui elle est. N’y mets jamais de secret (mot de passe, jeton, numéro de carte) et ne t’en sers jamais pour donner un accès ou agir sur un compte : vérifie toujours de ton côté.
Stats
Compte ce qui se passe dans ton app, comme Hooky.track sur le web : les cartes s’affichent dans Stats, avec celles du site. Appelle-le juste après que l’action a réussi.
Hooky.track("inscription")
Hooky.track("commande", value: 49.9, properties: ["offre": "pro", "places": 3, "essai": false])value: un montant ou une quantité, additionné dans la carte. Les propriétés sont à plat (textes, nombres, booléens).- Appelable depuis n’importe quel fil, et même avant
configure: les événements attendent (100 au plus). Ne lève jamais d’erreur ; hors ligne, l’événement est perdu. - Mêmes règles que sur le web (nom, 20 propriétés au plus, débit) : voir Stats dans la page.
N’envoie jamais de donnée personnelle (e-mail, nom, téléphone) dans un événement. Compte des actions, pas des personnes.
Ce que fait la conversation
- Le premier message, avec prénom et e-mail facultatifs (ou ceux d’
identify), puis un fil comme celui de la bulle web : noms de l’équipe, « … écrit », « Lu », liens cliquables, emoji en grand, messages plus anciens à la demande. - Jusqu’à 4 pièces jointes par message, 10 Mo chacune : photos (JPEG, PNG, HEIC, WebP, GIF) ou PDF. Les photos de la photothèque passent par le sélecteur du système (aucune autorisation à demander) et sont réduites à 2048 px avant l’envoi ; les fichiers viennent de l’app Fichiers.
- Pour proposer l’appareil photo, ajoute
NSCameraUsageDescriptionà l’Info.plistde ton app. Sans elle, l’option est simplement cachée. - Les photos reçues s’ouvrent en plein écran (zoom, balayage, partage), les fichiers dans Coup d’œil.
- Le brouillon survit à la fermeture de la conversation et au relancement de l’app. Le clavier ne cache jamais le champ de saisie.
- Des erreurs en clair, dans la langue de l’utilisateur : hors ligne, fichier trop lourd, trop de fichiers, trop de messages d’un coup.
Traduction des messages
Ton utilisateur et ton équipe écrivent chacun dans leur langue, comme avec la bulle web (voir Traduction des messages).
- Sous un message de l’équipe écrit dans une autre langue que celle de la conversation (celle de l’appareil, ou celle imposée par
language), un lien discret Traduire. Traduit, le message s’affiche dans la langue de l’utilisateur, avec « Traduit de l’anglais · Voir l’original » pour revenir au texte d’origine. - Le bouton de traduction de l’en-tête propose Toujours traduire : les messages de l’équipe arrivent alors déjà traduits. Le choix est gardé sur l’appareil, pour chaque clé de site.
- Une traduction ratée affiche une courte erreur avec Réessayer. Les messages de l’utilisateur ne sont jamais traduits chez lui ; ton équipe a son propre bouton Traduire, dans le tableau de bord et l’app Hooky.
- Seul le texte est traduit, pas les pièces jointes. La traduction est active par défaut ; pour la retirer de ton app :
Hooky.configure(siteKey: "TA_CLE_PUBLIQUE", translation: false).
Ce que le SDK envoie
Ce qu’envoie la bulle web, et rien d’autre. Sans identifiant publicitaire, sans suivi entre apps, sans bibliothèque tierce. Pour remplir les informations de confidentialité de ta fiche App Store :
| Donnée | Quand | Fiche App Store |
|---|---|---|
| Messages, photos et PDF | Quand l’utilisateur écrit | Contenu utilisateur : service client, photos |
| Prénom et e-mail | S’il les donne, ou si tu les passes à identify | Coordonnées : nom, adresse e-mail |
Ton userId | Si tu le passes à identify | Identifiants : identifiant de l’utilisateur |
| Données libres | Ce que tu passes à identify et setAttributes | Selon ce que tu y mets (un panier : historique d’achats…) |
Événements track | Quand tu les envoies, sans identifiant de personne | Données d’utilisation : interactions avec le produit (analyses), non liées à l’utilisateur |
| Appareil, langue, fuseau horaire, app | Avec la conversation | Informations techniques pour ton équipe (voir ci-dessous) |
- Toutes ces données servent au fonctionnement de l’app (le support client), et les stats à tes analyses. Aucune ne sert au suivi au sens d’Apple : pas de demande App Tracking Transparency.
- L’appareil est lisible : « iPhone 15 Pro · iOS 18.2 », « iPad Air 11-inch (M2) · iPadOS 18.1 », « Mac · macOS 15.1 ». Chaque requête porte
X-Hooky-Client: hooky-ios/1.0.0etX-Hooky-App: <bundle id> <version> (<build>): ton équipe voit la conversation venir de « iOS · com.exemple.app 2.3 (45) ». - Sur l’appareil : le jeton de la conversation (la seule clé de l’utilisateur) et le
userIddans le Trousseau (cet appareil seulement, après le premier déverrouillage), par clé de site ; le brouillon, le dernier message vu et le choix « Toujours traduire » dansUserDefaults. Une traduction n’envoie que l’identifiant du message et la langue de la conversation. - Hooky traite ces données pour ton compte (sous-traitant au sens du RGPD) : voir la politique de confidentialité. Tu restes responsable de ta fiche : déclare ce que ton app envoie vraiment.
Langue, mode sombre, accessibilité
- Langue
- L’interface existe en français, anglais, espagnol, portugais, italien et allemand. Elle suit les langues préférées de l’appareil, l’anglais par défaut ;
Hooky.configure(siteKey:language:)en impose une. La langue part avec le premier message, pour que ton équipe réponde dans la bonne langue et à la bonne heure (avec le fuseau horaire). - Mode sombre
- La conversation suit le mode clair ou sombre de l’appareil, avec la couleur de ton site. Rien à régler.
- Accessibilité
- Chaque contrôle a son libellé VoiceOver (la bulle : « Ouvrir le chat avec … », et le nombre de non-lus) ; Dynamic Type partout, et le clavier ne cache jamais la saisie.
Dépannage
- Mon site a déclaré ses adresses : l’app est-elle refusée ?
- Non. Une app n’a pas d’adresse : elle se présente par son en-tête
X-Hooky-Clientet passe même quand le site a déclaré ses domaines. N’ajoute rien dans Sites. - La bulle garde des couleurs par défaut, ou rien n’arrive
- Vérifie la clé : c’est la clé publique du site (pas la clé secrète
hks_…, ni une clé APIhka_…). Avec une clé inconnue, la console Xcode affiche « Hooky: the site couldn’t be loaded (HTTP 404…) ». - Hors ligne
- Le message affiche « Pas de connexion » et reste dans le champ ;
identifyetsetAttributesrepartent plus tard. L’apparence du site se recharge à l’ouverture de la conversation. - L’option Appareil photo n’apparaît pas
- Ajoute
NSCameraUsageDescriptionà tonInfo.plist. - Un autre utilisateur voit l’ancienne conversation
- Appelle
Hooky.reset()à la déconnexion, et passe unuserIdàidentify: un identifiant différent repart d’une conversation vierge.
Journal des versions
- 1.0.0
- Première version :
configure, la bulle SwiftUI et UIKit,openetclose,identify(avecuserIdet données libres),setAttributes,track,reset, les non-lus en direct, photos et PDF, la traduction des messages de l’équipe, six langues, mode sombre, VoiceOver et Dynamic Type.