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
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:
payment_reference.La integración completa son unos siete archivos pequeños. Sus funciones:
ZealClient.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_statusPaso 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
cardBin, loyalty_token, currency_code, decimal_shift, amount, Masked_Pan, expired_Datetransaction_id, decimal_shift, amount, currency_codeeReceipt — E_RECEIPT
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_numberpayment_referenceCompilar 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.zipO lee el código junto a esta guía: los siete archivos que se listan en Qué demuestra son toda la integración.