Este guia mostra como integrar o fluxo de verificação KYC da Gu1 em seus aplicativos móveis (Android, iOS, React Native, Flutter) e aplicações web (React, Vue, vanilla JS). A verificação é executada em um WebView/iframe apontando para uma URL segura gerada pelo seu backend.
Este guia assume que você já configurou a integração do seu backend com a API da Gu1. Caso contrário, comece com a Visão Geral da API KYC.
CRÍTICO: Nunca exponha as credenciais da API da Gu1 ou IDs de workflow no código do lado do cliente. Sempre gere URLs de verificação a partir do seu backend seguro.
Gere URLs de verificação que expirem rapidamente (15-30 minutos). Se o usuário não iniciar dentro desse tempo, gere uma nova.
Copy
Ask AI
// No seu backendconst validation = await gu1.createValidation({ // ... dados da entidade});// Armazenar com expiraçãoawait redis.setex( `kyc:${userId}`, 1800, // TTL de 30 minutos validation.validation_url);
Usar padrão Process-and-Purge
Ao receber webhooks da Gu1:
Extrair apenas os dados necessários
Atualizar o status do usuário no seu BD
Não armazenar dados sensíveis de verificação a longo prazo
Copy
Ask AI
// No seu manipulador de webhookapp.post('/webhooks/kyc', async (req, res) => { const { event, payload } = req.body; if (event === 'kyc.validation_approved') { // Extrair apenas o que você precisa await db.users.update({ where: { externalId: payload.entity.externalId }, data: { kycStatus: 'approved', kycVerifiedAt: new Date(), // Não armazenar extractedData a menos que seja necessário } }); } res.status(200).send('OK');});
// ❌ ERRADO: Expondo credenciais no código do clienteconst response = await fetch('https://api.gu1.ai/kyc/validations', { headers: { 'x-api-key': 'gsk_live_abc123...', // NUNCA FAÇA ISSO 'x-workflow-id': 'workflow-id' // NUNCA FAÇA ISSO }});
Por que é perigoso:
Chaves de API expostas em bundles de app podem ser extraídas
Qualquer pessoa pode criar validações em seu nome
Impossível rotacionar chaves comprometidas sem atualizações do app
Não reutilizar URLs de verificação
Copy
Ask AI
// ❌ ERRADO: Armazenar URL permanentementelocalStorage.setItem('kycUrl', verificationUrl);// Mais tarde...window.open(localStorage.getItem('kycUrl')); // A URL pode ter expirado
Em vez disso: Sempre solicite uma URL nova do seu backend quando necessário.
Não confiar em validação do lado do cliente
Copy
Ask AI
// ❌ ERRADO: Confiar no cliente para reportar conclusãofunction onVerificationComplete() { // O usuário poderia falsificar isso updateUserStatus('approved');}
Em vez disso: Confie apenas em webhooks recebidos pelo seu backend.
import UIKitimport WebKitclass KYCViewController: UIViewController { private var webView: WKWebView! private let userId: String private var pollingTimer: Timer? init(userId: String) { self.userId = userId super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() // Configurar WebView let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] webView = WKWebView(frame: view.bounds, configuration: configuration) webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(webView) // Carregar URL de verificação loadVerificationURL() } private func loadVerificationURL() { Task { do { let urlString = try await apiClient.requestKYCURL(userId: userId) guard let url = URL(string: urlString) else { return } let request = URLRequest(url: url) webView.load(request) // Iniciar polling startStatusPolling() } catch { showError("Failed to load verification") } } } private func startStatusPolling() { pollingTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in guard let self = self else { return } Task { do { let status = try await self.apiClient.getKYCStatus(userId: self.userId) switch status { case "approved": self.handleApproved() case "rejected": self.handleRejected() default: break // Continuar polling } } catch { // Tratar erro } } } } private func handleApproved() { pollingTimer?.invalidate() // Mostrar sucesso e fechar dismiss(animated: true) } private func handleRejected() { pollingTimer?.invalidate() // Mostrar mensagem de rejeição } deinit { pollingTimer?.invalidate() }}
Copy
Ask AI
import SwiftUIimport WebKitstruct KYCVerificationView: View { let userId: String @State private var verificationURL: URL? @State private var isLoading = true @State private var status: String = "pending" @Environment(\.dismiss) var dismiss var body: some View { ZStack { if isLoading { ProgressView("Loading verification...") } else if let url = verificationURL { WebView(url: url) } } .onAppear { loadVerificationURL() startStatusPolling() } } private func loadVerificationURL() { Task { do { let urlString = try await apiClient.requestKYCURL(userId: userId) verificationURL = URL(string: urlString) isLoading = false } catch { // Tratar erro } } } private func startStatusPolling() { Task { while status == "pending" || status == "in_progress" { try? await Task.sleep(nanoseconds: 3_000_000_000) // 3 segundos do { status = try await apiClient.getKYCStatus(userId: userId) if status == "approved" || status == "rejected" { dismiss() return } } catch { // Tratar erro } } } }}struct WebView: UIViewRepresentable { let url: URL func makeUIView(context: Context) -> WKWebView { let configuration = WKWebViewConfiguration() configuration.allowsInlineMediaPlayback = true configuration.mediaTypesRequiringUserActionForPlayback = [] return WKWebView(frame: .zero, configuration: configuration) } func updateUIView(_ webView: WKWebView, context: Context) { let request = URLRequest(url: url) webView.load(request) }}
Permissões (Info.plist):
Copy
Ask AI
<key>NSCameraUsageDescription</key><string>We need camera access for identity verification</string><key>NSPhotoLibraryUsageDescription</key><string>We need photo library access for document upload</string>
Perfeito para compartilhar lógica entre Android e iOS:
Common Code
Android Implementation
iOS Implementation
Copy
Ask AI
// commonMain/KYCManager.ktexpect class KYCManager { fun openVerification(url: String) fun startStatusPolling(userId: String, onComplete: (String) -> Unit)}// Lógica de negócio compartilhadaclass KYCCoordinator( private val apiClient: ApiClient, private val kycManager: KYCManager) { suspend fun startVerification(userId: String) { try { val url = apiClient.requestKYCUrl(userId) kycManager.openVerification(url) kycManager.startStatusPolling(userId) { status -> when (status) { "approved" -> handleApproved() "rejected" -> handleRejected() } } } catch (e: Exception) { // Tratar erro } } private fun handleApproved() { // Lógica de aprovação compartilhada } private fun handleRejected() { // Lógica de rejeição compartilhada }}
Copy
Ask AI
// androidMain/KYCManager.ktactual class KYCManager(private val activity: Activity) { actual fun openVerification(url: String) { val intent = Intent(activity, KYCWebViewActivity::class.java).apply { putExtra("url", url) } activity.startActivity(intent) } actual fun startStatusPolling(userId: String, onComplete: (String) -> Unit) { // Usar coroutines para polling CoroutineScope(Dispatchers.Main).launch { while (true) { delay(3000) val status = apiClient.getKYCStatus(userId) if (status in listOf("approved", "rejected")) { onComplete(status) break } } } }}
Copy
Ask AI
// iosMain/KYCManager.ktactual class KYCManager { actual fun openVerification(url: String) { let vc = KYCViewController(urlString: url) // Apresentar view controller UIApplication.shared.windows.first?.rootViewController? .present(vc, animated: true) } actual fun startStatusPolling(userId: String, onComplete: (String) -> Unit) { Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in Task { let status = try await apiClient.getKYCStatus(userId: userId) if status == "approved" || status == "rejected" { onComplete(status) timer.invalidate() } } } }}
O WebView/iframe não pode notificar diretamente seu app quando a verificação é concluída. A UI de verificação da Gu1 é executada em isolamento por razões de segurança.
async function pollStatus(userId) { const interval = setInterval(async () => { const response = await fetch(`/api/kyc/status/${userId}`); const { status } = await response.json(); if (status === 'approved' || status === 'rejected') { clearInterval(interval); handleComplete(status); } }, 3000); // 3 segundos}
Copy
Ask AI
fun pollStatus(userId: String) { lifecycleScope.launch { while (true) { delay(3000) val status = apiClient.getKYCStatus(userId) if (status in listOf("approved", "rejected")) { handleComplete(status) break } } }}
Copy
Ask AI
func pollStatus(userId: String) { Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in Task { let status = try await apiClient.getKYCStatus(userId: userId) if status == "approved" || status == "rejected" { timer.invalidate() handleComplete(status) } } }}