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

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:

1 · Establecer info del terminalsetTerminalInfo
Anuncia este terminal a Zeal y le pide que registre sus handlers. Hazlo siempre primero.
2 · Hash de la tarjetahashCard
Solo para la demo. Hace las veces de la huella de la tarjeta que tu app de pago genera al leerla. El ejemplo no tiene lector de tarjetas ni clave, así que calcula un SHA-256 simple del número de tarjeta introducido para obtener card_id. Sin red: solo registra el resultado en el log.
3 · Comprobación de sociofetchSaleBenefits
Simula la llamada host a host que obtiene el cuerpo de beneficios de la venta: envía por POST los datos hasheados de la tarjeta al servicio web de fidelización de Zeal y recibe una respuesta de beneficios de la venta (customer_identified, transaction_processed, has_voucher).
4 · Tras detectar la tarjetaAFTER_CARD_DETECTED
Envía la tarjeta + el monto y recibe un monto ajustado / estado. Envía card_id como loyalty_token y adjunta la respuesta de beneficios de la venta.
5 · eReceiptE_RECEIPT
Envía los datos del recibo y recibe un 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:

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 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:

1 · Establerequisito
La misma tarjeta física genera siempre la misma huella en todas las compras, terminales y comercios.
2 · Irreversiblerequisito
Nadie sin tu clave o tu bóveda puede recuperar el PAN a partir de ella. La clave o la bóveda se quedan contigo y nunca se comparten con Zeal.
3 · Fija durante la integraciónrequisito
Mantén la misma clave o bóveda durante toda la vida de la integración. Cambiarla cambia todas las huellas y deja huérfanas todas las tarjetas inscritas.

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

SolicitudHashMap
cardBin, loyalty_token, currency_code, decimal_shift, amount, Masked_Pan, expired_Date
Solicitudopcional
par — referencia de cuenta de pago EMV, reenviada al backend de Zeal
La respuesta agregaHashMap
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
Solicitudopcional
par — referencia de cuenta de pago EMV, reenviada al backend de Zeal
La respuesta agregaHashMap
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.