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

# SDK para apps móviles

> Integra el widget de TapiPay en tu app de Android o iOS con el SDK nativo.

El **SDK nativo de TapiPay** abre el widget de pago dentro de tu app de Android o iOS, en una pantalla propia del SDK, y te avisa por eventos qué está haciendo el usuario. Es el equivalente móvil del [SDK de medios de pago](https://devs.tapipay.la/es/portal-de-pagos/sdk-medios-de-pago.md): monta las mismas vistas (pagos y [gestión de adhesiones](https://devs.tapipay.la/es/portal-de-pagos/sdk-adhesiones.md)) y usa los mismos datos para identificar a tu empresa y a tu usuario.

> **[Android](#android)**
>
> Kotlin, Jetpack Compose o Views. Se instala con Gradle.

> **[iOS](#ios)**
>
> Swift, SwiftUI o UIKit. Se instala con Swift Package Manager.

## Cómo funciona

1. Tu app llama a `Tapipay.present(...)` (o muestra el `TapipayButton` del SDK) con el identificador del usuario y el de tu empresa.
2. El SDK abre el widget de TapiPay en una pantalla nativa, encima de tu app.
3. Mientras el usuario paga o administra sus adhesiones, el SDK te entrega eventos en un callback.
4. Tu app cierra el widget cuando lo necesita, o el usuario lo cierra con el botón **Cerrar** o el gesto atrás.

> [!WARNING]
>
> Ningún evento del SDK confirma un pago. Cuando llega `PaymentSubmitted` (Android) o `.paymentSubmitted` (iOS), el usuario envió el pago, pero todavía no está acreditado. El estado real llega a tu backend por la [notificación de pago](https://devs.tapipay.la/es/webhooks/notificacion-de-pago.md).

## Requisitos

| | Android | iOS |
| - | - | - |
| Versión mínima del sistema | Android 8.0 (`minSdk` 26) | iOS 16 |
| Lenguaje | Kotlin | Swift |
| Herramientas | JDK 17 o superior para compilar | Xcode 26 o superior |
| Distribución | Repositorio Maven de TapiPay | Swift Package Manager |
| Versión actual del SDK | `0.1.0` | `0.1.0` |

En los dos casos necesitas:

* El **identificador de tu usuario final** (`identifier`), el mismo que envías al crear la deuda.
* El **alias de tu empresa** (`organization`) **o** tu **código de empresa** (`companyCode`). Funcionan igual que en el SDK web: ver [Identifica tu empresa](https://devs.tapipay.la/es/portal-de-pagos/sdk-medios-de-pago.md#identifica-tu-empresa).

## Android

### 1. Agrega el repositorio de TapiPay

El SDK se distribuye por un repositorio Maven propio de TapiPay. Agrégalo en el `settings.gradle.kts` de tu proyecto:

```kotlin settings.gradle.kts theme={null}
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven("https://sdk-tapipay.apps.tapila.cloud/maven") // TapiPay Maven repository
    }
}
```

### 2. Agrega la dependencia

En el `build.gradle.kts` del módulo de tu app:

```kotlin build.gradle.kts theme={null}
dependencies {
    implementation("la.tapipay.ui:button:0.1.0")
}
```

> [!NOTE]
>
> El artefacto se llama `button` por compatibilidad, pero incluye el SDK completo: la pantalla del widget, los eventos y el botón opcional.

No necesitas tocar tu `AndroidManifest.xml`: el SDK ya declara el permiso `INTERNET` y su propia pantalla. Tampoco necesitas reglas de ProGuard o R8: el SDK trae las suyas.

El SDK incluye Jetpack Compose y Material 3 como dependencias. Si tu app usa Views (XML), igual puedes abrir el widget con `Tapipay.present()`; solo `TapipayButton` requiere Compose.

### 3. Abre el widget

Llama a `Tapipay.present()` desde cualquier `Context` (idealmente una `Activity`):

```kotlin theme={null}
import la.tapipay.ui.button.Tapipay
import la.tapipay.ui.button.TapipayConfig
import la.tapipay.ui.button.TapipayEnvironment
import la.tapipay.ui.button.TapipayEvent

Tapipay.present(
    context = this,
    config = TapipayConfig(
        identifier = "CLI-00042",
        organization = "acme-corp", // or companyCode = "MX-S-12345"
        environment = TapipayEnvironment.HOMO, // omit it in production
    ),
) { event ->
    when (event) {
        TapipayEvent.PaymentWindowOpened -> Unit
        is TapipayEvent.PaymentSubmitted -> {
            // Submitted, not settled: show "processing" until the payment notification arrives.
            // Reconcile with externalRequestId, not with paymentId (see "After the payment").
            showProcessing()
        }
        is TapipayEvent.PaymentFailed -> Unit // the user can retry inside the widget
        TapipayEvent.PaymentCancelled -> Unit
        TapipayEvent.Dismissed -> Unit        // the user closed the widget
        is TapipayEvent.LoadFailed -> showError(event.reason)
        else -> Unit                          // required, see below
    }
}
```

`present()` lanza `IllegalArgumentException` si la configuración es inválida: falta `identifier`, no pasas `organization` ni `companyCode`, o usas `selectionStrategy` con la vista de adhesiones.

El callback de eventos siempre se invoca en el hilo principal.

### 4. Cierra el widget

El widget **no se cierra solo** después de un pago. Ciérralo tú cuando lo necesites:

```kotlin theme={null}
Tapipay.dismiss()
```

`dismiss()` no emite ningún evento (tú ya sabes que lo cerraste) y no hace nada si no hay un widget abierto. Puedes llamarlo desde cualquier hilo.

### Botón de Compose (opcional)

Si tu app usa Jetpack Compose, `TapipayButton` muestra un botón de Material 3 que abre el widget al tocarlo:

```kotlin theme={null}
import la.tapipay.ui.button.TapipayButton
import la.tapipay.ui.button.TapipayButtonConfig

TapipayButton(
    config = TapipayConfig(identifier = "CLI-00042", organization = "acme-corp"),
    button = TapipayButtonConfig(text = "Pay my bill"),
    onEvent = { event -> viewModel.onTapipayEvent(event) },
)
```

Toma los colores y la tipografía de tu `MaterialTheme`. Si pasas `onClick`, el botón no abre el widget y te delega el toque.

### Opciones de `TapipayConfig`

| Opción | Tipo | Requerido | Default | Descripción |
| - | - | - | - | - |
| `identifier` | `String` | Sí | | Identificador del usuario final. |
| `organization` | `String?` | Sí\* | `null` | `slug` de tu empresa. \*Requerido salvo que pases `companyCode`. |
| `companyCode` | `String?` | Sí\* | `null` | Código de empresa. \*Alternativa a `organization`; si pasas las dos, esta gana. |
| `view` | `TapipayView` | No | `PAYMENTS` | `PAYMENTS` monta la vista de pagos; `AUTOPAY`, la [gestión de adhesiones](#gestión-de-adhesiones). |
| `environment` | `TapipayEnvironment` | No | `PRODUCTION` | `PRODUCTION` o `HOMO` (homologación). |
| `externalRequestId` | `String?` | No | `null` | Deuda a preseleccionar. |
| `selectionStrategy` | `TapipaySelectionStrategy?` | No | `null` | `OLDEST_CREATED` elige la deuda pendiente más antigua. Solo en `PAYMENTS`. |
| `appearance` | `TapipayAppearance` | No | tema de tu app | Barra superior nativa del widget. Ver [Apariencia](#apariencia-en-android). |

### Eventos en Android

`TapipayEvent` es una `sealed interface`:

| Evento | Cuándo llega |
| - | - |
| `PaymentWindowOpened` | Se abrió la ventana de pago del proveedor. |
| `PaymentSubmitted(paymentId, status, data)` | El usuario envió el pago. `status` es `PENDING`, `SUCCEEDED` o `UNKNOWN` (ver [Qué significa cada `status`](#qué-significa-cada-status)). **No** es la acreditación. |
| `PaymentFailed(paymentId, status, reason)` | El pago fue rechazado (`FAILED`) o quedó pendiente de una acción (`REQUIRES_ACTION`, por ejemplo 3DS). El usuario sigue en el widget y puede reintentar. |
| `PaymentCancelled` | El usuario cerró la ventana de pago sin completar el flujo. |
| `Dismissed` | El **usuario** cerró el widget (botón **Cerrar** o atrás). Es el último evento de la sesión. No llega cuando lo cierras con `Tapipay.dismiss()`. |
| `LoadFailed(reason)` | El widget no pudo cargar: `reason` es `network_error` o `http_<código>`. |
| `AutopayLoaded`, `AutopayActivated`, `AutopayRemoved`, `AutopayError` | Eventos de la [gestión de adhesiones](#gestión-de-adhesiones). |

> [!WARNING]
>
> Incluye siempre una rama `else ->` en tu `when`. En versiones nuevas del SDK pueden sumarse eventos: sin `else`, tu código deja de compilar o ignora el evento sin aviso. Kotlin puede marcar el `else` como redundante; déjalo igual.

#### Campos de los eventos de pago en Android

`PaymentSubmitted`:

| Campo | Tipo | Descripción |
| - | - | - |
| `paymentId` | `String?` | Identificador del intento de cobro en el procesador de tarjetas. Llega cuando el usuario paga con el formulario de tarjeta del widget; es `null` cuando paga en la ventana de pago del proveedor. **No viaja en la notificación de pago**: no lo uses para conciliar (ver [Conciliar con la notificación de pago](#conciliar-con-la-notificación-de-pago)). |
| `status` | `TapipayPaymentStatus` | `PENDING` (default si no viene), `SUCCEEDED` o `UNKNOWN` (un valor que esta versión del SDK no conoce). Ver [Qué significa cada `status`](#qué-significa-cada-status). |
| `data` | `Map<String, String>` | Parámetros que devuelve el proveedor al terminar un pago en la ventana de pago del proveedor. Vacío cuando el usuario paga con el formulario de tarjeta. Las claves dependen del proveedor y no están garantizadas: no las uses para conciliar ni para decidir el estado. Los objetos anidados llegan serializados como JSON y los valores `null` se omiten. |

`PaymentFailed`:

| Campo | Tipo | Descripción |
| - | - | - |
| `paymentId` | `String?` | Identificador del intento de cobro, si llegó a crearse. Es `null` cuando el pago falla antes de crearse (por ejemplo, si no se pudo iniciar la sesión de pago). |
| `status` | `TapipayPaymentStatus` | `FAILED` (rechazado; default si no viene), `REQUIRES_ACTION` (el banco pidió una autenticación adicional que el widget no completó) o `UNKNOWN`. |
| `reason` | `String?` | Código corto del motivo. Ver [Valores de `reason`](#valores-de-reason-en-los-pagos-fallidos). |

`LoadFailed`:

| Campo | Tipo | Descripción |
| - | - | - |
| `reason` | `String` | `network_error` si la página del widget no se pudo descargar (sin conexión, DNS, TLS o timeout). `http_<código>` si el servidor respondió con un error HTTP, por ejemplo `http_404` o `http_503`. |

### Apariencia en Android

`TapipayAppearance` configura la barra superior nativa que envuelve al widget. Si no pasas nada, usa los colores de tu `MaterialTheme`.

| Opción | Tipo | Default | Descripción |
| - | - | - | - |
| `title` | `String?` | "Pagar" | Título de la barra. |
| `showToolbar` | `Boolean` | `true` | Con `false`, el widget ocupa toda la pantalla y se cierra con el botón atrás del sistema. |
| `closeText` | `String?` | "Cerrar" | Texto del botón de cierre. |
| `closeContentDescription` | `String?` | `closeText` | Texto que lee TalkBack para el botón de cierre. |
| `toolbarContainerColorArgb` | `Int?` | tema | Color de fondo de la barra (ARGB). |
| `toolbarContentColorArgb` | `Int?` | tema | Color del título y del botón de cierre (ARGB). |

También puedes redefinir los textos por default en tu app con los recursos `tapipay_button_text`, `tapipay_checkout_title`, `tapipay_checkout_close` y `tapipay_checkout_load_failed`.

### Buenas prácticas en Android

* **Un widget a la vez.** Si llamas `present()` con un widget ya abierto (por ejemplo, por un doble toque), la llamada se ignora.
* **No captures la `Activity` en el callback.** El SDK retiene el callback mientras el widget está abierto. Reenvía los eventos a un `ViewModel` o revisa el `Lifecycle` antes de tocar la UI.
* **Rotación y modo oscuro** no recrean la pantalla del widget. Si Android destruye la pantalla por falta de memoria, el widget se recarga y el usuario tiene que volver a cargar los datos del formulario.

## iOS

### 1. Agrega el paquete

El SDK se distribuye por **Swift Package Manager** desde este repositorio:

```text theme={null}
https://bitbucket.org/tapila/tapi-tapipay-ios-sdk
```

**Xcode**

1. Abre **File → Add Package Dependencies…**
2. Pega la URL del repositorio.
3. En **Dependency Rule**, elige **Up to Next Minor Version** desde `0.1.0`.
4. Agrega el producto `Tapipay` al target de tu app.

Xcode embebe el framework sin pasos extra.

**Package.swift**

```swift Package.swift theme={null}
dependencies: [
    .package(
        url: "https://bitbucket.org/tapila/tapi-tapipay-ios-sdk.git",
        .upToNextMinor(from: "0.1.0")
    ),
],
targets: [
    .target(
        name: "MyApp",
        dependencies: [
            .product(name: "Tapipay", package: "tapi-tapipay-ios-sdk"),
        ]
    ),
]
```

> [!NOTE]
>
> Mientras el SDK esté en versiones `0.x`, usa **Up to Next Minor Version** (`.upToNextMinor`): recibes las correcciones (`0.1.x`) sin que una versión `0.2.0` cambie tu build sin aviso. El SDK solo se distribuye por Swift Package Manager (no hay CocoaPods ni Carthage).

No necesitas agregar permisos ni claves en tu `Info.plist`.

### 2. Abre el widget

`Tapipay.present()` presenta el widget de forma modal sobre la pantalla visible. Llámalo desde el hilo principal:

```swift theme={null}
import Tapipay

do {
    try Tapipay.present(
        config: TapipayConfig(
            identifier: "CLI-00042",
            organization: "acme-corp",   // or companyCode: "MX-S-12345"
            environment: .homologation   // omit it in production
        )
    ) { event in
        switch event {
        case .paymentWindowOpened:
            break
        case .paymentSubmitted:
            // Submitted, not settled: show "processing" until the payment notification arrives.
            // Reconcile with externalRequestId, not with paymentId (see "After the payment").
            showProcessing()
        case .paymentFailed:
            break                        // the user can retry inside the widget
        case .paymentCancelled:
            break
        case .closed:
            break                        // the user closed the widget
        case .failed(let error):
            showError(error)
        case .autopayLoaded, .autopayActivated, .autopayRemoved, .autopayError:
            break                        // only with view: .autopay
        @unknown default:
            break                        // required, see below
        }
    }
} catch {
    // Invalid configuration (TapipayError)
}
```

Por default el SDK presenta el widget desde la pantalla visible de tu app. Si quieres elegir desde dónde se presenta, pasa el parámetro `from:` con tu `UIViewController`.

### 3. Cierra el widget

El widget **no se cierra solo** después de un pago. Ciérralo tú cuando lo necesites:

```swift theme={null}
Tapipay.dismiss()
// or, if you need to chain something:
Tapipay.dismiss(animated: true) {
    // the widget is closed
}
```

`dismiss()` no emite `.closed` (tú ya sabes que lo cerraste) y no hace nada si no hay un widget abierto.

### Botón de SwiftUI (opcional)

`TapipayButton` es un botón de SwiftUI que abre el widget al tocarlo:

```swift theme={null}
import SwiftUI
import Tapipay

struct PaymentView: View {
    let config = TapipayConfig(identifier: "CLI-00042", organization: "acme-corp")

    var body: some View {
        TapipayButton(title: "Pay my bill", config: config) { event in
            print(event)
        }
        .buttonStyle(.borderedProminent)
    }
}
```

Si la configuración es inválida, el botón no lanza un error: te lo entrega como evento `.failed`. Si pasas `onClick`, el botón no abre el widget y te delega el toque.

### Opciones de `TapipayConfig` en iOS

| Opción | Tipo | Requerido | Default | Descripción |
| - | - | - | - | - |
| `identifier` | `String` | Sí | | Identificador del usuario final. |
| `organization` | `String?` | Sí\* | `nil` | `slug` de tu empresa. \*Requerido salvo que pases `companyCode`. |
| `companyCode` | `String?` | Sí\* | `nil` | Código de empresa. \*Alternativa a `organization`; si pasas las dos, esta gana. |
| `view` | `TapipayViewMode` | No | `.payments` | `.payments` monta la vista de pagos; `.autopay`, la [gestión de adhesiones](#gestión-de-adhesiones). |
| `environment` | `TapipayEnvironment` | No | `.production` | `.production` o `.homologation`. |
| `externalRequestId` | `String?` | No | `nil` | Deuda a preseleccionar. |
| `selectionStrategy` | `TapipaySelectionStrategy?` | No | `nil` | `.oldestCreated` elige la deuda pendiente más antigua. Solo en `.payments`. |
| `appearance` | `TapipayAppearance` | No | `TapipayAppearance()` | Barra de navegación nativa del widget. Ver [Apariencia](#apariencia-en-ios). |

### Eventos en iOS

| Evento | Cuándo llega |
| - | - |
| `.paymentWindowOpened` | Se abrió la ventana de pago del proveedor. |
| `.paymentSubmitted(TapipayPaymentSubmission)` | El usuario envió el pago. Trae `paymentId`, `status` (`.pending`, `.succeeded` o `.unknown`; ver [Qué significa cada `status`](#qué-significa-cada-status)) y `data`. **No** es la acreditación. |
| `.paymentFailed(TapipayPaymentFailure)` | El pago fue rechazado (`.failed`) o quedó pendiente de una acción (`.requiresAction`). El usuario sigue en el widget y puede reintentar. |
| `.paymentCancelled` | El usuario cerró la ventana de pago sin completar el flujo. |
| `.closed` | El **usuario** cerró el widget (botón **Cerrar** o deslizando la hoja). No llega cuando lo cierras con `Tapipay.dismiss()`. |
| `.failed(TapipayError)` | El widget no pudo cargar (`.navigationFailed`). Con `TapipayButton`, también trae los [errores de configuración](#errores-de-configuración). |
| `.autopayLoaded`, `.autopayActivated`, `.autopayRemoved`, `.autopayError` | Eventos de la [gestión de adhesiones](#gestión-de-adhesiones). |

> [!WARNING]
>
> Incluye siempre `@unknown default` en tu `switch`. En versiones nuevas del SDK pueden sumarse eventos: sin `@unknown default`, si llega un evento que tu app no conoce, la app se cierra con un error.

#### Campos de los eventos de pago en iOS

`TapipayPaymentSubmission` (en `.paymentSubmitted`):

| Campo | Tipo | Descripción |
| - | - | - |
| `paymentId` | `String?` | Identificador del intento de cobro en el procesador de tarjetas. Llega cuando el usuario paga con el formulario de tarjeta del widget; es `nil` cuando paga en la ventana de pago del proveedor. **No viaja en la notificación de pago**: no lo uses para conciliar (ver [Conciliar con la notificación de pago](#conciliar-con-la-notificación-de-pago)). |
| `status` | `TapipayPaymentStatus` | `.pending` (default si no viene), `.succeeded` o `.unknown` (un valor que esta versión del SDK no conoce). Ver [Qué significa cada `status`](#qué-significa-cada-status). |
| `data` | `[String: String]` | Parámetros que devuelve el proveedor al terminar un pago en la ventana de pago del proveedor. Vacío cuando el usuario paga con el formulario de tarjeta. Las claves dependen del proveedor y no están garantizadas: no las uses para conciliar ni para decidir el estado. Solo se conservan los valores de texto. |

`TapipayPaymentFailure` (en `.paymentFailed`):

| Campo | Tipo | Descripción |
| - | - | - |
| `paymentId` | `String?` | Identificador del intento de cobro, si llegó a crearse. Es `nil` cuando el pago falla antes de crearse (por ejemplo, si no se pudo iniciar la sesión de pago). |
| `status` | `TapipayPaymentFailureStatus` | `.failed` (rechazado; default si no viene), `.requiresAction` (el banco pidió una autenticación adicional que el widget no completó) o `.unknown`. |
| `reason` | `String?` | Código corto del motivo. Ver [Valores de `reason`](#valores-de-reason-en-los-pagos-fallidos). |

`TapipayError` en `.failed`:

| Error | Cuándo llega |
| - | - |
| `.navigationFailed(String)` | El widget no pudo cargar o su contenido dejó de responder y no se pudo recuperar. El `String` es la descripción del error del sistema (localizada, en el idioma del dispositivo): úsala para tus logs, no para tu lógica ni para mostrarla tal cual al usuario. |
| Errores de configuración | Solo con `TapipayButton`, que no lanza errores: te entrega como evento los mismos [errores de configuración](#errores-de-configuración) que lanzaría `Tapipay.present()`. |

A diferencia de `LoadFailed` en Android, una respuesta HTTP de error del servidor no genera `.failed` en iOS.

### Errores de configuración

`Tapipay.present()` valida la configuración antes de abrir el widget y lanza un `TapipayError`:

| Error | Causa |
| - | - |
| `.missingIdentifier` | `identifier` vacío. |
| `.invalidIdentifier` | `identifier` contiene `/` o `\`, o es `.` o `..`. |
| `.missingTarget` | No pasaste `organization` ni `companyCode`. |
| `.invalidTarget` | `organization` o `companyCode` contienen `/` o `\`, o son `.` o `..`. |
| `.selectionStrategyNotSupported` | Usaste `selectionStrategy` con `view: .autopay`. |
| `.noPresenterAvailable` | No hay una pantalla visible desde donde presentar el widget. Pasa `from:`. |
| `.invalidURL` | La configuración no produjo una dirección válida para el widget. |

### Apariencia en iOS

| Opción | Tipo | Default | Descripción |
| - | - | - | - |
| `navigationTitle` | `String` | "Pago" | Título de la barra de navegación. |
| `closeButtonTitle` | `String` | "Cerrar" | Texto del botón de cierre. |
| `tint` | `Color?` | tint de tu app | Color de los controles nativos (también tiñe el `TapipayButton`). |
| `backgroundColor` | `Color?` | sistema | Color de fondo mientras carga el widget. |
| `showsLoadingIndicator` | `Bool` | `true` | Muestra un indicador mientras carga el widget. |
| `showsNavigationBar` | `Bool` | `true` | Con `false`, el widget se muestra sin barra y se cierra deslizando la hoja. |

## Filtrar medios de pago (beta)

Por defecto, el widget muestra todos los medios de pago que tu empresa tiene habilitados. Si quieres que tu usuario vea solo algunos, pasa `paymentMethods` en `TapipayConfig` con los medios a mostrar.

**Android**

```kotlin theme={null}
import la.tapipay.ui.button.TapipayPaymentMethod

Tapipay.present(
    context = this,
    config = TapipayConfig(
        identifier = "CLI-00042",
        organization = "acme-corp",
        paymentMethods = setOf(TapipayPaymentMethod.CARD, TapipayPaymentMethod.CASH),
    ),
) { event -> handle(event) }
```

**iOS**

```swift theme={null}
try Tapipay.present(
    config: TapipayConfig(
        identifier: "CLI-00042",
        organization: "acme-corp",
        paymentMethods: [.card, .cash]
    )
) { event in
    handle(event)
}
```

| Medio de pago | Android | iOS |
| - | - | - |
| Tarjeta de crédito o débito | `TapipayPaymentMethod.CARD` | `.card` |
| Transferencia SPEI | `TapipayPaymentMethod.BANK_TRANSFER` | `.bankTransfer` |
| Efectivo en tiendas de conveniencia | `TapipayPaymentMethod.CASH` | `.cash` |
| Pago digital (billeteras y bancos digitales) | `TapipayPaymentMethod.DIGITAL_WALLET` | `.digitalWallet` |
| Pago Directo BBVA | `TapipayPaymentMethod.BBVA_PAY_BY_BANK` | `.bbvaPayByBank` |
| Domiciliación | `TapipayPaymentMethod.DIRECT_DEBIT` | `.directDebit` |

Cómo se comporta:

* **Solo restringe.** El widget muestra los medios que pediste **y** que tu empresa tiene habilitados. Nunca habilita uno que tu empresa no tenga.
* **El orden lo define TapiPay**, no el orden de tu lista.
* **Si no queda ningún medio disponible**, el widget muestra su estado vacío.
* **Si no pasas `paymentMethods` o pasas un set vacío**, se muestran todos los medios habilitados.
* **Un solo medio:** por ejemplo, con solo `CARD` tu usuario ve únicamente tarjeta y la toca para continuar. Así puedes tener en tu app un botón por cada medio de pago y abrir el widget con el medio que eligió.
* Solo aplica a la vista de pagos. Con la vista de adhesiones, Android lanza `IllegalArgumentException` en `present()` e iOS ignora la opción.

> [!WARNING]
>
> Esta opción está en beta. Cuando esté disponible el [canal bidireccional](#próximamente), **la forma de usarla va a cambiar**. Te avisaremos en las [Novedades](https://devs.tapipay.la/es/changelog.md).

## Gestión de adhesiones

Con `view = TapipayView.AUTOPAY` (Android) o `view: .autopay` (iOS), el SDK monta la [gestión de adhesiones](https://devs.tapipay.la/es/portal-de-pagos/sdk-adhesiones.md): tu usuario ve sus medios adheridos, adhiere uno nuevo o desadhiere uno existente.

**Android**

```kotlin theme={null}
Tapipay.present(
    context = this,
    config = TapipayConfig(
        identifier = "CLI-00042",
        organization = "acme-corp",
        view = TapipayView.AUTOPAY,
    ),
) { event ->
    when (event) {
        is TapipayEvent.AutopayLoaded -> updateCounter(event.count)
        is TapipayEvent.AutopayActivated -> Unit
        is TapipayEvent.AutopayRemoved -> Unit
        is TapipayEvent.AutopayError -> Unit
        TapipayEvent.Dismissed -> Unit
        is TapipayEvent.LoadFailed -> Unit
        else -> Unit
    }
}
```

**iOS**

```swift theme={null}
try Tapipay.present(
    config: TapipayConfig(
        identifier: "CLI-00042",
        organization: "acme-corp",
        view: .autopay
    )
) { event in
    switch event {
    case .autopayLoaded(let loaded):
        updateCounter(loaded.count)
    case .autopayActivated, .autopayRemoved, .autopayError:
        break
    case .closed, .failed:
        break
    default:
        break
    }
}
```

| Evento | Cuándo llega |
| - | - |
| `AutopayLoaded` / `.autopayLoaded` | Cargaron las adhesiones activas, y cada vez que el conjunto cambia. Trae `count`, `mediaTypes` y `adhesions`. |
| `AutopayActivated` / `.autopayActivated` | Se registró una adhesión nueva. |
| `AutopayRemoved` / `.autopayRemoved` | El usuario desadhirió un medio con éxito. |
| `AutopayError` / `.autopayError` | Falló la carga (`LOAD` / `.load`) o la desadhesión (`REMOVE` / `.remove`). Con `UNAVAILABLE` / `.unavailable` no es un fallo: tu organización no tiene la gestión de adhesiones habilitada. |

Igual que en web, los eventos de adhesiones solo exponen el tipo de medio, los últimos 4 dígitos y un `id` opaco. Nunca viajan datos del titular ni el número de cuenta o tarjeta completos.

## Entorno de homologación

Usa homologación mientras desarrollas y pruebas tu integración, y quita el parámetro (o usa producción) antes de publicar tu app.

| Plataforma | Homologación | Producción (default) |
| - | - | - |
| Android | `TapipayEnvironment.HOMO` | `TapipayEnvironment.PRODUCTION` |
| iOS | `.homologation` | `.production` |

| Entorno | Origen del widget |
| - | - |
| Producción | `https://app.tapipay.la` |
| Homologación | `https://homo.tapipay.la` |

## Después del pago

El widget no se cierra solo cuando el usuario envía el pago: muestra una pantalla de "pago enviado" y queda abierto hasta que alguien lo cierre.

| Evento | Qué te recomendamos hacer |
| - | - |
| `PaymentSubmitted` / `.paymentSubmitted` | Espera 1.5 a 2 segundos para que el usuario vea la confirmación y cierra con `Tapipay.dismiss()`, o reemplázalo por tu propia pantalla de "pago en proceso". Muestra el pago como pendiente hasta que llegue la [notificación de pago](https://devs.tapipay.la/es/webhooks/notificacion-de-pago.md) (ver [Conciliar con la notificación de pago](#conciliar-con-la-notificación-de-pago)). |
| `PaymentFailed` / `.paymentFailed` | No cierres el widget: el usuario puede reintentar ahí mismo. |

### Qué significa cada `status`

El `status` de `PaymentSubmitted` / `.paymentSubmitted` es el estado del intento **en el momento en que el usuario envió el pago**. No cambia después: el resultado final solo llega por la notificación de pago.

| Android | iOS | Qué significa | Qué hacer en tu app |
| - | - | - | - |
| `PENDING` | `.pending` | El pago se envió y el resultado todavía no se conoce. Es el único valor cuando el usuario paga en la ventana de pago del proveedor; con el formulario de tarjeta, indica que el procesador sigue procesando el cobro. | Muestra el pago como "en proceso". |
| `SUCCEEDED` | `.succeeded` | Solo con el formulario de tarjeta: el procesador aprobó el cobro al momento del envío. | Puedes mostrar "pago aprobado, lo estamos confirmando", pero en tu sistema la deuda sigue pendiente. |
| `UNKNOWN` | `.unknown` | El widget informó un estado que tu versión del SDK no conoce. | Trátalo como `PENDING` / `.pending`. |

> [!WARNING]
>
> **Un `SUCCEEDED` todavía puede no terminar en un pago acreditado.** Después de la aprobación, el pago se confirma y se aplica a la deuda de forma asíncrona; si ese proceso no se completa, la notificación con `status: "confirmed"` no llega. Además, un pago ya confirmado se puede revertir más tarde: en ese caso recibes una notificación con `status: "reversed"`. Marca una deuda como pagada solo cuando llega la notificación con `status: "confirmed"`.

### Valores de `reason` en los pagos fallidos

El `reason` de `PaymentFailed` / `.paymentFailed` es un código corto: solo letras, números, `_`, `.` o `-`, hasta 64 caracteres. Nunca trae datos de la tarjeta ni del titular, y puede no venir (`null` / `nil`). Puede ser:

* **El código de respuesta del procesador o del banco**, cuando existe. Por ejemplo, `INSUFFICIENT_FUNDS`, `DO_NOT_HONOR` o `51`. El catálogo depende del procesador y no es cerrado.
* **Una etiqueta de TapiPay**, cuando no hay un código del procesador:

| `reason` | `status` | Cuándo llega |
| - | - | - |
| `PAYMENT_DECLINED` | `FAILED` | El procesador rechazó el cobro sin informar un código. |
| `PAYMENT_REJECTED` | `FAILED` | El cobro no se pudo procesar y no hubo un código del procesador. |
| `REQUIRES_ACTION` | `REQUIRES_ACTION` | El banco pidió una autenticación adicional (por ejemplo, 3DS) que el widget no completó. |
| `CHECKOUT_SESSION_ERROR` | `FAILED` | No se pudo iniciar la sesión de pago con tarjeta. |
| `MISSING_SESSION_PAYLOAD` | `FAILED` | Faltaron datos para iniciar la sesión de pago con tarjeta. |
| `PAYMENT_LINK_ERROR` | `FAILED` | No se pudo preparar la ventana de pago del proveedor. |
| `UNEXPECTED_ERROR` | `FAILED` | Error inesperado durante el pago. |

Usa `reason` para tus logs y métricas, o para elegir tu propio mensaje. No lo muestres tal cual al usuario y contempla valores que no estén en esta lista: el widget ya le muestra al usuario un mensaje sobre el rechazo.

### Conciliar con la notificación de pago

El `paymentId` de los eventos identifica el intento de cobro en el procesador de tarjetas y **no viaja en la [notificación de pago](https://devs.tapipay.la/es/webhooks/notificacion-de-pago.md)**: ningún campo de la notificación coincide con él. Para pasar tu pantalla de "en proceso" a "pagado", concilia por la **deuda** y el **usuario**, que sí viajan en la notificación:

| Lo que le pasas al SDK | Campo de la notificación de pago |
| - | - |
| `identifier` | `externalClientId` |
| `externalRequestId` | `externalRequestId` |
| `paymentId` (del evento) | No viaja. Guárdalo solo como referencia del intento. |

1. Abre el widget con `externalRequestId`, la deuda que quieres que pague el usuario. Así sabes de antemano qué deuda va a cambiar de estado.
2. Cuando llegue `PaymentSubmitted` / `.paymentSubmitted`, marca esa deuda como "en proceso" en tu app.
3. En tu backend, cuando llegue la notificación de pago, ubica la deuda por `externalRequestId` y verifica que `externalClientId` sea el `identifier` del usuario. Con `status: "confirmed"`, márcala como pagada; con `failed`, devuélvela a pendiente; con `reversed`, revierte el pago.
4. Usa `operationId` como llave de idempotencia, como en cualquier [notificación de pago](https://devs.tapipay.la/es/webhooks/notificacion-de-pago.md#reintentos-e-idempotencia).

Si abres el widget sin `externalRequestId` (por ejemplo, con `selectionStrategy`), el widget elige la deuda: la conoces recién cuando llega la notificación, por su `externalRequestId`.

Si la notificación tarda más de lo que esperas, consulta la deuda con [`GET /debts/{id}`](https://devs.tapipay.la/es/recursos/deudas#consultar-y-listar) usando su `externalRequestId`: mientras su `status` no pase a `PAID` o `PARTIALLY_PAID`, el pago todavía no se aplicó.

## Enlaces externos

El widget siempre se abre dentro de tu app. Si el usuario toca un enlace a otro sitio, se abre en el navegador; los enlaces `tel:`, `mailto:` o a otras apps se abren con la app correspondiente del sistema.

## Próximamente

> [!NOTE]
>
> **Canal bidireccional entre tu app y el SDK.** Estamos desarrollando un canal de comunicación en los dos sentidos entre tu app y el SDK. Con él vamos a sumar nuevas funcionalidades a los SDK de TapiPay. Te avisaremos en las [Novedades](https://devs.tapipay.la/es/changelog.md) cuando esté disponible.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.