> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gu1.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Guia de Integração do Lado do Cliente

> Guia completo para integrar verificação KYC em aplicativos móveis e aplicações web — na API KYC da gu1 para fluxos de verificação de identidade.

## Visão Geral

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.

<Note>
  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](/pt/use-cases/kyc/overview).
</Note>

## Visão Geral da Arquitetura

```mermaid theme={null}
sequenceDiagram
    participant User
    participant YourApp as Seu Frontend<br/>(Móvel/Web)
    participant YourBackend as Seu Backend
    participant Gu1API as API da Gu1
    participant Gu1UI as UI de Verificação da Gu1

    User->>YourApp: Iniciar verificação KYC
    YourApp->>YourBackend: Solicitar URL de verificação
    YourBackend->>Gu1API: POST /kyc/validations<br/>(com dados da entidade)
    Gu1API-->>YourBackend: Retorna validation_url
    YourBackend-->>YourApp: Retorna URL segura
    YourApp->>Gu1UI: Abrir WebView/iframe com URL
    User->>Gu1UI: Completar verificação<br/>(documento + selfie)
    Gu1UI-->>User: Mostrar mensagem de conclusão
    Gu1API->>YourBackend: Enviar webhook (aprovado/rejeitado)
    YourBackend->>YourBackend: Atualizar status do usuário
    YourApp->>YourBackend: Polling para atualizações de status
    YourBackend-->>YourApp: Retornar resultado da verificação
    YourApp->>User: Mostrar resultado final
```

**Pontos-Chave:**

* Seu **backend** gera a URL de verificação (nunca exponha chaves de API no cliente)
* Seu **frontend** abre a URL no WebView/iframe
* A Gu1 envia **webhooks** para seu backend quando a verificação é concluída
* Seu **frontend** faz polling no seu backend para detectar a conclusão

***

## Melhores Práticas de Segurança

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

### ✅ FAZER

<AccordionGroup>
  <Accordion title="Gerar URLs sob demanda a partir do seu backend">
    ```javascript theme={null}
    // ✅ CORRETO: Frontend solicita URL do SEU backend
    const response = await fetch('https://yourapi.com/kyc/start', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${userToken}`, // Sua autenticação
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        userId: '123'
      })
    });

    const { verificationUrl } = await response.json();
    // Abrir verificationUrl no WebView/iframe
    ```
  </Accordion>

  <Accordion title="Implementar TTL curto para URLs">
    Gere URLs de verificação que expirem rapidamente (15-30 minutos). Se o usuário não iniciar dentro desse tempo, gere uma nova.

    ```javascript theme={null}
    // No seu backend
    const validation = await gu1.createValidation({
      // ... dados da entidade
    });

    // Armazenar com expiração
    await redis.setex(
      `kyc:${userId}`,
      1800, // TTL de 30 minutos
      validation.validation_url
    );
    ```
  </Accordion>

  <Accordion title="Usar padrão Process-and-Purge">
    Ao receber webhooks da Gu1:

    1. Extrair apenas os dados necessários
    2. Atualizar o status do usuário no seu BD
    3. Não armazenar dados sensíveis de verificação a longo prazo

    ```javascript theme={null}
    // No seu manipulador de webhook
    app.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');
    });
    ```
  </Accordion>

  <Accordion title="Validar assinaturas de webhook">
    Sempre verifique se os webhooks são da Gu1 usando assinaturas HMAC.

    [Saiba mais sobre segurança de webhooks →](/pt/webhooks/security)
  </Accordion>
</AccordionGroup>

### ❌ NÃO FAZER

<AccordionGroup>
  <Accordion title="Nunca codificar chaves de API ou IDs de workflow">
    ```javascript theme={null}
    // ❌ ERRADO: Expondo credenciais no código do cliente
    const 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
  </Accordion>

  <Accordion title="Não reutilizar URLs de verificação">
    ```javascript theme={null}
    // ❌ ERRADO: Armazenar URL permanentemente
    localStorage.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.
  </Accordion>

  <Accordion title="Não confiar em validação do lado do cliente">
    ```javascript theme={null}
    // ❌ ERRADO: Confiar no cliente para reportar conclusão
    function onVerificationComplete() {
      // O usuário poderia falsificar isso
      updateUserStatus('approved');
    }
    ```

    **Em vez disso:** Confie apenas em webhooks recebidos pelo seu backend.
  </Accordion>
</AccordionGroup>

***

## Integração Móvel

### Android (Kotlin)

<Tabs>
  <Tab title="Standard Android">
    ```kotlin theme={null}
    import android.webkit.WebView
    import android.webkit.WebViewClient
    import androidx.appcompat.app.AppCompatActivity

    class KYCActivity : AppCompatActivity() {
        private lateinit var webView: WebView

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            webView = WebView(this).apply {
                settings.apply {
                    javaScriptEnabled = true
                    domStorageEnabled = true
                    mediaPlaybackRequiresUserGesture = false // Para acesso à câmera
                }

                webViewClient = object : WebViewClient() {
                    override fun onPageFinished(view: WebView?, url: String?) {
                        // Página carregada
                    }
                }
            }

            setContentView(webView)

            // Solicitar URL de verificação do SEU backend
            loadVerificationUrl()
        }

        private fun loadVerificationUrl() {
            lifecycleScope.launch {
                try {
                    val url = apiClient.requestKYCUrl(userId)
                    webView.loadUrl(url)

                    // Iniciar polling para conclusão
                    startStatusPolling()
                } catch (e: Exception) {
                    // Tratar erro
                    showError("Failed to load verification")
                }
            }
        }

        private fun startStatusPolling() {
            lifecycleScope.launch {
                while (true) {
                    delay(3000) // Polling a cada 3 segundos

                    val status = apiClient.getKYCStatus(userId)
                    when (status) {
                        "approved" -> {
                            showSuccess()
                            finish()
                            return@launch
                        }
                        "rejected" -> {
                            showRejected()
                            finish()
                            return@launch
                        }
                        "pending", "in_progress" -> {
                            // Continuar polling
                        }
                    }
                }
            }
        }

        override fun onDestroy() {
            webView.destroy()
            super.onDestroy()
        }
    }
    ```
  </Tab>

  <Tab title="Jetpack Compose">
    ```kotlin theme={null}
    import androidx.compose.runtime.*
    import androidx.compose.ui.viewinterop.AndroidView
    import android.webkit.WebView

    @Composable
    fun KYCVerificationScreen(
        userId: String,
        onComplete: (status: String) -> Unit
    ) {
        var verificationUrl by remember { mutableStateOf<String?>(null) }
        var isLoading by remember { mutableStateOf(true) }

        // Carregar URL de verificação
        LaunchedEffect(userId) {
            try {
                val url = apiClient.requestKYCUrl(userId)
                verificationUrl = url
                isLoading = false
            } catch (e: Exception) {
                // Tratar erro
            }
        }

        // Polling para status
        LaunchedEffect(userId) {
            while (true) {
                delay(3000) // Polling a cada 3 segundos

                val status = apiClient.getKYCStatus(userId)
                when (status) {
                    "approved", "rejected" -> {
                        onComplete(status)
                        return@LaunchedEffect
                    }
                }
            }
        }

        if (isLoading) {
            CircularProgressIndicator()
        } else {
            verificationUrl?.let { url ->
                AndroidView(
                    factory = { context ->
                        WebView(context).apply {
                            settings.apply {
                                javaScriptEnabled = true
                                domStorageEnabled = true
                                mediaPlaybackRequiresUserGesture = false
                            }
                            loadUrl(url)
                        }
                    }
                )
            }
        }
    }
    ```
  </Tab>
</Tabs>

**Permissões (AndroidManifest.xml):**

```xml theme={null}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
```

***

### iOS (Swift)

<Tabs>
  <Tab title="UIKit">
    ```swift theme={null}
    import UIKit
    import WebKit

    class 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()
        }
    }
    ```
  </Tab>

  <Tab title="SwiftUI">
    ```swift theme={null}
    import SwiftUI
    import WebKit

    struct 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)
        }
    }
    ```
  </Tab>
</Tabs>

**Permissões (Info.plist):**

```xml theme={null}
<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>
```

***

### Kotlin Multiplatform (KMP)

Perfeito para compartilhar lógica entre Android e iOS:

<Tabs>
  <Tab title="Common Code">
    ```kotlin theme={null}
    // commonMain/KYCManager.kt
    expect class KYCManager {
        fun openVerification(url: String)
        fun startStatusPolling(userId: String, onComplete: (String) -> Unit)
    }

    // Lógica de negócio compartilhada
    class 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
        }
    }
    ```
  </Tab>

  <Tab title="Android Implementation">
    ```kotlin theme={null}
    // androidMain/KYCManager.kt
    actual 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
                    }
                }
            }
        }
    }
    ```
  </Tab>

  <Tab title="iOS Implementation">
    ```swift theme={null}
    // iosMain/KYCManager.kt
    actual 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()
                    }
                }
            }
        }
    }
    ```
  </Tab>
</Tabs>

***

### React Native

```typescript theme={null}
import React, { useEffect, useState } from 'react';
import { View, ActivityIndicator, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';

interface KYCVerificationProps {
  userId: string;
  onComplete: (status: string) => void;
}

export const KYCVerification: React.FC<KYCVerificationProps> = ({
  userId,
  onComplete
}) => {
  const [verificationUrl, setVerificationUrl] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    loadVerificationUrl();
    const pollInterval = startStatusPolling();

    return () => clearInterval(pollInterval);
  }, [userId]);

  const loadVerificationUrl = async () => {
    try {
      const response = await fetch(`https://yourapi.com/kyc/start`, {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${userToken}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ userId })
      });

      const { verificationUrl } = await response.json();
      setVerificationUrl(verificationUrl);
      setIsLoading(false);
    } catch (error) {
      console.error('Failed to load KYC URL:', error);
      setIsLoading(false);
    }
  };

  const startStatusPolling = () => {
    return setInterval(async () => {
      try {
        const response = await fetch(`https://yourapi.com/kyc/status/${userId}`, {
          headers: {
            'Authorization': `Bearer ${userToken}`
          }
        });

        const { status } = await response.json();

        if (status === 'approved' || status === 'rejected') {
          onComplete(status);
        }
      } catch (error) {
        console.error('Polling error:', error);
      }
    }, 3000); // Polling a cada 3 segundos
  };

  if (isLoading) {
    return (
      <View style={styles.loading}>
        <ActivityIndicator size="large" />
      </View>
    );
  }

  if (!verificationUrl) {
    return (
      <View style={styles.error}>
        <Text>Failed to load verification</Text>
      </View>
    );
  }

  return (
    <WebView
      source={{ uri: verificationUrl }}
      style={styles.webview}
      javaScriptEnabled={true}
      domStorageEnabled={true}
      mediaPlaybackRequiresUserAction={false} // Para câmera
      allowsInlineMediaPlayback={true}
    />
  );
};

const styles = StyleSheet.create({
  webview: {
    flex: 1
  },
  loading: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  },
  error: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center'
  }
});
```

**Permissões:**

* Adicione permissões de câmera ao `AndroidManifest.xml` e `Info.plist`
* Instalar: `npm install react-native-webview`

***

### Flutter

```dart theme={null}
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'dart:async';

class KYCVerificationScreen extends StatefulWidget {
  final String userId;
  final Function(String) onComplete;

  const KYCVerificationScreen({
    Key? key,
    required this.userId,
    required this.onComplete,
  }) : super(key: key);

  @override
  _KYCVerificationScreenState createState() => _KYCVerificationScreenState();
}

class _KYCVerificationScreenState extends State<KYCVerificationScreen> {
  String? verificationUrl;
  bool isLoading = true;
  Timer? pollingTimer;
  late WebViewController controller;

  @override
  void initState() {
    super.initState();
    _loadVerificationUrl();
    _startStatusPolling();
  }

  @override
  void dispose() {
    pollingTimer?.cancel();
    super.dispose();
  }

  Future<void> _loadVerificationUrl() async {
    try {
      final response = await http.post(
        Uri.parse('https://yourapi.com/kyc/start'),
        headers: {
          'Authorization': 'Bearer $userToken',
          'Content-Type': 'application/json',
        },
        body: jsonEncode({'userId': widget.userId}),
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        setState(() {
          verificationUrl = data['verificationUrl'];
          isLoading = false;
        });

        // Inicializar WebView controller
        controller = WebViewController()
          ..setJavaScriptMode(JavaScriptMode.unrestricted)
          ..setBackgroundColor(const Color(0x00000000))
          ..loadRequest(Uri.parse(verificationUrl!));
      }
    } catch (e) {
      print('Error loading KYC URL: $e');
      setState(() => isLoading = false);
    }
  }

  void _startStatusPolling() {
    pollingTimer = Timer.periodic(Duration(seconds: 3), (timer) async {
      try {
        final response = await http.get(
          Uri.parse('https://yourapi.com/kyc/status/${widget.userId}'),
          headers: {'Authorization': 'Bearer $userToken'},
        );

        if (response.statusCode == 200) {
          final data = jsonDecode(response.body);
          final status = data['status'];

          if (status == 'approved' || status == 'rejected') {
            timer.cancel();
            widget.onComplete(status);
          }
        }
      } catch (e) {
        print('Polling error: $e');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    if (isLoading) {
      return Scaffold(
        body: Center(
          child: CircularProgressIndicator(),
        ),
      );
    }

    if (verificationUrl == null) {
      return Scaffold(
        body: Center(
          child: Text('Failed to load verification'),
        ),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('Identity Verification'),
      ),
      body: WebViewWidget(controller: controller),
    );
  }
}
```

**Dependências (pubspec.yaml):**

```yaml theme={null}
dependencies:
  webview_flutter: ^4.4.0
  http: ^1.1.0
```

**Permissões:**

* Android: Adicione permissão de câmera ao `AndroidManifest.xml`
* iOS: Adicione descrição de uso de câmera ao `Info.plist`

***

## Integração Web

### React

```typescript theme={null}
import React, { useEffect, useState, useRef } from 'react';

interface KYCVerificationProps {
  userId: string;
  onComplete: (status: string) => void;
}

export const KYCVerification: React.FC<KYCVerificationProps> = ({
  userId,
  onComplete
}) => {
  const [verificationUrl, setVerificationUrl] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const pollingInterval = useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    loadVerificationUrl();
    startStatusPolling();

    return () => {
      if (pollingInterval.current) {
        clearInterval(pollingInterval.current);
      }
    };
  }, [userId]);

  const loadVerificationUrl = async () => {
    try {
      const response = await fetch('/api/kyc/start', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${getAuthToken()}`
        },
        body: JSON.stringify({ userId })
      });

      const { verificationUrl } = await response.json();
      setVerificationUrl(verificationUrl);
      setIsLoading(false);
    } catch (error) {
      console.error('Failed to load KYC URL:', error);
      setIsLoading(false);
    }
  };

  const startStatusPolling = () => {
    pollingInterval.current = setInterval(async () => {
      try {
        const response = await fetch(`/api/kyc/status/${userId}`, {
          headers: {
            'Authorization': `Bearer ${getAuthToken()}`
          }
        });

        const { status } = await response.json();

        if (status === 'approved' || status === 'rejected') {
          if (pollingInterval.current) {
            clearInterval(pollingInterval.current);
          }
          onComplete(status);
        }
      } catch (error) {
        console.error('Polling error:', error);
      }
    }, 3000); // Polling a cada 3 segundos
  };

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-screen">
        <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
      </div>
    );
  }

  if (!verificationUrl) {
    return (
      <div className="flex items-center justify-center h-screen">
        <p className="text-red-500">Failed to load verification</p>
      </div>
    );
  }

  return (
    <div className="w-full h-screen">
      <iframe
        ref={iframeRef}
        src={verificationUrl}
        className="w-full h-full border-0"
        allow="camera; microphone"
        title="KYC Verification"
      />
    </div>
  );
};
```

***

### Vue 3

```vue theme={null}
<template>
  <div class="kyc-verification">
    <div v-if="isLoading" class="loading">
      <div class="spinner"></div>
      <p>Loading verification...</p>
    </div>

    <div v-else-if="error" class="error">
      <p>{{ error }}</p>
    </div>

    <iframe
      v-else-if="verificationUrl"
      :src="verificationUrl"
      class="verification-iframe"
      allow="camera; microphone"
      title="KYC Verification"
    ></iframe>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';

interface Props {
  userId: string;
}

const props = defineProps<Props>();
const emit = defineEmits<{
  complete: [status: string];
}>();

const verificationUrl = ref<string | null>(null);
const isLoading = ref(true);
const error = ref<string | null>(null);
let pollingInterval: number | null = null;

onMounted(async () => {
  await loadVerificationUrl();
  startStatusPolling();
});

onUnmounted(() => {
  if (pollingInterval) {
    clearInterval(pollingInterval);
  }
});

async function loadVerificationUrl() {
  try {
    const response = await fetch('/api/kyc/start', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${getAuthToken()}`
      },
      body: JSON.stringify({ userId: props.userId })
    });

    if (!response.ok) throw new Error('Failed to load verification URL');

    const data = await response.json();
    verificationUrl.value = data.verificationUrl;
  } catch (err) {
    error.value = 'Failed to load verification';
    console.error(err);
  } finally {
    isLoading.value = false;
  }
}

function startStatusPolling() {
  pollingInterval = setInterval(async () => {
    try {
      const response = await fetch(`/api/kyc/status/${props.userId}`, {
        headers: {
          'Authorization': `Bearer ${getAuthToken()}`
        }
      });

      const { status } = await response.json();

      if (status === 'approved' || status === 'rejected') {
        if (pollingInterval) clearInterval(pollingInterval);
        emit('complete', status);
      }
    } catch (err) {
      console.error('Polling error:', err);
    }
  }, 3000);
}

function getAuthToken(): string {
  // Sua lógica de token de autenticação
  return localStorage.getItem('authToken') || '';
}
</script>

<style scoped>
.kyc-verification {
  width: 100%;
  height: 100vh;
}

.loading,
.error {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.spinner {
  width: 50px;
  height: 50px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.verification-iframe {
  width: 100%;
  height: 100%;
  border: none;
}

.error {
  color: #e74c3c;
}
</style>
```

***

### Plain HTML/JavaScript (Vanilla)

```html theme={null}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>KYC Verification</title>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            height: 100%;
            font-family: Arial, sans-serif;
        }

        #loading {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            height: 100vh;
        }

        .spinner {
            width: 50px;
            height: 50px;
            border: 4px solid #f3f3f3;
            border-top: 4px solid #3498db;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        #verification-iframe {
            width: 100%;
            height: 100vh;
            border: none;
            display: none;
        }

        #error {
            display: none;
            text-align: center;
            padding: 20px;
            color: #e74c3c;
        }
    </style>
</head>
<body>
    <div id="loading">
        <div class="spinner"></div>
        <p>Loading verification...</p>
    </div>

    <div id="error">
        <p>Failed to load verification. Please try again.</p>
    </div>

    <iframe
        id="verification-iframe"
        allow="camera; microphone"
        title="KYC Verification"
    ></iframe>

    <script>
        const userId = 'USER_ID_HERE'; // Substituir com ID de usuário real
        let pollingInterval;

        async function loadVerificationUrl() {
            try {
                const response = await fetch('/api/kyc/start', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${getAuthToken()}`
                    },
                    body: JSON.stringify({ userId })
                });

                if (!response.ok) throw new Error('Failed to load');

                const { verificationUrl } = await response.json();

                // Mostrar iframe e ocultar loading
                document.getElementById('loading').style.display = 'none';
                const iframe = document.getElementById('verification-iframe');
                iframe.src = verificationUrl;
                iframe.style.display = 'block';

                // Iniciar polling para status
                startStatusPolling();
            } catch (error) {
                console.error('Error:', error);
                document.getElementById('loading').style.display = 'none';
                document.getElementById('error').style.display = 'block';
            }
        }

        function startStatusPolling() {
            pollingInterval = setInterval(async () => {
                try {
                    const response = await fetch(`/api/kyc/status/${userId}`, {
                        headers: {
                            'Authorization': `Bearer ${getAuthToken()}`
                        }
                    });

                    const { status } = await response.json();

                    if (status === 'approved') {
                        clearInterval(pollingInterval);
                        handleApproved();
                    } else if (status === 'rejected') {
                        clearInterval(pollingInterval);
                        handleRejected();
                    }
                } catch (error) {
                    console.error('Polling error:', error);
                }
            }, 3000); // Polling a cada 3 segundos
        }

        function handleApproved() {
            alert('Verification approved!');
            window.location.href = '/dashboard'; // Redirecionar
        }

        function handleRejected() {
            alert('Verification rejected. Please try again.');
            window.location.href = '/kyc-retry'; // Redirecionar
        }

        function getAuthToken() {
            // Obter token de autenticação do localStorage, cookie, etc.
            return localStorage.getItem('authToken');
        }

        // Iniciar carregamento quando a página carregar
        window.addEventListener('DOMContentLoaded', loadVerificationUrl);

        // Limpeza ao descarregar a página
        window.addEventListener('beforeunload', () => {
            if (pollingInterval) {
                clearInterval(pollingInterval);
            }
        });
    </script>
</body>
</html>
```

***

## Detecção de Conclusão do Fluxo

<Warning>
  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.
</Warning>

### Padrão Recomendado: Polling ao Backend

Seu frontend faz polling no seu backend, que recebe webhooks da Gu1:

```mermaid theme={null}
sequenceDiagram
    participant User
    participant Frontend
    participant YourBackend
    participant Gu1

    User->>Gu1: Concluir verificação
    Gu1->>YourBackend: Webhook (aprovado/rejeitado)
    YourBackend->>YourBackend: Atualizar status do usuário no BD

    loop A cada 3 segundos
        Frontend->>YourBackend: Polling: GET /kyc/status/:userId
        YourBackend-->>Frontend: Retornar status atual
    end

    Frontend->>Frontend: Detectar mudança de status
    Frontend->>User: Mostrar resultado
```

**Exemplo de endpoint do backend:**

```javascript theme={null}
// Exemplo Node.js/Express
app.get('/api/kyc/status/:userId', authenticate, async (req, res) => {
  const { userId } = req.params;

  const user = await db.users.findOne({ id: userId });

  res.json({
    status: user.kycStatus, // 'pending', 'in_progress', 'approved', 'rejected'
    verifiedAt: user.kycVerifiedAt,
    lastUpdated: user.kycLastUpdated
  });
});
```

**Polling do frontend (todas as plataformas):**

<Tabs>
  <Tab title="JavaScript">
    ```javascript theme={null}
    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
    }
    ```
  </Tab>

  <Tab title="Kotlin">
    ```kotlin theme={null}
    fun pollStatus(userId: String) {
      lifecycleScope.launch {
        while (true) {
          delay(3000)
          val status = apiClient.getKYCStatus(userId)
          if (status in listOf("approved", "rejected")) {
            handleComplete(status)
            break
          }
        }
      }
    }
    ```
  </Tab>

  <Tab title="Swift">
    ```swift theme={null}
    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)
          }
        }
      }
    }
    ```
  </Tab>
</Tabs>

### Alternativa: WebSockets (Avançado)

Para melhor UX, use WebSockets para enviar atualizações em vez de polling:

```javascript theme={null}
// Backend (exemplo Socket.IO)
io.on('connection', (socket) => {
  socket.on('subscribe-kyc', (userId) => {
    socket.join(`kyc:${userId}`);
  });
});

// Quando o webhook chega
app.post('/webhooks/kyc', async (req, res) => {
  const { event, payload } = req.body;

  // Atualizar banco de dados
  await updateUserStatus(payload);

  // Notificar clientes conectados
  io.to(`kyc:${payload.entity.externalId}`).emit('kyc-update', {
    status: payload.status
  });

  res.status(200).send('OK');
});

// Frontend
const socket = io();
socket.emit('subscribe-kyc', userId);
socket.on('kyc-update', ({ status }) => {
  handleComplete(status);
});
```

***

## Manipulação de Webhooks

Seu backend recebe webhooks da Gu1 quando a verificação é concluída.

<Card title="Guia de Integração de Webhooks" icon="webhook" href="/pt/webhooks/events/kyc-events">
  Veja o guia completo de integração de webhooks com exemplos de código, segurança e estruturas de payload
</Card>

### Eventos-Chave de Webhook

| Evento                     | Quando Disparado               | Ação                                                                    |
| -------------------------- | ------------------------------ | ----------------------------------------------------------------------- |
| `kyc.validation_approved`  | Usuário verificado com sucesso | Atualizar status do usuário para `verified`, habilitar recursos         |
| `kyc.validation_rejected`  | Verificação falhou             | Atualizar status para `rejected`, opcionalmente permitir nova tentativa |
| `kyc.validation_abandoned` | Usuário saiu sem concluir      | Marcar como `incomplete`, enviar e-mail de lembrete                     |
| `kyc.validation_expired`   | Sessão expirou                 | Permitir ao usuário solicitar nova URL                                  |

**Manipulador mínimo de webhook:**

```javascript theme={null}
app.post('/webhooks/gu1/kyc', async (req, res) => {
  // Verificar assinatura (ver guia de webhooks)
  const { event, payload } = req.body;

  const userId = payload.entity.externalId;

  switch (event) {
    case 'kyc.validation_approved':
      await db.users.update({
        where: { id: userId },
        data: {
          kycStatus: 'approved',
          kycVerifiedAt: new Date(),
          isVerified: true
        }
      });
      break;

    case 'kyc.validation_rejected':
      await db.users.update({
        where: { id: userId },
        data: {
          kycStatus: 'rejected',
          isVerified: false
        }
      });
      break;
  }

  res.status(200).send('OK');
});
```

***

## Exemplo de Fluxo Completo

Vamos ver um exemplo completo de ponta a ponta:

### 1. Backend: Gerar URL

```javascript theme={null}
// POST /api/kyc/start
app.post('/api/kyc/start', authenticate, async (req, res) => {
  const { userId } = req.body;
  const user = await db.users.findOne({ id: userId });

  // Garantir que a entidade pessoa existe na Gu1 (criar ou obter por externalId/taxId)
  let entityId = user.gu1EntityId;
  if (!entityId) {
    const entityRes = await gu1.post('/entities', {
      type: 'person',
      externalId: user.id,
      name: user.name,
      taxId: user.taxId,
      countryCode: user.countryCode || 'US'
    });
    entityId = entityRes.entity?.id ?? entityRes.id;
    await db.users.update({ where: { id: userId }, data: { gu1EntityId: entityId } });
  }

  // Criar validação KYC (entityId + integrationCode)
  const validation = await gu1.post('/kyc/validations', {
    entityId,
    integrationCode: 'global_gueno_validation_kyc'
  });

  await db.users.update({
    where: { id: userId },
    data: {
      kycValidationId: validation.id,
      kycStatus: 'pending'
    }
  });

  res.json({
    verificationUrl: validation.providerSessionUrl
  });
});
```

### 2. Frontend: Abrir Verificação

```javascript theme={null}
// Usuário clica em "Verify Identity"
async function startVerification() {
  const { verificationUrl } = await fetch('/api/kyc/start', {
    method: 'POST',
    body: JSON.stringify({ userId: currentUser.id })
  }).then(r => r.json());

  // Abrir no WebView/iframe
  openVerificationUI(verificationUrl);

  // Iniciar polling
  pollForCompletion();
}
```

### 3. Backend: Receber Webhook

```javascript theme={null}
// POST /webhooks/gu1/kyc
app.post('/webhooks/gu1/kyc', async (req, res) => {
  const { event, payload } = req.body;

  if (event === 'kyc.validation_approved') {
    await db.users.update({
      where: { externalId: payload.entity.externalId },
      data: {
        kycStatus: 'approved',
        isVerified: true
      }
    });
  }

  res.status(200).send('OK');
});
```

### 4. Frontend: Detectar Conclusão

```javascript theme={null}
async function pollForCompletion() {
  const interval = setInterval(async () => {
    const { status } = await fetch(`/api/kyc/status/${userId}`)
      .then(r => r.json());

    if (status === 'approved') {
      clearInterval(interval);
      showSuccess('Identity verified!');
    } else if (status === 'rejected') {
      clearInterval(interval);
      showError('Verification failed');
    }
  }, 3000);
}
```

***

## Testes

### Ambiente Sandbox

Use o modo sandbox para testes:

```javascript theme={null}
// Backend
const gu1 = new Gu1Client({
  apiKey: process.env.GU1_SANDBOX_API_KEY,
  environment: 'sandbox' // Usar sandbox para testes
});
```

No modo sandbox:

* Nenhum documento real é necessário
* Você pode simular diferentes resultados
* Webhooks ainda disparam normalmente

### Testando Diferentes Resultados

Para testar cenários de rejeição/expiração, use diferentes dados de teste no modo sandbox.

***

## Solução de Problemas

<AccordionGroup>
  <Accordion title="WebView mostra página em branco">
    **Possíveis causas:**

    * URL expirou (gerar uma nova)
    * JavaScript desabilitado no WebView
    * Problemas de conectividade de rede

    **Soluções:**

    * Habilitar JavaScript: `settings.javaScriptEnabled = true`
    * Verificar se a URL é válida e não expirou
    * Testar URL em navegador normal primeiro
  </Accordion>

  <Accordion title="Câmera não funciona no WebView">
    **Possíveis causas:**

    * Permissões de câmera faltando
    * Reprodução de mídia requer gesto do usuário

    **Soluções:**

    **Android:**

    ```xml theme={null}
    <uses-permission android:name="android.permission.CAMERA" />
    ```

    **iOS:**

    ```swift theme={null}
    configuration.mediaTypesRequiringUserActionForPlayback = []
    ```

    **Web:**

    ```html theme={null}
    <iframe allow="camera; microphone" />
    ```
  </Accordion>

  <Accordion title="Polling nunca detecta conclusão">
    **Possíveis causas:**

    * Webhook não recebido pelo backend
    * Verificação de assinatura de webhook falhou
    * Banco de dados não está sendo atualizado

    **Soluções:**

    * Verificar logs de webhook no painel da Gu1
    * Verificar implementação de assinatura de webhook
    * Adicionar logging ao manipulador de webhook
    * Testar endpoint de webhook manualmente
  </Accordion>

  <Accordion title="Usuário preso na verificação">
    **Possíveis causas:**

    * Iluminação fraca para fotos de documentos
    * Tipo de documento não suportado
    * Problemas técnicos

    **Soluções:**

    * Fornecer instruções claras antes de começar
    * Mostrar exemplos de fotos boas vs ruins
    * Implementar timeout (15-20 minutos)
    * Permitir ao usuário sair e tentar novamente
  </Accordion>
</AccordionGroup>

***

## Resumo de Melhores Práticas

<CardGroup cols={2}>
  <Card title="Segurança em Primeiro Lugar" icon="shield-check">
    * Nunca expor chaves de API no cliente
    * Sempre gerar URLs a partir do backend
    * Verificar assinaturas de webhook
    * Usar HTTPS em todos os lugares
  </Card>

  <Card title="Experiência do Usuário" icon="face-smile">
    * Mostrar estados de carregamento
    * Fornecer instruções claras
    * Tratar erros com elegância
    * Permitir nova tentativa em caso de falha
  </Card>

  <Card title="Confiabilidade" icon="chart-line">
    * Implementar polling com intervalos razoáveis
    * Tratar falhas de rede
    * Definir timeouts apropriados
    * Testar todas as plataformas completamente
  </Card>

  <Card title="Conformidade" icon="scale-balanced">
    * Não armazenar dados sensíveis de verificação
    * Usar padrão Process-and-Purge
    * Respeitar políticas de retenção de dados
    * Documentar sua integração
  </Card>
</CardGroup>

***

## Próximos Passos

<CardGroup cols={2}>
  <Card title="Visão Geral da API KYC" icon="book" href="/pt/use-cases/kyc/overview">
    Aprenda sobre a API KYC e integração do backend
  </Card>

  <Card title="Integração de Webhooks" icon="webhook" href="/pt/webhooks/events/kyc-events">
    Configure webhooks para receber resultados de verificação
  </Card>

  <Card title="Guia de Fluxo Completo" icon="diagram-project" href="/en/use-cases/kyc/complete-flow">
    Veja o fluxo completo de verificação KYC
  </Card>

  <Card title="Guia de Segurança" icon="lock" href="/pt/webhooks/security">
    Implemente segurança de webhooks e verificação HMAC
  </Card>
</CardGroup>
