Zeal Direct App-to-App
Intégrate en el flujo de pago de Zeal en terminales Android sin el Communicator SDK: broadcasts de Intent y BroadcastReceivers puros, con una app de ejemplo lista para descargar.
¿Prefieres una API tipada en Kotlin? El Communicator SDK envuelve este mismo
protocolo de broadcast en llamadas a FlowHandler, así que no tienes que escribir los receptores tú mismo.
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 cinco botones. Cada uno corresponde a un paso de una integración real:
card_id. Sin red: solo registra el resultado en el log.customer_identified, transaction_processed, has_voucher).card_id como loyalty_token y adjunta la respuesta de beneficios de la venta.payment_reference. También envía card_id como loyalty_token.No uses en producción el manejo de tarjetas de la app de ejemplo. El ejemplo obtiene el identificador de la
tarjeta con un SHA-256 simple, sin clave, del número de tarjeta (un hash que se puede revertir) y envía el número de
tarjeta completo como Masked_Pan. En producción, loyalty_token debe llevar una huella de la tarjeta estable
e irreversible (consulta Huella de la tarjeta),
y Masked_Pan solo lleva los cuatro últimos dígitos, como en el fragmento de más abajo. Las mismas reglas se aplican
con o sin el SDK.
El log en pantalla muestra lo que se envió y el resultado que se recibió.
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 agregar 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í, con la huella de la tarjeta en loyalty_token:
lifecycleScope.launch {
val req = mapOf(
"cardBin" to card.take(6),
"loyalty_token" to cardFingerprint, // nunca el PAN
"currency_code" to "818",
"decimal_shift" to "0",
"amount" to "100.00",
"Masked_Pan" to card.takeLast(4),
"expired_Date" to "1234",
// Opcional. Omite la clave por completo si no tienes PAR.
"par" to "V0010013000000000000000000001",
)
val result = ZealClient.afterCardDetected(this@MainActivity, ZealConstants.TXN_SALE, req)
// result["tran_status"], result["amount"], ...
}Huella de la tarjeta
loyalty_token lleva la huella de la tarjeta (Card Fingerprint), también llamada identificador único de
tarjeta (Unique Card Identifier). Es el valor que le indica a Zeal qué tarjeta se ha usado: tu app de pago
lo envía con cada evento de tarjeta (AFTER_CARD_DETECTED y E_RECEIPT), y Zeal reconoce por él a un cliente que
vuelve.
Tu app de pago obtiene la huella de la tarjeta al leerla, dentro de su propio procesamiento seguro de tarjetas, de una de estas dos formas:
- Hash con clave: un HMAC-SHA-256 del PAN completo, con una clave que guardas tú.
- Consulta a una bóveda: una bóveda de tokenización (vault) que devuelve el mismo identificador cada vez que ve la misma tarjeta, de modo que la huella no se deriva del propio PAN.
El número de tarjeta (PAN) nunca se envía a Zeal. El BIN (cardBin) y los cuatro últimos dígitos (Masked_Pan) que
también envías no se usan para identificarla.
La huella de la tarjeta debe cumplir tres requisitos:
Si tu app de pago ya genera un hash estable por tarjeta para sus propios fines, como un hash del PAN que expone el SDK de tu terminal, puedes enviar ese valor como huella de la tarjeta, siempre que cumpla los tres requisitos.
Zeal no prescribe cómo generarla: prescribe estabilidad e irreversibilidad.
¿No puedes generar una huella de la tarjeta que cumpla estos requisitos? Ponte en contacto con Zeal antes de desarrollar la integración.
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_Datepar — referencia de cuenta de pago EMV, reenviada al backend de Zealtransaction_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_numberpar — referencia de cuenta de pago EMV, reenviada al backend de Zealpayment_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.