ZealDeveloper Hub Beta
EN ES
App-to-App Integration · Android

Zeal App-to-App Integration

Intégrate en el flujo de pago de Zeal en terminales Android: descuentos, recompensas, recibos digitales y devoluciones. Dos formas de integrarte: el Communicator SDK tipado o Direct App-to-App con broadcasts de Android puros. Mismo protocolo, mismo resultado.

Dos formas de integrarte app-to-app, mismo resultado. Usa el Communicator SDK para una API tipada en Kotlin, o ve por Direct App-to-App con broadcasts de Android puros y sin dependencias. Ambos usan el mismo flujo de pago de Zeal a través del mismo protocolo de broadcast; elige el que encaje con tu app.

Communicator SDK

La API de terceros es un servicio que ofrece Zeal para dar soporte a una aplicación de pago autónoma en terminales Android durante el flujo de pago. Esta guía especifica cómo una aplicación de pago se integra con la aplicación autónoma de Zeal.

Cómo funciona. La API de terceros utiliza mensajes de broadcast de Android encaminados a través de receptores declarados en el manifiesto. No hay endpoints REST que llamar desde fuera del dispositivo.

Instalación

Paso 1 · Agrega el repositorio Maven

Agrega el siguiente repositorio Maven a dependencyResolutionManagement en settings.gradle:

dependencyResolutionManagement {
  repositories {
      maven {
          url = uri("https://maven.pkg.github.com/zeal-io/Zeal-POS-App-Communicator-SDK")
          credentials {
              username = "*USERNAME*"
              password = "*PASSWORD*"
          }
      }
  }
}

Paso 2 · Agrega la dependencia

dependencies {
  // ...
  implementation "com.zeal.zealmodule:zeal_communicator_sdk:1.2.20"
}

Paso 3 · Inicializa la app de Zeal

Llama a este método al principio del ciclo de vida de la app, o antes de iniciar el flujo de pago, para inicializar la app de Zeal con los datos de tu comercio:

FlowHandler()
  .setTerminalInfo(context, "serialNumber", "terminalId", "merchantId")

Enviar una acción

Para ejecutar acciones antes o después de los pasos del pago, sigue estos cinco pasos.

1 · Importa las clases necesarias

import com.zeal.zeal_communicator_sdk.*

2 · Inicializa FlowHandler

val flowHandler = FlowHandler()

3 · Prepara el contexto y el tipo de transacción

val context: Context = this // reemplázalo por tu contexto real
val transactionType: TransactionTypes = TransactionTypes.Sale // o TransactionTypes.Void

4 · Construye el objeto de solicitud

Crea una solicitud para la acción concreta, por ejemplo BeforeAmountEntryRequest:

val request = BeforeAmountEntryRequest(
  currency_code = "818",
  decimal_shift = "2",
  amount = "1000"
)

5 · Suscríbete a la acción

Usa la instancia de FlowHandler para llamar a la acción y gestionar la respuesta y las excepciones:

flowHandler.beforeAmountEntry(
  context,
  transactionType, // selecciona el tipo de la transacción
  request           // solicitud para la acción concreta
).subscribe({
  // respuesta con los datos de beforeAmountEntry
}) { throwable ->
  // en caso de excepción
  if (throwable is NotRegisteredFlowException) {
      // la aplicación de destino no ha registrado ninguna acción para este paso
  } else {
      // otra excepción
  }
}

Acciones de transacción disponibles

beforeAmountEntryacción
Se activa tras el paso de introducción del monto y antes de los pasos siguientes. Permite que la aplicación de terceros ejecute lógica personalizada o recopile datos antes de continuar.
afterAmountEnteracción
Se activa antes de la detección de la tarjeta. La aplicación de terceros puede gestionar la transacción con métodos alternativos distintos de las tarjetas de crédito o débito.
afterCardDetectedacción
Se activa tras la lectura de la tarjeta y antes de su verificación. Proporciona el monto total, la moneda y los seis primeros dígitos del PAN; útil para descuentos o validaciones personalizadas.
eReceiptacción
Se activa después de que se aprueba la transacción y antes de imprimir el recibo.
afterTransactionacción
Se activa después de procesar el recibo y antes de volver a la pantalla de espera. Úsala para encuestas, captura de números de teléfono o alta en programas de fidelización.
reverseacción
Se activa cuando hay que revertir una venta completada. Marca la transacción original como revertida y deshaz los puntos, recompensas o cupones asociados.
refundacción
Se activa cuando se realiza una devolución. Marca la transacción como devuelta y revierte los beneficios concedidos por la venta original; puede vincularse a una venta anterior o procesarse de forma independiente.

Tipos de transacción

El enum TransactionTypes define los tipos de transacción disponibles:

enum class TransactionTypes {
  Sale,
  Void,
  Refund,
  PreAuth,
  Completion
}

Valores de tran_status

Cada respuesta incluye un tran_status que describe el resultado del canje:

tran_status {
  FULLY_COVERED, // el descuento cubre el monto íntegro de la transacción
  NEW_AMOUNT,    // el descuento cubre parcialmente el monto; se devuelve el resto
  SAME_AMOUNT,   // no se produjo ningún canje
  ERROR,         // error en el lado de Zeal o del canje
  CANCEL         // se omitió o se decidió no canjear puntos
}

Modelo de respuesta base

Todas las clases de respuesta extienden BaseResponse:

package com.zeal.zeal_communicator_sdk.communicationResponses

open class BaseResponse(
  var third_party_request_type: String,
  var tran_status: String,
  var message: String
)

Consulta la Referencia de la API para ver todos los modelos de solicitud y respuesta: BeforeAmountEntryRequest, AfterCardDetectedRequest, DigitalReceiptRequest, RefundTransactionRequest, MarkAsReverseRequest y sus respuestas correspondientes.

Ejemplo completo

Implementación completa de beforeAmountEntry, incluida la gestión de excepciones:

import com.zeal.zeal_communicator_sdk.*

fun performBeforeAmountEntryAction(context: Context) {
  val flowHandler = FlowHandler()
  val transactionType = TransactionTypes.Sale
  val request = BeforeAmountEntryRequest(
      currency_code = "818",
      decimal_shift = "2",
      amount = "1000"
  )

  flowHandler.beforeAmountEntry(
      context,
      transactionType,
      request
  ).subscribe({
      // Gestiona la respuesta correcta
      Log.d("TransactionAction", "Response: $it")
  }, { throwable ->
      if (throwable is NotRegisteredFlowException) {
          Log.e("TransactionAction", "UnRegisteredFlowException: ${throwable.message}")
      } else {
          Log.e("TransactionAction", "Exception: ${throwable.message}")
      }
  })
}

Direct App-to-App

Direct App-to-App es la forma de integrarte sin dependencias: tu app se comunica con la app POS de Zeal usando únicamente broadcasts de Android — Intent + BroadcastReceiver. Hace el mismo trabajo que el zeal_communicator_sdk oficial, pero con el protocolo escrito a mano en Kotlin para que veas exactamente qué envía y recibe una integración, y copies las piezas en tu propia app.

Una integración se reduce a cuatro movimientos: descubre Zeal → regístrate → envía una solicitud → espera el resultado. Sin llamadas de red, sin AIDL, sin Service vinculado: solo broadcasts de Intent, algo de estado en SharedPreferences y una corrutina que espera la respuesta.

Qué demuestra

El ejemplo tiene una sola pantalla con tres botones. Cada uno corresponde a un paso de una integración real:

1 · Establecer info del terminalsetTerminalInfo
Anuncia este terminal a Zeal y le pide que registre sus handlers. Hazlo siempre primero.
2 · Tras detectar la tarjetaAFTER_CARD_DETECTED
Flujo de ejemplo: envía la tarjeta + el monto y recibe un monto ajustado / estado.
3 · eReceiptE_RECEIPT
Flujo de ejemplo: envía los datos del recibo y recibe un payment_reference.

La integración completa son unos siete archivos pequeños. Sus funciones:

ZealConstants.ktcontrato
Cada action string y clave de datos en un solo lugar: el contrato del protocolo.
ZealRegistration.ktmodelo
Modelo de datos (parseado desde JSON con Gson) que describe un handler que Zeal registró.
ZealClient.ktnúcleo
Descubrimiento, almacenamiento de registros, envío de solicitudes y espera de resultados.
ZealRegisterReceiver.ktreceptor
Recibe los broadcasts «yo manejo el evento X» de Zeal y los almacena.
ZealResultReceiver.ktreceptor
Recibe el resultado de una solicitud y se lo devuelve a ZealClient.
ZealOpenActivityReceiver.ktreceptor
Gestiona cuando Zeal pide a tu app que abra una pantalla.
ZealRegisterResultReceiver.ktreceptor
Recibe la confirmación de Zeal de que el registro se completó correctamente.

Cómo fluye

Dos fases: un handshake de registro único y, después, una solicitud/respuesta por transacción para cada flujo.

A · Discovery & registration (once)
──────────────────────────────────────
Your app   →  queryBroadcastReceivers(ACTION_REGISTER_FROM_COMMUNICATOR)
Android    ←  matching Zeal receiver(s)
Your app   →  ACTION_REGISTER_FROM_COMMUNICATOR + terminal IDs, response_pkg/cls
Zeal       ←  REGISTER_APP — register_request JSON (one per event type)
           ←  SELF_REGISTRATION_RESULT (ack — optional)

B · Per transaction
──────────────────────────────────────
Your app   →  explicit broadcast to Zeal receiver (parameters = HashMap)
           ·  suspend, await result (20 s timeout)
Zeal       ←  ACTION_THIRD_PARTY_RESULT — parameters HashMap
           ·  resume coroutine, read tran_status

Paso A — Descubrir y registrar (setTerminalInfo). Tu app guarda los IDs del terminal, pregunta a Android «¿quién escucha la acción de descubrimiento de Zeal?» mediante queryBroadcastReceivers(ACTION_REGISTER_FROM_COMMUNICATOR) y luego envía un broadcast explícito a cada coincidencia con los IDs del terminal más response_pkg / response_cls (dónde debe Zeal enviar la confirmación de registro). Zeal responde emitiendo un REGISTER_APP por cada tipo de evento que admite: cada uno lleva un blob JSON register_request que parseas en un ZealRegistration y almacenas indexado por tipo de evento (AFTER_CARD_DETECTED, E_RECEIPT, …).

Paso B — Ejecutar un flujo. Busca el registro almacenado para el tipo de evento. Construye la solicitud como un HashMap<String, String>: tus campos de negocio más el tipo de solicitud (third_party_request_type) más las claves de conexión del callback. Envía un broadcast implícito com.zeal_api.ACTION_* (best-effort) y luego el broadcast explícito autoritativo al receptor exacto que Zeal registró. Suspende y espera: Zeal hace su trabajo y emite el resultado mediante ACTION_THIRD_PARTY_RESULT, con un Serializable HashMap<String, String> en el extra parameters. Lee tran_status para decidir qué hacer a continuación (NEW_AMOUNT, FULLY_COVERED, SAME_AMOUNT, ERROR, CANCEL).

Instalación

Paso 1 · Manifest

Declara cómo puede encontrarte Zeal (<queries>) y los receptores a los que Zeal enviará broadcasts:

<queries>
  <intent>
      <action android:name="com.zeal.zealapplication.ACTION_REGISTER_FROM_COMMUNICATOR" />
  </intent>
</queries>

<application ...>
  <receiver android:name=".receivers.ZealRegisterReceiver" android:exported="true">
      <intent-filter>
          <action android:name="com.zeal.api.communicator.action.REGISTER_APP" />
      </intent-filter>
  </receiver>

  <receiver android:name=".receivers.ZealResultReceiver" android:exported="true">
      <intent-filter android:priority="4">
          <action android:name="com.zeal.api.communicator.action.ACTION_THIRD_PARTY_RESULT" />
      </intent-filter>
  </receiver>

  <receiver android:name=".receivers.ZealOpenActivityReceiver" android:exported="true">
      <intent-filter android:priority="4">
          <action android:name="com.zeal.api.communicator.action.ACTION_THIRD_PARTY_REQUEST_OPEN_ACTIVITY" />
      </intent-filter>
  </receiver>

  <receiver android:name=".receivers.ZealRegisterResultReceiver" android:exported="true">
      <intent-filter>
          <action android:name="com.zeal.api.communicator.action.SELF_REGISTRATION_RESULT" />
      </intent-filter>
  </receiver>
</application>

Los receptores deben ser exported="true": Zeal es una app distinta que te envía broadcasts explícitos. Durante el desarrollo puedes añadir QUERY_ALL_PACKAGES, pero para una build de Play Store elimina el permiso amplio y confía en el elemento <queries> acotado de arriba.

Paso 2 · Dependencias

Solo dos además de las librerías habituales de AndroidX: una para parsear el JSON de registro y otra para conectar el broadcast receiver de vuelta a una corrutina:

dependencies {
  implementation("com.google.code.gson:gson:2.10.1")
  implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
  // + appcompat / material / core-ktx as usual
}

android {
  buildFeatures { viewBinding = true } // optional; the demo UI uses it
}

Paso 3 · Constantes

Pon cada action string y clave en un solo archivo para que no se desincronicen. Zeal hace coincidencias con estas cadenas exactas: cópialas tal cual:

object ZealConstants {
  const val ACTION_REGISTER_FROM_COMMUNICATOR = "com.zeal.zealapplication.ACTION_REGISTER_FROM_COMMUNICATOR"
  const val ACTION_REGISTER_APP               = "com.zeal.api.communicator.action.REGISTER_APP"
  const val ACTION_THIRD_PARTY_RESULT         = "com.zeal.api.communicator.action.ACTION_THIRD_PARTY_RESULT"

  // Event types — NOTE: the AfterCardDetected event travels on the wire as "AFTER_TOTAL_AMOUNT";
  // keep that string value verbatim (Zeal matches on it) even though the constant is AFTER_CARD_DETECTED.
  const val AFTER_CARD_DETECTED = "AFTER_TOTAL_AMOUNT"
  const val E_RECEIPT           = "E_RECEIPT"

  // Intent extras
  const val EXTRA_PARAMETERS       = "parameters"
  const val EXTRA_TERMINAL_ID      = "terminalId"
  const val EXTRA_REGISTER_REQUEST = "register_request"
  const val EXTRA_RESPONSE_PKG     = "response_pkg"
  const val EXTRA_RESPONSE_CLS     = "response_cls"

  // Callback-wiring keys placed inside the parameters map
  const val PARAM_REQUEST_TYPE       = "third_party_request_type"
  const val PARAM_THIRD_PARTY_CALLER = "third_party_caller"
  // ... see ZealConstants.kt in the sample for the full list
}

Descubrir y registrar

El handshake único que debe ejecutarse antes de que funcione cualquier flujo. Persiste los IDs del terminal, pide a Android los receptores de Zeal e indica a cada uno dónde enviar su confirmación de registro.

fun setTerminalInfo(
  ctx: Context,
  serial: String,
  terminalId: String,
  merchantId: String,
) {
  // 1. Remember the terminal so later requests can include it.
  prefs(ctx).edit()
      .putString(EXTRA_TERMINAL_SERIAL, serial)
      .putString(EXTRA_TERMINAL_ID, terminalId)
      .putString(EXTRA_MERCHANT_ID, merchantId)
      .apply()

  // 2. Find Zeal by the discovery action it listens for.
  val discovery = Intent(ACTION_REGISTER_FROM_COMMUNICATOR)
  val receivers = ctx.packageManager
      .queryBroadcastReceivers(discovery, PackageManager.GET_RECEIVERS)

  // 3. Tell each Zeal receiver our IDs + where to send the registration ack.
  receivers.forEach { ri ->
      val info = ri.activityInfo ?: return@forEach
      ctx.sendBroadcast(Intent(ACTION_REGISTER_FROM_COMMUNICATOR).apply {
          setClassName(info.packageName, info.name) // explicit = only Zeal
          putExtra(EXTRA_TERMINAL_SERIAL, serial)
          putExtra(EXTRA_TERMINAL_ID, terminalId)
          putExtra(EXTRA_MERCHANT_ID, merchantId)
          putExtra(EXTRA_RESPONSE_PKG, ctx.packageName)
          putExtra(EXTRA_RESPONSE_CLS, ZealRegisterResultReceiver::class.java.name)
      })
  }
}

Cuando Zeal responde, almacena lo que te indica indexado por tipo de evento:

class ZealRegisterReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context, intent: Intent) {
      val json = intent.extras?.getString(EXTRA_REGISTER_REQUEST) ?: return
      val reg = Gson().fromJson(json, ZealRegistration::class.java) ?: return

      // "Zeal handles <eventType> via <package>/<receiver> using <action>" — keyed by event type.
      if (reg.registrationType == REGISTRATION_TYPE_REGISTER) {
          ZealClient.storeRegistration(context, reg.thirdPartyEventType, json)
      } else {
          ZealClient.removeRegistration(context, reg.thirdPartyEventType)
      }
  }
}

Después de esto, tu app sabe qué paquete/receptor/acción de Zeal maneja cada tipo de evento.

Enviar y esperar un resultado

El truco: un BroadcastReceiver entrega la respuesta, pero tú quieres una llamada suspend limpia. Conecta ambos con un CompletableDeferred almacenado en un mapa indexado por tipo de evento.

private val pending = ConcurrentHashMap<String, CompletableDeferred<Map<String, String>>>()

private suspend fun sendFlow(
  ctx: Context,
  eventType: String,
  txnType: String,
  req: Map<String, String>,
): Map<String, String> {
  // Must be registered first (from the setTerminalInfo handshake).
  val reg = getRegistration(ctx, eventType)
      ?: throw NotRegisteredException("$eventType is not registered yet")

  // Your business fields + request type + callback wiring (who to reply to).
  val params = HashMap(req).apply {
      put(PARAM_REQUEST_TYPE, eventType)
      put(PARAM_THIRD_PARTY_CALLER, ctx.packageName)
      put(PARAM_RESPONSE_RECEIVER, ZealResultReceiver::class.java.name)
      put(PARAM_RESULT_CALLBACK, ACTION_THIRD_PARTY_RESULT)
      // + open-activity wiring (see ZealClient.kt in the sample)
  }

  val deferred = CompletableDeferred<Map<String, String>>()
  pending[eventType] = deferred

  // Explicit broadcast to the exact receiver Zeal registered.
  ctx.sendBroadcast(Intent(reg.thirdPartyAction).apply {
      setClassName(reg.thirdPartyPackage, reg.thirdPartyReceiver)
      putExtra(EXTRA_PARAMETERS, HashMap(params)) // Serializable HashMap<String, String>
      // + terminal IDs
  })

  // Suspend until ZealResultReceiver delivers — or give up after 20 s.
  return withTimeout(20_000L) { deferred.await() }
}

El result receiver simplemente desempaqueta el mapa y reanuda a quien espera:

class ZealResultReceiver : BroadcastReceiver() {
  override fun onReceive(context: Context, intent: Intent) {
      @Suppress("DEPRECATION", "UNCHECKED_CAST")
      val raw = intent.getSerializableExtra(EXTRA_PARAMETERS) as? Map<*, *>
      val params = raw?.entries?.associate { (k, v) -> k.toString() to (v?.toString() ?: "") } ?: emptyMap()
      ZealClient.deliverResult(params) // -> pending[type]?.complete(params)
  }
}

Y quien lo llama se ve así:

lifecycleScope.launch {
  val req = mapOf(
      "cardBin"       to card.take(6),
      "loyalty_token" to card,
      "currency_code" to "818",
      "decimal_shift" to "0",
      "amount"        to "100.00",
      "Masked_Pan"    to card,
      "expired_Date"  to "1234",
  )
  val result = ZealClient.afterCardDetected(this@MainActivity, ZealConstants.TXN_SALE, req)
  // result["tran_status"], result["amount"], ...
}

Contrato de datos

Envía las claves exactamente como están escritas, incluidas las que no están en snake_case (cardBin, Masked_Pan, expired_Date). Los valores son siempre cadenas.

Cada respuesta lleva estos campos base: third_party_request_type, tran_status, message. tran_status es uno de:

tran_status {
  FULLY_COVERED, // discount covers the entire transaction amount
  NEW_AMOUNT,    // discount partially covers the amount; remainder returned
  SAME_AMOUNT,   // no redemption occurred
  ERROR,         // error on Zeal or redemption side
  CANCEL         // skipped or chose not to redeem points
}

AfterCardDetected — AFTER_CARD_DETECTED

SolicitudHashMap
cardBin, loyalty_token, currency_code, decimal_shift, amount, Masked_Pan, expired_Date
La respuesta añadeHashMap
transaction_id, decimal_shift, amount, currency_code

eReceipt — E_RECEIPT

SolicitudHashMap
currency_code, decimal_shift, amount, entry_mode, card_brand, card_type, customer_receipt_data, merchant_receipt_data, loyalty_token, third_party_approval_code, third_party_tran_date, third_party_tran_time, phone_number
La respuesta añadeHashMap
payment_reference

Compilar y ejecutar

Requisitos: Android Studio (con el JDK 17 incluido), minSdk 24, compileSdk/targetSdk 34, y un dispositivo o terminal que también tenga instalada la app de Zeal: el ejemplo solo hace algo útil cuando Zeal está ahí para responder.

./gradlew :app:assembleDebug   # build the debug APK
./gradlew :app:installDebug    # install on a connected device/terminal

El APK se genera en app/build/outputs/apk/debug/app-debug.apk.

Flujo típico al probar: introduce el serial / ID / merchant ID del terminal y pulsa Establecer info del terminal, espera a que ZealRegisterReceiver registre en el log los registros que almacenó, y luego pulsa Tras detectar la tarjeta / eReceipt y lee el resultado en el log en pantalla.

Descargar

Descarga el código fuente completo como ZIP y ábrelo en Android Studio:

Download zeal-raw-example.zip

O lee el código junto a esta guía: los siete archivos que se listan en Qué demuestra son toda la integración.