iOS SDK
SDK nativo Swift para captura autenticada com assinatura ICP-Brasil. O SDK é dono da experiência de captura (preview ao vivo + botão de disparo + máscara-guia opcional) — o app integrador cuida apenas da navegação, do contexto de negócio e de exibir o resultado.
Requisitos
Seção intitulada “Requisitos”| Item | Valor |
|---|---|
| iOS mínimo | 16.0 |
| Distribuição | Swift Package Manager (sem dependências externas) |
| Permissões | Câmera (NSCameraUsageDescription), Localização (NSLocationWhenInUseUsageDescription) |
Instalação
Seção intitulada “Instalação”O SDK é distribuído como Swift Package fornecido pela equipe Provvi. Adicione via SPM apontando para o repositório fornecido:
.package(url: "URL_DO_REPOSITORIO_PROVVI", from: "1.0.0")Ou em Xcode: File ▸ Add Package Dependencies… e cole a URL fornecida.
Visão geral do fluxo
Seção intitulada “Visão geral do fluxo”A integração tem quatro passos. Os dois primeiros rodam uma vez por sessão do app; os dois últimos, uma vez por captura.
1. configure(...) → inicializa o SDK e valida a licença (uma vez, no startup)2. provisionDeviceIfNeeded() → provisiona o certificado do dispositivo (uma vez)3. ProvviCaptureScreen → tela de captura do SDK (SwiftUI) (por captura)4. onComplete: CaptureOutcome → resultado da captura (por captura)O ponto de entrada é o tipo ProvviSDK. Toda a rede, assinatura e orquestração ficam internas ao
SDK — o integrador nunca monta manifesto, nem fala com o backend diretamente.
1. Inicialização
Seção intitulada “1. Inicialização”Chame ProvviSDK.configure(...) uma vez no startup do app. É async throws — chame de um contexto
assíncrono.
import ProvviSDK
do { try await ProvviSDK.configure( licenseKey: licenseKey, // pvv_live_* / pvv_sand_* / pvv_stag_* operatorId: operatorId, // UUID do operador (identidade que assina) captureProfile: .vehicleInspection )} catch ProvviSDKError.alreadyConfigured { // Já configurado nesta sessão — seguro ignorar.} catch { // Licença inválida/expirada/revogada/sem volume — trate antes de capturar.}Parâmetros
Seção intitulada “Parâmetros”| Parâmetro | Tipo | Default | Descrição |
|---|---|---|---|
licenseKey | String | — | Obrigatório. Chave de licença pvv_live_* / pvv_sand_* / pvv_stag_*. |
operatorId | String | — | Obrigatório. UUID do operador (vistoriador) recebido no onboarding. Seleciona o certificado do dispositivo e precisa estar ACTIVE. |
captureProfile | CaptureProfile | .vehicleInspection | Perfil de captura. .kycSelfie usa a câmera frontal. |
A validação da licença acontece dentro de
configure(...). Se a chave for inválida, expirada, revogada ou estiver acima do volume contratado, a chamada lançaProvviSDKError.licenseValidationFailed(_)— trate-a antes de prosseguir. Consulte Licenciamento.
Identidade do app é derivada automaticamente. O SDK identifica o app (Bundle ID + Apple Team ID) em runtime — você não informa esses dados (o App Attest da Apple valida a identidade real do app). A persistência opcional do JPEG é definida pela sua licença (política de contrato), não por parâmetro de código.
2. Provisionamento do dispositivo
Seção intitulada “2. Provisionamento do dispositivo”Antes da primeira captura, provisione o certificado do dispositivo. É uma operação única por instalação.
switch await ProvviSDK.provisionDeviceIfNeeded() {case .success: // Dispositivo pronto para capturar.case .failure(let error): showError("Falha no provisionamento: \(error.localizedDescription)")}O provisionamento é fail-closed: se falhar, a captura não deve prosseguir. A chave do dispositivo é gerada no Secure Enclave e nunca sai do aparelho.
3. Captura
Seção intitulada “3. Captura”A captura usa a tela do SDK (ProvviCaptureScreen), uma view SwiftUI com preview ao vivo,
botão de disparo e, opcionalmente, uma máscara-guia. Apresente-a (por exemplo, via
.fullScreenCover) e receba o resultado no callback onComplete.
import SwiftUIimport ProvviSDK
struct CapturaView: View { @State private var capturando = false
var body: some View { Button("Capturar") { capturando = true } .fullScreenCover(isPresented: $capturando) { ProvviCaptureScreen( capturedBy: "operador-123", referenceId: "VIN-9BWZZZ377VT004251", maskConfig: MaskConfig(shape: .rectangle, label: "Enquadre o veículo") ) { outcome in capturando = false handle(outcome) } } }}O
onCompleteé chamado uma vez com oCaptureOutcome:.success(com o JPEG assinado),.cancelled(usuário fechou a tela antes do disparo) ou um caso de erro. Diferente do Android, não há leitura separada do resultado — ele chega direto no callback.
Captura sem a tela do SDK (avançado)
Seção intitulada “Captura sem a tela do SDK (avançado)”Se você não usa a tela do SDK, há um caminho direto (sem preview/shutter):
let outcome = await ProvviSDK.capture( useFrontCamera: false, capturedBy: "operador-123", referenceId: "ref-001")4. Máscara-guia (MaskConfig)
Seção intitulada “4. Máscara-guia (MaskConfig)”A máscara é apenas visual — orienta o enquadramento e nunca altera a imagem assinada.
| Campo | Tipo | Default | Descrição |
|---|---|---|---|
shape | MaskShape | .square | .rectangle, .oval, .square ou .none (sem máscara). |
sizeRatio | CGFloat | 0.72 | Tamanho da moldura relativo ao menor lado da tela (0–1). |
color | UIColor | teal Provvi | Cor da borda. |
lineWidth | CGFloat | 2.5 | Espessura da borda em pontos. |
dimOutside | CGFloat | 0.45 | Opacidade do escurecimento fora da moldura (0–1). |
label | String? | ”Enquadre o objeto na moldura” | Texto-guia; nil/vazio = sem texto. |
labelPosition | MaskLabelPosition | .bottom | .top, .bottom ou .center. |
aspectRatio | CGFloat? | por forma | Proporção largura÷altura; nil usa o default da forma. |
5. Metadados (capturedBy, referenceId, assertionsJson)
Seção intitulada “5. Metadados (capturedBy, referenceId, assertionsJson)”capturedBy e referenceId são propagados ao manifesto C2PA da captura. Para campos adicionais,
passe um JSON em assertionsJson — ele é mesclado verbatim na assertion com.provvi.capture.
ProvviCaptureScreen( capturedBy: "operador-123", referenceId: "policy-12345", assertionsJson: #"{"inspector_name":"João Silva"}"#) { outcome in /* ... */ }6. Resultado
Seção intitulada “6. Resultado”Sucesso — ProvviCaptureResult
Seção intitulada “Sucesso — ProvviCaptureResult”| Campo | Tipo | Descrição |
|---|---|---|
sessionId | String | Identificador único da captura. |
authenticatedJpegData | Data? | JPEG com o manifesto C2PA + assinatura ICP-Brasil embutidos. |
A prova de autenticidade viaja dentro do JPEG — não há URL de manifesto separada a tratar em runtime. O JPEG é autoverificável: o manifesto pode ser extraído e verificado em verify.provvi.com.br. GPS, integridade, horário e demais sinais já estão embarcados no manifesto.
Tratamento de CaptureOutcome
Seção intitulada “Tratamento de CaptureOutcome”func handle(_ outcome: CaptureOutcome) { switch outcome { case .success(let result, _): let sessionId = result.sessionId if let jpeg = result.authenticatedJpegData { // Salvar, exibir ou enviar ao seu backend. }
case .cancelled: break // usuário desistiu — fluxo normal, sem erro
case .backendError(let message, _): showRetry(message) // falha ao concluir no backend (ex.: indisponibilidade)
case .licenseError(let reason): showError(reason) // licença inválida/expirada/revogada/sem volume
case .permissionDenied: showError("Permissão de câmera ou localização negada")
case .deviceCompromised: showError("Dispositivo não íntegro (jailbreak)")
case .clockSuspicious: showError("Relógio do dispositivo alterado")
case .signingFailed(let reason): showError("Falha ao assinar: \(reason)")
case .captureError(let reason): showError("Erro na captura: \(reason)")
case .mockLocationDetected, .recaptureSuspected: showError("Captura bloqueada por suspeita de fraude") }}Os casos
deviceCompromised,mockLocationDetectederecaptureSuspectedindicam que o SDK bloqueou a captura por suspeita de fraude — o app deve apresentar a situação ao usuário, não tentar contornar.
7. Erros
Seção intitulada “7. Erros”| Situação | Onde aparece | Recuperável |
|---|---|---|
| Já configurado nesta sessão | ProvviSDKError.alreadyConfigured (em configure) | Sim — ignorar |
| Licença ausente/inválida/expirada/revogada/sem volume | ProvviSDKError.licenseValidationFailed (em configure) ou CaptureOutcome.licenseError | Depende — ver mensagem |
| Permissão negada | CaptureOutcome.permissionDenied | Sim — solicitar permissões |
| Dispositivo com jailbreak | CaptureOutcome.deviceCompromised | Não |
| Localização simulada | CaptureOutcome.mockLocationDetected | Não |
| Relógio alterado | CaptureOutcome.clockSuspicious | Não |
| Falha na assinatura | CaptureOutcome.signingFailed | Sim (retry) |
| Falha no backend | CaptureOutcome.backendError | Sim (retry) |
Consulte Erros para a tabela completa e Licenciamento para os erros de licença.
8. Permissões (Info.plist)
Seção intitulada “8. Permissões (Info.plist)”<key>NSCameraUsageDescription</key><string>A câmera é necessária para captura autenticada de imagens.</string>
<key>NSLocationWhenInUseUsageDescription</key><string>A localização é necessária para autenticar a captura.</string>O SDK dispara os prompts de câmera (AVCaptureDevice.requestAccess) e localização
(CLLocationManager.requestWhenInUseAuthorization) automaticamente ao iniciar a captura. Se o
usuário negar, o resultado cai em CaptureOutcome.permissionDenied — capturável no switch, sem
crash.
Para controlar o momento do prompt (exibir contexto na sua UI antes do sheet do iOS), pré-autorize antes de apresentar a tela de captura:
import AVFoundationimport CoreLocation
if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined { await AVCaptureDevice.requestAccess(for: .video)}let manager = CLLocationManager()if manager.authorizationStatus == .notDetermined { manager.requestWhenInUseAuthorization()}9. Exemplo completo
Seção intitulada “9. Exemplo completo”import SwiftUIimport ProvviSDK
struct CapturaView: View { let licenseKey: String let operatorId: String
@State private var pronto = false @State private var capturando = false @State private var status = ""
var body: some View { VStack(spacing: 16) { Button("Capturar") { capturando = true } .disabled(!pronto) Text(status).font(.caption).foregroundStyle(.secondary) } .task { await prepare() } .fullScreenCover(isPresented: $capturando) { ProvviCaptureScreen( capturedBy: operatorId, referenceId: "ref-\(Int(Date().timeIntervalSince1970))", maskConfig: MaskConfig(shape: .rectangle) ) { outcome in capturando = false handle(outcome) } } }
// 1. Configurar + 2. Provisionar (uma vez) private func prepare() async { do { try await ProvviSDK.configure( licenseKey: licenseKey, operatorId: operatorId, captureProfile: .vehicleInspection ) } catch ProvviSDKError.alreadyConfigured { // ok } catch { status = "Licença inválida: \(error.localizedDescription)" return } if case .failure(let e) = await ProvviSDK.provisionDeviceIfNeeded() { status = "Provisionamento falhou: \(e.localizedDescription)" return } pronto = true }
// 4. Tratar resultado private func handle(_ outcome: CaptureOutcome) { switch outcome { case .success(let result, _): status = "OK: \(result.sessionId)" // result.authenticatedJpegData → salvar/exibir case .cancelled: status = "Cancelado" case .licenseError(let reason): status = "Licença: \(reason)" default: status = "Falha na captura" } }}Reconfiguração
Seção intitulada “Reconfiguração”Para trocar de operador ou licença na mesma sessão do app, chame await ProvviSDK.reset() antes de
configure(...) novamente. O reset() limpa apenas o estado local do SDK — não revoga o
certificado do dispositivo nem altera nada no backend.