Intégrer le SDK mobile

Ce que le SDK iOS et Android sait faire, ce que votre application garde à sa charge, comment l'installer aujourd'hui, et pourquoi il n'est pas encore distribuable à une équipe extérieure.

Sur cette page

Le SDK mobile est un client typé de l'API publique SealTrust, plus trois écrans prêts à poser, pour iOS et pour Android. En quittant cette page, vous saurez ce qu'il sait faire, ce qu'il ne fera jamais, ce que votre application doit garder à sa charge, comment le brancher, et dans quel état de distribution il se trouve aujourd'hui.

Commençons par cet état, parce qu'il change la réponse à la question « puis-je m'en servir cette semaine ».

#Où en est ce SDK aujourd'hui

Les deux plateformes sont en version 0.1.0 et ne sont publiées sur aucun dépôt de paquets.

Côté iOS, le manifeste du paquet Swift ne se trouve pas à la racine d'un dépôt git. Le gestionnaire de paquets Swift ne résout un paquet depuis une adresse git que dans ce cas. La déclaration .package(url:) ne fonctionne donc pas, et seule la déclaration par chemin local fonctionne.

Côté Android, les deux modules déclarent bien des coordonnées Maven, io.sealtrust:sealtrust-core et io.sealtrust:sealtrust-ui, en version 0.1.0. La configuration de publication ne déclare aucun dépôt distant. La seule publication possible est donc locale, sur votre propre machine.

#Le partage du travail

Le SDK ne lit pas la puce. C'est la première chose à comprendre, parce qu'elle détermine tout le reste.

Une puce NFC SealTrust prouve son authenticité en calculant une signature neuve à chaque lecture, avec une clef propre à la puce. Vérifier cette signature exige les clefs de puce de la marque, et ces clefs ne quittent pas nos serveurs.

QuiFait quoi
Le système du téléphoneLit l'adresse que la puce émet, ou le QR imprimé.
Votre applicationPossède la session NFC, l'appareil photo, la connexion de l'utilisateur.
Le SDKLit cette adresse, appelle l'API, transforme la réponse en verdict affichable.
L'API SealTrustVérifie la cryptographie de la puce et répond.

Trois conséquences concrètes.

Le SDK ne demande aucune autorisation NFC et aucune autorisation de localisation. C'est le module Android des écrans qui déclare l'autorisation d'accès à internet, et c'est la seule qu'il déclare. Le module client seul ne déclare aucune autorisation.

La vérification hors ligne n'existe pas. Chaque contrôle est un appel réseau.

Le SDK ne contient aucun secret. Il n'y a pas de clef d'API mobile, pas de clef de signature, pas d'identifiant embarqué. La seule chose qu'il peut porter est le jeton de session d'un utilisateur déjà connecté par vos soins. Il l'envoie sur les appels de revendication de propriété, et sur la lecture de passeport quand ce jeton est disponible.

#Un produit peut porter deux codes, et ils ne prouvent pas la même chose

Le SDK nomme lequel des deux a été lu. Le traitement diffère ensuite pour chacun.

Ce qui a été scannéCe que cela prouveCe que vous appelez
La puce NFCL'objet physique était là. La puce recalcule une signature neuve à chaque lecture.verifyScan
Le QR impriméQuel article c'est. Une photographie de l'étiquette redonne la même chaîne.passport, certificate, history, et requestClaimTicket à partir du numéro de série

Beaucoup de produits ne portent qu'un QR. La plateforme sert ce mode à part entière. Le passeport, le certificat et l'historique se lisent alors normalement, à partir du seul numéro imprimé. C'est la preuve de présence physique qui n'est pas fournie.

La fonction de lecture d'adresse vous dit laquelle des deux vous avez entre les mains, et rend une valeur vide quand l'adresse n'est ni l'une ni l'autre. Ce dernier cas arrive souvent en production : un client qui pointe votre scanner sur le lien GS1 d'une autre marque mérite une réponse propre.

import SealTrustKit

func traiter(_ chaineScannee: String, avec client: SealTrustClient) async throws {
    switch ScanURL.read(chaineScannee) {
    case .chip(let preuve):
        let verification = try await client.verifyScan(preuve)
        print(verification.productName ?? "")
    case .printedSerial(let numeroDeSerie):
        let passeport = try await client.passport(identifier: numeroDeSerie)
        print(passeport.productName ?? "")
    case nil:
        print("Ce code n'est pas un code SealTrust.")
    }
}

Le QR imprimé porte deux formes : /p/{serial} et la forme GS1 /01/{gtin}/21/{serial}. Un lien /01/{gtin} sans numéro de série nomme une référence produit, donc la lecture ne rend aucun numéro de série. Ce n'est pas une impasse : une référence a son propre passeport, celui du modèle, et le client le lit avec referencePassport. Gardez la distinction en tête, un passeport de référence décrit un modèle et ne prouve rien sur l'objet que vous tenez.

Le passeport se sert à deux niveaux seulement : le modèle, par son code produit GTIN, et l'article, par son identifiant. Un passeport de modèle couvre tous les exemplaires qui partagent le même code produit. Le règlement ESPR autorise aussi un passeport de lot, que la plateforme ne sert pas aujourd'hui.

Le numéro de série est rendu exactement tel qu'il est imprimé, jamais réécrit. C'est le serveur qui applique les règles de normalisation. Une application publiée ne se met pas à jour au même rythme que le serveur, et un client qui appliquerait ses propres règles répondrait « produit introuvable » sur une étiquette valable le jour où les deux jeux de règles divergeraient.

#Plateformes et versions minimales

CaractéristiqueiOSAndroid
Version minimaleiOS 16niveau d'API 26
DistributionPaquet Swift SealTrustSDKProjet Gradle, deux modules
ClientBibliothèque SealTrustKitModule sealtrust-core
ÉcransBibliothèque SealTrustUIModule sealtrust-ui
Dépendances du clientAucune, Foundation seulAucune
Dépendances des écransSwiftUI seulJetpack Compose et androidx.lifecycle:lifecycle-viewmodel-compose

Le module Android du client est un module Kotlin ordinaire, sans rien d'Android dedans. Il compile en Java 17. Le module des écrans vise le niveau de compilation 35 et le niveau minimal 26, ce dernier étant imposé par la lecture des dates. Descendez plus bas uniquement avec le sucre syntaxique de la bibliothèque de base active dans votre application.

#Installer

// Package.swift de votre application.
// Remplacez le chemin par celui de votre copie locale des sources.
dependencies: [
    .package(name: "SealTrustSDK", path: "<chemin-vers-les-sources>/ios"),
],
targets: [
    .target(
        name: "VotreApplication",
        dependencies: [
            .product(name: "SealTrustKit", package: "SealTrustSDK"),
            .product(name: "SealTrustUI", package: "SealTrustSDK"),
        ]
    ),
]

Le paramètre name: est obligatoire sur iOS. Un paquet déclaré par chemin prend pour identité le nom de son répertoire. Sans ce paramètre, le gestionnaire de paquets Swift ne trouve pas SealTrustSDK et la compilation échoue.

Sur Android, une seconde voie existe si le build composite ne vous convient pas. Depuis le répertoire du projet Gradle du SDK, ./gradlew publishToMavenLocal installe les deux modules dans votre dépôt Maven local. Vous les consommez ensuite comme des dépendances versionnées.

Kotlin
repositories {
    mavenLocal()
}

dependencies {
    implementation("io.sealtrust:sealtrust-core:0.1.0")
    implementation("io.sealtrust:sealtrust-ui:0.1.0")
}

Si vous ne voulez que le client, prenez SealTrustKit seul, ou sealtrust-core seul. Vous ne payez alors rien pour les écrans.

Une conséquence sur Android : c'est le module des écrans qui déclare l'autorisation d'accès à internet. Si vous prenez sealtrust-core seul, déclarez android.permission.INTERNET dans votre propre manifeste.

#Créer le client

Le client se construit avec une configuration. Ses quatre valeurs :

ValeurDéfautRôle
Adresse de basehttps://api.sealtrust.ioÀ remplacer pour viser un autre environnement ou votre propre relais.
Délai d'attente15 secondesDélai de connexion et de lecture.
Identifiant du clientsealtrust-sdk-ios/0.1.0 ou sealtrust-sdk-android/0.1.0Envoyé en en-tête X-SealTrust-Client.
Fournisseur de jetonabsentRend le jeton de session de l'utilisateur connecté, ou rien.

Le SDK ne connecte personne. Les appels de revendication de propriété exigent un utilisateur authentifié, donc votre application fournit le jeton. Rendez une valeur vide quand personne n'est connecté : le SDK échoue alors avec une erreur d'authentification au lieu d'envoyer une requête qui ne peut revenir qu'en 401.

import Foundation
import SealTrustKit

enum SessionApplication {
    /// Jeton de l'utilisateur connecté, alimenté par votre propre connexion.
    static var jeton: String?
}

let client = SealTrustClient(
    configuration: SealTrustConfiguration(
        accessTokenProvider: { SessionApplication.jeton }
    )
)

Deux en-têtes accompagnent chaque appel : Accept: application/json et X-SealTrust-Client. Les appels qui portent un corps ajoutent Content-Type: application/json.

L'en-tête Authorization est ajouté sur les appels de revendication, et sur la lecture de passeport quand votre application a fourni un jeton. Un propriétaire connecté voit alors sa lecture élargie.

Une différence de style entre les deux plateformes, à ne pas rater. Sur iOS, toutes les méthodes qui appellent le réseau sont asynchrones. La construction de l'adresse du certificat en PDF est synchrone, parce qu'elle n'appelle rien. Sur Android, les méthodes sont bloquantes : appelez-les hors du fil principal. Les trois écrans Android le font déjà pour vous.

#Vérifier un scan

C'est l'appel central. Vous lui donnez l'adresse lue sur la puce, il rend un verdict.

import SealTrustKit

func verifier(_ adresse: URL, avec client: SealTrustClient) async -> ScanOutcome {
    do {
        return ScanOutcome.from(try await client.verifyScan(url: adresse))
    } catch {
        return ScanOutcome.from(error)
    }
}

Le verdict prend huit formes, et les distinguer compte pour l'utilisateur.

VerdictCe qui s'est passéCe que l'écran doit dire
AuthentiqueLa puce a répondu une signature que le serveur a validée.Produit authentique.
RejeuCette adresse exacte a déjà été présentée.Alerte. C'est le signal qu'une étiquette a pu être copiée.
Signature invalideLa signature ne correspond pas à cette puce.Ce n'est pas une puce SealTrust, ou c'est une copie.
Frappe en coursPuce authentique, son certificat est en cours d'écriture.Demandez à l'utilisateur de réessayer dans un moment.
Jamais enregistréPuce authentique, rien n'a jamais été enregistré pour elle.L'anomalie est du côté de la marque. L'acheteur n'a rien à corriger.
Lien illisibleL'adresse ne portait pas de preuve exploitable, ou l'API a refusé les paramètres.Invitez à rescanner.
Trop d'appelsLe plafond par adresse IP est atteint.Invitez à réessayer dans une minute.
ÉchecTout le reste, y compris le réseau injoignable.Erreur technique, sans verdict d'authenticité.

Un conflit non reconnu est lu comme un rejeu. Le SDK remonte donc l'anomalie au lecteur.

#Le scellé électronique et la déclaration de vol

Deux informations voyagent avec la vérification et méritent un traitement particulier.

Le scellé Tag Tamper est un état à trois valeurs : jamais ouvert, ouvert, ou rien à dire. Une puce standard et une puce dont le scellé n'a jamais été activé rendent toutes deux la troisième valeur, sur des produits authentiques. N'affichez jamais l'absence d'information comme un scellé rompu. Le SDK expose une propriété qui combine cet état avec la politique choisie par la marque, et qui rend une valeur vide dès qu'il n'y a rien à montrer. Trois politiques existent : silence, information, et confiance affichée dégradée. La valeur par défaut est l'information.

Une déclaration de vol ouverte sur une unité ne fait pas échouer la vérification. Le scan répond normalement, et un booléen porte l'avertissement. Affichez-le, sinon il n'est dit nulle part.

#Lire un passeport, ses preuves et son certificat

Le client expose sept lectures, toutes publiques.

AppelCe qu'il rend
passportLe passeport numérique publié d'une unité.
passportProofLe faisceau de preuves de ce passeport.
referencePassportLe passeport du modèle, à partir d'un GTIN.
referencePassportProofLe faisceau de preuves du modèle.
certificateLe certificat d'authenticité public.
certificatePDFURL sur iOS, certificatePdfUrl sur AndroidL'adresse du même certificat en PDF. Rien n'est appelé, l'adresse est construite.
historyLes mouvements de propriété et les scans d'authentification, du plus récent au plus ancien.

Le passeport se lit à un niveau d'accès. Le SDK expose les six valeurs que l'API accepte : public, end_user, repairer, recycler, upstream et authority. Les trois niveaux de métier sont des publics distincts. Aucun ne contient les autres : détenir l'accréditation de réparateur n'ouvre pas celle du recycleur. Le défaut est public. Les niveaux supérieurs sont contrôlés par le serveur, qui répond 401 ou 403 à un appelant qui n'y a pas droit. Le niveau réellement servi figure dans la réponse, et il n'est pas toujours celui demandé : un propriétaire connecté voit sa lecture publique élargie automatiquement.

let passeport = try await client.passport(
    identifier: "0x0000000000000000000000000000000000000000000000000000000000000000"
)
print(passeport.accessTier, passeport.productName ?? "")

let preuves = try await client.passportProof(
    identifier: "0x0000000000000000000000000000000000000000000000000000000000000000"
)
print(preuves.dataHash ?? "", preuves.ipfsURI ?? "")

let modele = try await client.referencePassport(gtin: "03701234567890")
print(modele.productName ?? "")

Le faisceau de preuves porte l'empreinte du contenu, la copie sur IPFS, l'état d'ancrage sur Base et l'état du certificat vérifiable. L'état d'ancrage prend trois valeurs distinctes : ancré, jamais ancré, et ancré puis modifié. La troisième est la seule que ce mécanisme existe pour lever. Ne l'affichez jamais comme un « non ancré ».

Il prend une quatrième forme, une valeur vide, quand le serveur n'a servi aucun bloc d'ancrage. Traitez ce cas à part et n'affichez jamais cette valeur vide comme un « non ancré ». Elle signifie que vous n'avez aucune information sur l'ancrage de ce produit. L'ancrage sur Base est une opération que SealTrust déclenche à la main, et la plupart des produits ne sont jamais ancrés. Une marque ne peut pas le déclencher elle-même.

#Revendiquer la propriété d'un produit

Le parcours enregistre un produit scanné au nom de l'utilisateur connecté. Il tient en deux appels, plus une lecture d'état.

Le premier appel demande un ticket à partir de la lecture de la puce. Un ticket n'est délivré que tant que le produit n'appartient à personne.

Une réponse en succès sans ticket est normale. Le champ de détail dit alors pourquoi : déjà revendiqué, déjà transféré, identifiant inconnu, revendication concurrente en cours, ou code d'achat exigé.

import SealTrustKit

func revendiquer(_ chaineScannee: String, avec client: SealTrustClient) async throws {
    guard case .chip(let preuve) = ScanURL.read(chaineScannee) else {
        print("Ce code n'est pas une lecture de puce.")
        return
    }
    let demande = try await client.requestClaimTicket(for: preuve)
    guard let ticket = demande.ticket else {
        print(demande.detail ?? "Aucun ticket délivré.")
        return
    }
    let resultat = try await client.completeClaim(ticket)
    print(resultat.txHash ?? "")
}

Le second appel transforme le ticket en propriété inscrite sur la chaîne. Il exige le jeton de l'utilisateur connecté. La réponse est écrite dès que le transfert est diffusé, avant qu'il soit gravé, donc l'empreinte de transaction qu'elle porte désigne une opération en attente.

Certaines marques remettent un code d'achat avec le produit. N'en demandez un à l'utilisateur qu'après que l'API a répondu une première fois qu'il est exigé. Les clients des marques qui n'en émettent aucun ne verront alors jamais ce champ.

Le ticket porte un nonce. C'est la preuve que quelqu'un a physiquement lu l'étiquette. Gardez-le en mémoire, ne le journalisez pas, ne le stockez pas.

Un troisième appel lit l'état d'un ticket : délivré, consommé, expiré ou annulé. Il exige aussi le jeton de l'utilisateur. Le serveur renvoie cet état tel quel, sans filtrage. Traitez-le comme une chaîne ouverte et prévoyez un affichage par défaut pour une valeur que vous ne connaissez pas.

Il existe une variante qui demande un ticket à partir du numéro de série imprimé, sans puce. Elle est authentifiée, et elle ne fonctionne que sur la toute première revendication d'un produit : un produit déjà possédé ou déjà revendiqué est refusé en 409. Un numéro imprimé prouve beaucoup moins qu'une lecture de puce, et ce verrouillage en est la conséquence. Elle n'est pas ouverte sur tous les comptes : si elle ne l'est pas sur le vôtre, elle répond 404, et c'est la seule lecture à faire de ce code sur cet appel. Vérifiez avec nous qu'elle est ouverte pour votre application avant de l'intégrer.

#Les trois écrans prêts à poser

Le module d'interface fournit trois écrans construits sur le client. Ils existent en SwiftUI et en Compose, avec les mêmes paramètres.

ÉcranCe qu'il prendCe qu'il rend à votre application
VérificationLe client, l'adresse scannée ou la preuve, des coordonnées facultatives, les textes.Deux rappels : ouvrir le passeport, ou lancer la revendication.
RevendicationLe client, la preuve, un booléen disant si un utilisateur est connecté, le nom du produit, les textes.Deux rappels : demander la connexion, ou annoncer la revendication faite.
PasseportLe client, l'identifiant, le niveau d'accès, les textes.Rien, c'est un écran de lecture.

L'écran de revendication ne lance pas votre connexion. Vous lui dites si un utilisateur est connecté, il demande la connexion au lieu de déclencher un appel qui ne peut revenir qu'en 401.

Tous les textes visibles sont surchargeables champ par champ. Un jeu anglais complet et un jeu français complet sont fournis, et aucune phrase visible n'est figée à l'intérieur d'un écran.

Les couleurs sont des champs d'un thème et se remplacent une par une. Les trois écrans n'ont qu'une seule apparence : le thème porte des couleurs fixes et ne réagit pas au mode sombre du système.

#Les erreurs

Le SDK ramène tout à cinq cas.

CasQuand
Adresse de scan invalideLe lien ne portait pas de preuve exploitable.
Non authentifiéUn appel authentifié a été tenté sans jeton de session.
Erreur d'APIL'API a répondu en 4xx ou 5xx. Le cas porte le code HTTP, le détail, et le code machine quand l'API en préfixe un.
DécodageLe corps de la réponse n'a pas la forme attendue.
TransportLa requête n'a jamais abouti : DNS, TLS, délai dépassé, pas de réseau.

Le code machine est extrait du détail quand celui-ci commence par un mot en majuscules contenant au moins un tiret bas, par exemple MINT_PENDING ou CLAIM_EXPIRED. Un mot en majuscules sans tiret bas n'est jamais extrait. Le code machine vaut une valeur vide quand l'API répond une phrase libre.

#Plafonds d'appels

Les appels du SDK visent des points d'entrée publics, dont les plafonds se comptent par adresse IP, sur une fenêtre de 60 secondes.

Point d'entréePlafond
Vérification de scan30 par minute
Demande de ticket depuis un scan30 par minute
Passeport et preuves60 par minute
Certificat60 par minute
Historique du produit30 par minute
Revendication20 par minute

Un dépassement revient en 429, et le SDK le traduit par le verdict « trop d'appels » sur le chemin de vérification.

#Coordonnées et vie privée

Les coordonnées sont facultatives sur la vérification de scan. Les deux paramètres valent une valeur vide par défaut, sur les deux plateformes : un intégrateur qui ne les passe jamais ne transmet rien.

Elles servent aux contrôles anti-fraude côté serveur. Ne les passez qu'avec le consentement de l'utilisateur.

Le SDK ne lit aucune position, ne demande aucune autorisation de localisation et ne vérifie pas que vous avez obtenu quoi que ce soit. Le consentement, sa trace et sa mention dans votre politique de confidentialité sont votre obligation d'éditeur de l'application.

Ce que la réponse rend en retour est une zone de scan au niveau de la ville, sous la forme d'un nom de ville et d'un code pays, et uniquement quand vous avez fourni des coordonnées. La réponse ne contient aucune coordonnée.

Sur iOS, le paquet embarque un manifeste de confidentialité Apple. Il déclare l'absence de suivi et deux types de données collectées.

La position précise, uniquement quand votre application passe des coordonnées.

L'identifiant utilisateur, c'est-à-dire le jeton de session. Le manifeste dit qu'il est envoyé uniquement sur POST /claims/from-serial, sur POST /claims/complete, sur GET /claims/{id}, et sur GET /passport/{id} quand un jeton est disponible. Reprenez cette liste telle quelle si vous remplissez la déclaration de confidentialité de l'App Store.

Sur Android, rien d'équivalent ne se déclare à l'intérieur du module. C'est l'éditeur de l'application qui remplit le formulaire de sécurité des données de la boutique, et il doit y couvrir les données collectées par chaque bibliothèque embarquée, celle-ci comprise.

#Ce qui n'est pas dans le SDK

  • Le transfert de propriété entre deux personnes. Il demande un code à usage unique et un scan concordant des deux côtés. C'est un autre parcours.
  • La frappe de produits et la destruction. Ce sont des opérations authentifiées, elles se conduisent depuis la console.
  • La lecture des puces, le scan des QR, le chargement des images, la connexion des utilisateurs. Votre application possède déjà tout cela, et le SDK n'embarque volontairement aucune dépendance pour les faire.

Votre réponse ouvre un courriel pré-rempli dans votre messagerie, à destination de contact@sealtrust.io. Vous le relisez avant de l'envoyer.

Proposer une correctionSignaler un problème