Saltar al contenido principal

Integrar desde Flutter

Beta

El SDK de cobro es Beta. Este plugin es una plantilla de referencia, no un paquete oficial publicado. Se verificó que compila sin cambios (Flutter 3.41.5, pago-sdk:1.0.0-beta.3 desde el Maven de FINORA Developers); el cobro con lector real se probó desde el SDK en Kotlin.

Flutter no cambia la regla: la tarjeta la maneja FINORA Pago. Tu app Flutter llama al SDK desde un plugin Android propio, con un canal de plataforma (MethodChannel).

Lo que decide si funciona

FINORA Pago identifica a tu app con getCallingPackage(), que Android solo informa cuando la pantalla se abre con startActivityForResult (o la Activity Result API). Los paquetes genéricos de Flutter que lanzan Intents sin esperar resultado dejan ese dato vacío y FINORA Pago responde NO_AUTORIZADO. Por eso necesitas tu propio plugin.

En iPhone no existe FINORA Pago: si tu app debe correr en iOS, cobra por cobro en la nube desde tu servidor.

Dependencia​

// android/build.gradle.kts del plugin
repositories { maven("https://developers.finoracore.com/maven") }
dependencies { implementation("com.dcgeeks.finora:pago-sdk:1.0.0-beta.3") }

La app que use el plugin también debe declarar ese repositorio Maven en su android/build.gradle.kts.

Lado Android (Kotlin)​

package com.tuempresa.finora_pago

import android.app.Activity
import android.content.Intent
import com.dcgeeks.finora.pago.sdk.EstadoPago
import com.dcgeeks.finora.pago.sdk.FinoraPago
import com.dcgeeks.finora.pago.sdk.ResultadoCobro
import com.dcgeeks.finora.pago.sdk.SolicitudCobro
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry

// android/build.gradle del plugin:
// repositories { maven { url "https://developers.finoracore.com/maven" } }
// implementation "com.dcgeeks.finora:pago-sdk:1.0.0-beta.3"
class FinoraPagoPlugin : FlutterPlugin, ActivityAware, MethodChannel.MethodCallHandler,
PluginRegistry.ActivityResultListener {

private lateinit var canal: MethodChannel
private var actividad: Activity? = null
private var pendiente: MethodChannel.Result? = null

override fun onAttachedToEngine(b: FlutterPlugin.FlutterPluginBinding) {
canal = MethodChannel(b.binaryMessenger, "finora/pago").also { it.setMethodCallHandler(this) }
}
override fun onDetachedFromEngine(b: FlutterPlugin.FlutterPluginBinding) =
canal.setMethodCallHandler(null)
override fun onAttachedToActivity(b: ActivityPluginBinding) {
actividad = b.activity; b.addActivityResultListener(this)
}
override fun onReattachedToActivityForConfigChanges(b: ActivityPluginBinding) =
onAttachedToActivity(b)
override fun onDetachedFromActivityForConfigChanges() { actividad = null }
override fun onDetachedFromActivity() { actividad = null }
override fun onMethodCall(call: MethodCall, res: MethodChannel.Result) {
val a = actividad ?: return res.error("SIN_ACTIVIDAD", null, null)
when (call.method) {
"estado" -> res.success(FinoraPago.estado(a).aMapa())
"prepararLector" -> res.success(FinoraPago.prepararLector(a).aMapa())
"cobrar" -> {
if (pendiente != null) return res.error("OCUPADO", "Cobro en curso", null)
val s = try {
SolicitudCobro.Builder()
.montoCentavos(call.argument<Number>("montoCentavos")!!.toLong())
.moneda(call.argument<String>("moneda") ?: "VES")
.referenciaExterna(call.argument<String>("referenciaExterna")!!)
.sucursalCodigo(call.argument<String>("sucursalCodigo")!!)
.pedido(call.argument<String>("pedido"))
.medio(call.argument<String>("medio"))
.build()
} catch (e: Exception) {
return res.error("PETICION_INVALIDA", e.message, null)
}
pendiente = res
a.startActivityForResult(FinoraPago.Cobrar().createIntent(a, s), REQ_COBRO)
}
else -> res.notImplemented()
}
}

override fun onActivityResult(req: Int, code: Int, data: Intent?): Boolean {
if (req != REQ_COBRO) return false
val r = FinoraPago.Cobrar().parseResult(code, data) // sin extras: SIN_RESPUESTA
pendiente?.success(r.aMapa()); pendiente = null
return true
}

private fun ResultadoCobro.aMapa() = mapOf(
"estado" to estado.name, "aprobada" to aprobada, "reintentable" to reintentable,
"codigoRespuesta" to codigoRespuesta, "mensaje" to mensaje,
"autorizacion" to autorizacion, "rrn" to rrn, "stan" to stan,
"ultimos4" to ultimos4, "marca" to marca, "medio" to medio,
"terminalId" to terminalId, "referenciaFinora" to referenciaFinora,
"referenciaExterna" to referenciaExterna, "sucursalCodigo" to sucursalCodigo,
"montoCentavos" to montoCentavos, "fechaHora" to fechaHora,
"simulado" to simulado, "estadoVerificado" to estadoVerificado)

private fun EstadoPago.aMapa() = mapOf(
"instalado" to instalado, "autorizado" to autorizado, "listo" to listo,
"motivo" to motivo, "terminalId" to terminalId, "medios" to medios,
"lectorListo" to lectorListo, "lectorEstado" to lectorEstado, "ocupado" to ocupado)

companion object { private const val REQ_COBRO = 0xF1A0 }
}

Lado Dart​

import 'package:flutter/services.dart';

enum EstadoCobro { APROBADA, RECHAZADA, EN_DUDA, CANCELADA, NO_COBRADA, OCUPADO, NO_LISTO,
NO_AUTORIZADO, PETICION_INVALIDA, CONFLICTO, SIN_RESPUESTA, NO_INSTALADO }

extension Reglas on EstadoCobro {
/// El banco pudo aprobar: nunca recobrar. Consultar o anular.
bool get requiereConsulta => this == EstadoCobro.EN_DUDA || this == EstadoCobro.SIN_RESPUESTA;
}

class FinoraPago {
static const _canal = MethodChannel('finora/pago');

static Future<Map<String, Object?>> estado() async =>
await _canal.invokeMapMethod<String, Object?>('estado') ?? const {};

static Future<void> prepararLector() => _canal.invokeMethod('prepararLector');

static Future<Map<String, Object?>> cobrar({
required int montoCentavos, // céntimos enteros, nunca double
required String referenciaExterna, // guárdala ANTES de llamar
required String sucursalCodigo,
String moneda = 'VES', String? pedido, String? medio,
}) async =>
await _canal.invokeMapMethod<String, Object?>('cobrar', {
'montoCentavos': montoCentavos, 'referenciaExterna': referenciaExterna,
'sucursalCodigo': sucursalCodigo, 'moneda': moneda, 'pedido': pedido, 'medio': medio,
}) ?? const {'estado': 'SIN_RESPUESTA'};
}

EstadoCobro estadoDe(Map<String, Object?> r) => EstadoCobro.values.firstWhere(
(e) => e.name == r['estado'], orElse: () => EstadoCobro.EN_DUDA); // desconocido = duda

Reglas​

  • Persiste la referencia antes de cobrar. Si la app muere a mitad del cobro, al volver consulta esa referencia; no generes otra.
  • Un estado desconocido o un error del canal se trata como EN_DUDA, nunca como rechazo. Ver Estados y errores.
  • Llama a prepararLector() al abrir el pedido y consulta estado() antes de mostrar «Cobrar»: autorizado y listo deben ser verdaderos.
  • Montos en céntimos enteros (int), nunca double.
  • Firma tu APK con tu llave y entrega su huella al comercio: Seguridad y alta. Las compilaciones de depuración tienen otra huella.