Pular para o conteúdo

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.

Item Valor
iOS mínimo 16.0
Distribuição Swift Package Manager (sem dependências externas)
Permissões Câmera (NSCameraUsageDescription), Localização (NSLocationWhenInUseUsageDescription)

O SDK é distribuído como Swift Package fornecido pela equipe Provvi. Adicione via SPM apontando para o repositório fornecido:

Package.swift
.package(url: "URL_DO_REPOSITORIO_PROVVI", from: "1.0.0")

Ou em Xcode: File ▸ Add Package Dependencies… e cole a URL fornecida.


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.


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â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ça ProvviSDKError.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.


Antes da primeira captura, provisione o certificado do dispositivo. A chamada é idempotente: se o dispositivo já tem certificado válido, ela retorna .alreadyProvisioned sem gerar chave no Secure Enclave, sem emitir CSR e sem ir à rede. Chamar a cada lançamento do app é barato e seguro — você não precisa guardar essa chamada com nenhuma verificação própria.

switch await ProvviSDK.provisionDeviceIfNeeded() {
case .success:
// Provisionou agora — dispositivo pronto para capturar.
case .alreadyProvisioned(let expiresAt):
// Já havia certificado válido (expira em expiresAt). Nada foi feito.
_ = expiresAt
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.


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 SwiftUI
import 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 o CaptureOutcome: .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.

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"
)

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 /* ... */ }

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.

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, mockLocationDetected e recaptureSuspected indicam que o SDK bloqueou a captura por suspeita de fraude — o app deve apresentar a situação ao usuário, não tentar contornar.


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.


<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 AVFoundation
import CoreLocation
if AVCaptureDevice.authorizationStatus(for: .video) == .notDetermined {
await AVCaptureDevice.requestAccess(for: .video)
}
let manager = CLLocationManager()
if manager.authorizationStatus == .notDetermined {
manager.requestWhenInUseAuthorization()
}

import SwiftUI
import 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"
}
}
}

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.