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.

ItemValor
iOS mínimo16.0
DistribuiçãoSwift Package Manager (sem dependências externas)
PermissõesCâ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âmetroTipoDefaultDescrição
licenseKeyStringObrigatório. Chave de licença pvv_live_* / pvv_sand_* / pvv_stag_*.
operatorIdStringObrigatório. UUID do operador (vistoriador) recebido no onboarding. Seleciona o certificado do dispositivo e precisa estar ACTIVE.
captureProfileCaptureProfile.vehicleInspectionPerfil 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. É 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.


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.

CampoTipoDefaultDescrição
shapeMaskShape.square.rectangle, .oval, .square ou .none (sem máscara).
sizeRatioCGFloat0.72Tamanho da moldura relativo ao menor lado da tela (0–1).
colorUIColorteal ProvviCor da borda.
lineWidthCGFloat2.5Espessura da borda em pontos.
dimOutsideCGFloat0.45Opacidade do escurecimento fora da moldura (0–1).
labelString?”Enquadre o objeto na moldura”Texto-guia; nil/vazio = sem texto.
labelPositionMaskLabelPosition.bottom.top, .bottom ou .center.
aspectRatioCGFloat?por formaProporçã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 /* ... */ }

CampoTipoDescrição
sessionIdStringIdentificador único da captura.
authenticatedJpegDataData?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çãoOnde apareceRecuperável
Já configurado nesta sessãoProvviSDKError.alreadyConfigured (em configure)Sim — ignorar
Licença ausente/inválida/expirada/revogada/sem volumeProvviSDKError.licenseValidationFailed (em configure) ou CaptureOutcome.licenseErrorDepende — ver mensagem
Permissão negadaCaptureOutcome.permissionDeniedSim — solicitar permissões
Dispositivo com jailbreakCaptureOutcome.deviceCompromisedNão
Localização simuladaCaptureOutcome.mockLocationDetectedNão
Relógio alteradoCaptureOutcome.clockSuspiciousNão
Falha na assinaturaCaptureOutcome.signingFailedSim (retry)
Falha no backendCaptureOutcome.backendErrorSim (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.