> ## 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.

# Guía de Integración del Lado del Cliente

> Guía completa para integrar la verificación KYC en aplicaciones móviles y aplicaciones web — en la API KYC de gu1 para flujos de verificación de identidad.

## Descripción General

Esta guía te muestra cómo integrar el flujo de verificación KYC de Gu1 en tus aplicaciones móviles (Android, iOS, React Native, Flutter) y aplicaciones web (React, Vue, vanilla JS). La verificación se ejecuta en un WebView/iframe que apunta a una URL segura generada por tu backend.

<Note>
  Esta guía asume que ya has configurado la integración de tu backend con la API de Gu1. Si no es así, comienza con la [Descripción General de la API KYC](/es/use-cases/kyc/overview).
</Note>

## Descripción General de la Arquitectura

```mermaid theme={null}
sequenceDiagram
    participant User
    participant YourApp as Tu Frontend<br/>(Móvil/Web)
    participant YourBackend as Tu Backend
    participant Gu1API as API de Gu1
    participant Gu1UI as UI de Verificación de Gu1

    User->>YourApp: Iniciar verificación KYC
    YourApp->>YourBackend: Solicitar URL de verificación
    YourBackend->>Gu1API: POST /kyc/validations<br/>(con datos de entidad)
    Gu1API-->>YourBackend: Devuelve validation_url
    YourBackend-->>YourApp: Devuelve URL segura
    YourApp->>Gu1UI: Abrir WebView/iframe con URL
    User->>Gu1UI: Completar verificación<br/>(documento + selfie)
    Gu1UI-->>User: Mostrar mensaje de finalización
    Gu1API->>YourBackend: Enviar webhook (aprobado/rechazado)
    YourBackend->>YourBackend: Actualizar estado del usuario
    YourApp->>YourBackend: Polling para actualizaciones de estado
    YourBackend-->>YourApp: Devolver resultado de verificación
    YourApp->>User: Mostrar resultado final
```

**Puntos Clave:**

* Tu **backend** genera la URL de verificación (nunca expongas claves de API en el cliente)
* Tu **frontend** abre la URL en WebView/iframe
* Gu1 envía **webhooks** a tu backend cuando la verificación se completa
* Tu **frontend** hace polling a tu backend para detectar la finalización

***

## Mejores Prácticas de Seguridad

<Warning>
  **CRÍTICO**: Nunca expongas las credenciales de la API de Gu1 o los IDs de workflow en tu código del lado del cliente. Siempre genera las URLs de verificación desde tu backend seguro.
</Warning>

### ✅ HACER

<AccordionGroup>
  <Accordion title="Generar URLs bajo demanda desde tu backend">
    ```javascript theme={null}
    // ✅ CORRECTO: Frontend solicita URL desde TU backend
    const response = await fetch('https://yourapi.com/kyc/start', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${userToken}`, // Tu autenticación
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        userId: '123'
      })
    });

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

  <Accordion title="Implementar TTL corto para URLs">
    Genera URLs de verificación que expiren rápidamente (15-30 minutos). Si el usuario no inicia dentro de ese tiempo, genera una nueva.

    ```javascript theme={null}
    // En tu backend
    const validation = await gu1.createValidation({
      // ... datos de entidad
    });

    // Almacenar con expiración
    await redis.setex(
      `kyc:${userId}`,
      1800, // TTL de 30 minutos
      validation.validation_url
    );
    ```
  </Accordion>

  <Accordion title="Usar patrón Process-and-Purge">
    Al recibir webhooks de Gu1:

    1. Extraer solo los datos necesarios
    2. Actualizar el estado del usuario en tu BD
    3. No almacenar datos sensibles de verificación a largo plazo

    ```javascript theme={null}
    // En tu manejador de webhook
    app.post('/webhooks/kyc', async (req, res) => {
      const { event, payload } = req.body;

      if (event === 'kyc.validation_approved') {
        // Extraer solo lo que necesitas
        await db.users.update({
          where: { externalId: payload.entity.externalId },
          data: {
            kycStatus: 'approved',
            kycVerifiedAt: new Date(),
            // No almacenar extractedData a menos que sea necesario
          }
        });
      }

      res.status(200).send('OK');
    });
    ```
  </Accordion>

  <Accordion title="Validar firmas de webhook">
    Siempre verifica que los webhooks provienen de Gu1 usando firmas HMAC.

    [Aprende sobre seguridad de webhooks →](/es/webhooks/security)
  </Accordion>
</AccordionGroup>

### ❌ NO HACER

<AccordionGroup>
  <Accordion title="Nunca codificar en duro claves de API o IDs de workflow">
    ```javascript theme={null}
    // ❌ INCORRECTO: Exponiendo credenciales en código del cliente
    const response = await fetch('https://api.gu1.ai/kyc/validations', {
      headers: {
        'x-api-key': 'gsk_live_abc123...', // NUNCA HAGAS ESTO
        'x-workflow-id': 'workflow-id' // NUNCA HAGAS ESTO
      }
    });
    ```

    **Por qué es peligroso:**

    * Las claves de API expuestas en bundles de app pueden ser extraídas
    * Cualquiera puede crear validaciones en tu nombre
    * Imposible rotar claves comprometidas sin actualizar la app
  </Accordion>

  <Accordion title="No reutilizar URLs de verificación">
    ```javascript theme={null}
    // ❌ INCORRECTO: Almacenar URL permanentemente
    localStorage.setItem('kycUrl', verificationUrl);

    // Más tarde...
    window.open(localStorage.getItem('kycUrl')); // La URL podría haber expirado
    ```

    **En su lugar:** Siempre solicita una URL nueva desde tu backend cuando sea necesario.
  </Accordion>

  <Accordion title="No confiar en validación del lado del cliente">
    ```javascript theme={null}
    // ❌ INCORRECTO: Confiar en que el cliente reporte la finalización
    function onVerificationComplete() {
      // El usuario podría falsificar esto
      updateUserStatus('approved');
    }
    ```

    **En su lugar:** Solo confía en webhooks recibidos por tu backend.
  </Accordion>
</AccordionGroup>

***

## Integración Móvil

### 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 acceso a cámara
                }

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

            setContentView(webView)

            // Solicitar URL de verificación desde TU backend
            loadVerificationUrl()
        }

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

                    // Iniciar polling para finalización
                    startStatusPolling()
                } catch (e: Exception) {
                    // Manejar error
                    showError("Failed to load verification")
                }
            }
        }

        private fun startStatusPolling() {
            lifecycleScope.launch {
                while (true) {
                    delay(3000) // Polling 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) }

        // Cargar URL de verificación
        LaunchedEffect(userId) {
            try {
                val url = apiClient.requestKYCUrl(userId)
                verificationUrl = url
                isLoading = false
            } catch (e: Exception) {
                // Manejar error
            }
        }

        // Polling para estado
        LaunchedEffect(userId) {
            while (true) {
                delay(3000) // Polling 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>

**Permisos (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)

            // Cargar URL de verificación
            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 {
                        // Manejar error
                    }
                }
            }
        }

        private func handleApproved() {
            pollingTimer?.invalidate()
            // Mostrar éxito y cerrar
            dismiss(animated: true)
        }

        private func handleRejected() {
            pollingTimer?.invalidate()
            // Mostrar mensaje de rechazo
        }

        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 {
                    // Manejar error
                }
            }
        }

        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 {
                        // Manejar error
                    }
                }
            }
        }
    }

    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>

**Permisos (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)

Perfecto para compartir 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 negocio compartida
    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) {
                // Manejar error
            }
        }

        private fun handleApproved() {
            // Lógica de aprobación compartida
        }

        private fun handleRejected() {
            // Lógica de rechazo compartida
        }
    }
    ```
  </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)
            // Presentar 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 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ámara
      allowsInlineMediaPlayback={true}
    />
  );
};

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

**Permisos:**

* Agrega permisos de cámara a `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),
    );
  }
}
```

**Dependencias (pubspec.yaml):**

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

**Permisos:**

* Android: Agrega permiso de cámara a `AndroidManifest.xml`
* iOS: Agrega descripción de uso de cámara a `Info.plist`

***

## Integración 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 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 {
  // Tu lógica de token de autenticación
  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'; // Reemplazar con ID de usuario 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 y ocultar loading
                document.getElementById('loading').style.display = 'none';
                const iframe = document.getElementById('verification-iframe');
                iframe.src = verificationUrl;
                iframe.style.display = 'block';

                // Iniciar polling para estado
                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 cada 3 segundos
        }

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

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

        function getAuthToken() {
            // Obtener token de autenticación de localStorage, cookie, etc.
            return localStorage.getItem('authToken');
        }

        // Iniciar carga cuando la página carga
        window.addEventListener('DOMContentLoaded', loadVerificationUrl);

        // Limpiar al descargar la página
        window.addEventListener('beforeunload', () => {
            if (pollingInterval) {
                clearInterval(pollingInterval);
            }
        });
    </script>
</body>
</html>
```

***

## Detección de Finalización del Flujo

<Warning>
  El WebView/iframe **no puede** notificar directamente a tu app cuando la verificación se completa. La UI de verificación de Gu1 se ejecuta en aislamiento por razones de seguridad.
</Warning>

### Patrón Recomendado: Polling al Backend

Tu frontend hace polling a tu backend, que recibe webhooks de Gu1:

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

    User->>Gu1: Completar verificación
    Gu1->>YourBackend: Webhook (aprobado/rechazado)
    YourBackend->>YourBackend: Actualizar estado del usuario en BD

    loop Cada 3 segundos
        Frontend->>YourBackend: Polling: GET /kyc/status/:userId
        YourBackend-->>Frontend: Devolver estado actual
    end

    Frontend->>Frontend: Detectar cambio de estado
    Frontend->>User: Mostrar resultado
```

**Ejemplo de endpoint del backend:**

```javascript theme={null}
// Ejemplo 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 del frontend (todas las 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 (Avanzado)

Para mejor UX, usa WebSockets para enviar actualizaciones en lugar de polling:

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

// Cuando llega el webhook
app.post('/webhooks/kyc', async (req, res) => {
  const { event, payload } = req.body;

  // Actualizar base de datos
  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);
});
```

***

## Manejo de Webhooks

Tu backend recibe webhooks de Gu1 cuando la verificación se completa.

<Card title="Guía de Integración de Webhooks" icon="webhook" href="/es/webhooks/events/kyc-events">
  Ver guía completa de integración de webhooks con ejemplos de código, seguridad y estructuras de payload
</Card>

### Eventos Clave de Webhook

| Evento                     | Cuándo se Dispara               | Acción                                                           |
| -------------------------- | ------------------------------- | ---------------------------------------------------------------- |
| `kyc.validation_approved`  | Usuario verificado exitosamente | Actualizar estado del usuario a `verified`, habilitar funciones  |
| `kyc.validation_rejected`  | Verificación fallida            | Actualizar estado a `rejected`, opcionalmente permitir reintento |
| `kyc.validation_abandoned` | Usuario salió sin completar     | Marcar como `incomplete`, enviar email de recordatorio           |
| `kyc.validation_expired`   | Sesión expirada                 | Permitir al usuario solicitar nueva URL                          |

**Manejador mínimo de webhook:**

```javascript theme={null}
app.post('/webhooks/gu1/kyc', async (req, res) => {
  // Verificar firma (ver guía 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');
});
```

***

## Ejemplo de Flujo Completo

Veamos un ejemplo completo de extremo a extremo:

### 1. Backend: Generar 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 });

  // Asegurar que la entidad persona existe en Gu1 (crear o obtener 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 } });
  }

  // Crear validación 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 Verificación

```javascript theme={null}
// Usuario hace clic en "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 en WebView/iframe
  openVerificationUI(verificationUrl);

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

### 3. Backend: Recibir 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 Finalización

```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);
}
```

***

## Pruebas

### Entorno Sandbox

Usa el modo sandbox para pruebas:

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

En modo sandbox:

* No se requieren documentos reales
* Puedes simular diferentes resultados
* Los webhooks se disparan normalmente

### Probando Diferentes Resultados

Para probar escenarios de rechazo/expiración, usa diferentes datos de prueba en modo sandbox.

***

## Solución de Problemas

<AccordionGroup>
  <Accordion title="WebView muestra página en blanco">
    **Posibles causas:**

    * URL expirada (generar una nueva)
    * JavaScript deshabilitado en WebView
    * Problemas de conectividad de red

    **Soluciones:**

    * Habilitar JavaScript: `settings.javaScriptEnabled = true`
    * Verificar que la URL es válida y no ha expirado
    * Probar URL en navegador normal primero
  </Accordion>

  <Accordion title="Cámara no funciona en WebView">
    **Posibles causas:**

    * Faltan permisos de cámara
    * La reproducción de medios requiere gesto del usuario

    **Soluciones:**

    **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 finalización">
    **Posibles causas:**

    * Webhook no recibido por el backend
    * Verificación de firma de webhook fallando
    * Base de datos no se está actualizando

    **Soluciones:**

    * Verificar logs de webhook en el dashboard de Gu1
    * Verificar implementación de firma de webhook
    * Agregar logging al manejador de webhook
    * Probar endpoint de webhook manualmente
  </Accordion>

  <Accordion title="Usuario atascado en verificación">
    **Posibles causas:**

    * Iluminación deficiente para fotos de documentos
    * Tipo de documento no soportado
    * Problemas técnicos

    **Soluciones:**

    * Proporcionar instrucciones claras antes de comenzar
    * Mostrar ejemplos de fotos buenas vs malas
    * Implementar timeout (15-20 minutos)
    * Permitir al usuario salir y reintentar
  </Accordion>
</AccordionGroup>

***

## Resumen de Mejores Prácticas

<CardGroup cols={2}>
  <Card title="Seguridad Primero" icon="shield-check">
    * Nunca exponer claves de API en el cliente
    * Siempre generar URLs desde el backend
    * Verificar firmas de webhook
    * Usar HTTPS en todas partes
  </Card>

  <Card title="Experiencia de Usuario" icon="face-smile">
    * Mostrar estados de carga
    * Proporcionar instrucciones claras
    * Manejar errores con gracia
    * Permitir reintentos en caso de fallo
  </Card>

  <Card title="Confiabilidad" icon="chart-line">
    * Implementar polling con intervalos razonables
    * Manejar fallos de red
    * Establecer timeouts apropiados
    * Probar todas las plataformas exhaustivamente
  </Card>

  <Card title="Cumplimiento" icon="scale-balanced">
    * No almacenar datos sensibles de verificación
    * Usar patrón Process-and-Purge
    * Respetar políticas de retención de datos
    * Documentar tu integración
  </Card>
</CardGroup>

***

## Próximos Pasos

<CardGroup cols={2}>
  <Card title="Descripción General de la API KYC" icon="book" href="/es/use-cases/kyc/overview">
    Aprende sobre la API KYC y la integración del backend
  </Card>

  <Card title="Integración de Webhooks" icon="webhook" href="/es/webhooks/events/kyc-events">
    Configura webhooks para recibir resultados de verificación
  </Card>

  <Card title="Guía de Flujo Completo" icon="diagram-project" href="/en/use-cases/kyc/complete-flow">
    Ver el flujo completo de verificación KYC
  </Card>

  <Card title="Guía de Seguridad" icon="lock" href="/es/webhooks/security">
    Implementa seguridad de webhooks y verificación HMAC
  </Card>
</CardGroup>
