Pular para o conteúdo principal

Exemplos Práticos

Exemplos de integração com o SDK, baseados no app de exemplo (:sample).

Os métodos de I/O do PosPinpad são suspend — chame-os de dentro de um CoroutineScope (ex.: viewModelScope).

Inicialização Completa (ViewModel)

Fluxo create → setEndpointProvider → getTerminalConfig → register → terminalInitialization.

class PaymentViewModel : ViewModel() {

private lateinit var pospinpad: PosPinpad

fun initialize(context: Context) {
viewModelScope.launch {
pospinpad = PosPinpad.create(
context = context,
posPinpadCallback = paymentCallback // ver seção abaixo
)

// Ambientes admin (não-PCI) e financeiro (PCI)
pospinpad.setEndpointProvider(
EndpointProvider(
adminBaseUrl = BuildConfig.ADMIN_BASE_URL,
financialBaseUrl = BuildConfig.FINANCIAL_BASE_URL
)
)

// Personalização do parceiro (define o registrationSchema)
pospinpad.getTerminalConfig(slug = PARTNER_SLUG)

// Registro CNPJ + Token (ids dos campos vêm do registrationSchema)
val registration = pospinpad.register(
fields = mapOf("cnpj" to operatorCnpj, "token" to operatorToken),
partnerSlug = PARTNER_SLUG
)

val merchantId = registration.merchantId
if (!registration.success || merchantId.isNullOrBlank()) {
showError(registration.error?.message ?: "Falha no registro do terminal.")
return@launch
}

// Inicializa o terminal (carrega tabelas EMV)
pospinpad.terminalInitialization(
request = TerminalInitializationRequest(merchantId = merchantId)
)
}
}
}

Implementação dos Callbacks

private val paymentCallback = object : PosPinpadCallback {

// ---- Cartão / PIN ----
override fun onStartGetCard() {
setMessage("Aproxime, insira ou passe o cartão")
}

override fun onStartGetPinCoordinates(setPinCoordinates: (List<String>) -> Unit) {
// Exibe a tela de PIN; a UI mede os botões e devolve as coordenadas
showPinScreen { coordinates -> setPinCoordinates(coordinates) }
}

override fun onPinEntry(pinMask: String, message: String, amount: Long) {
updatePinMask(pinMask)
}

override fun onPinError(remainingAttempts: Int, message: String) {
setMessage("$message — tentativas restantes: $remainingAttempts")
}

// ---- Resultado ----
override fun onTransactionCompleted(result: PaymentResult) {
when (result.status) {
PaymentResult.PaymentStatus.APPROVED -> handleApproved(result)
PaymentResult.PaymentStatus.DECLINED -> showError(result.displayMessage ?: "Negado")
PaymentResult.PaymentStatus.ERROR -> showError(result.displayMessage ?: "Erro")
}
}

// ---- Inputs ----
override fun onRequestUserInput(
inputCode: InputCode, minLength: Int, maxLength: Int,
onTimeout: (() -> Unit) -> Unit, onInputReceived: (String?) -> Unit
) {
onTimeout { dismissInputDialog() }
val title = when (inputCode) {
InputCode.CVV -> "Digite o CVV"
InputCode.LAST_4_DIGITS -> "Últimos 4 dígitos"
}
showInputDialog(title, minLength, maxLength) { value -> onInputReceived(value) }
}

override fun onRequestMenuSelection(
title: String, options: List<String>,
onTimeout: (() -> Unit) -> Unit, onOptionSelected: (Int?) -> Unit
) {
onTimeout { dismissMenu() }
showMenu(title, options) { index -> onOptionSelected(index) }
}

// ---- PIX ----
override fun onPixQrCodeGenerated(qrCode: String, transactionId: String, amount: Long, expiresInSeconds: Int?) {
showPixQrCode(qrCode, expiresInSeconds)
}

// ---- Estorno ----
override fun onRefundTransactionsAvailable(
transactions: List<TransactionSummary>, cardLastDigits: String,
cardBrand: String, cardBin: String,
confirmRefund: (Int?, Long?) -> Unit
) {
if (transactions.size == 1) { confirmRefund(0, null); return }
showSelection("Estornar (**** $cardLastDigits)", transactions.map { it.id }) { index ->
confirmRefund(index, null)
}
}

override fun onRefundCompleted(result: PaymentResult) {
setMessage("Estorno concluído: ${result.receipt?.transactionId}")
}

override fun onRefundError(errorCode: String, message: String) {
showError("Erro no estorno [$errorCode]: $message")
}

// ---- Pré-autorização ----
override fun onPreAuthTransactionsAvailable(
transactions: List<TransactionSummary>, cardLastDigits: String,
cardBrand: String, cardBin: String,
confirmPreAuth: (Int?, Long?) -> Unit
) {
if (transactions.size == 1) { confirmPreAuth(0, null); return }
showSelection("Pré-autorizações (**** $cardLastDigits)", transactions.map { it.id }) { index ->
confirmPreAuth(index, null)
}
}

// ---- Impressão / Tabelas ----
override fun onPrintCompleted() { setMessage("Comprovante impresso") }
override fun onNoHasPaper() { setMessage("Sem papel") }
override fun onNotifyPrinterError(code: Int, msg: String) { showError("Impressora $code: $msg") }
override fun onTablesLoaded(ok: Boolean) { if (!ok) showError("Erro ao carregar tabelas") }

override fun onUiNotify(text: String, type: NotificationType) { setMessage(text) }

// ---- Erros ----
override fun onAppError(error: SdkErrorCode, message: String) {
showError("${error.name} (${error.code}): $message")
}
override fun onNetworkConnectionError() { showError("Sem conexão") }
override fun onClientError() { showError("Erro de requisição") }
override fun onUnauthorizedError() { navigateToLogin() }
override fun onSessionExpired() { navigateToLogin() }
override fun onNotFoundError() { showError("Não encontrado") }
override fun onServerError() { showError("Erro no servidor") }
override fun onGenericError() { showError("Erro desconhecido") }
}

Pagamentos

fun processTransaction(amount: Long, paymentMethod: PaymentMethod?, transactionType: TransactionType) {
viewModelScope.launch {
when (transactionType) {
TransactionType.AUTHORIZATION ->
pospinpad.createPayment(
type = TransactionType.AUTHORIZATION,
amount = amount,
paymentMethod = paymentMethod,
installments = 1
)

TransactionType.PRE_AUTHORIZE ->
pospinpad.createPayment(
type = TransactionType.PRE_AUTHORIZE,
amount = amount,
paymentMethod = PaymentMethod.CREDIT,
installments = 1
)

TransactionType.VOID -> pospinpad.startRefundFlow()
TransactionType.CONFIRM_PRE_AUTHORIZED -> pospinpad.startPreAuthFlow()
else -> { /* sem ação */ }
}
}
}

Crédito parcelado

suspend fun creditoParcelado(valor: Long, parcelas: Int, comJuros: Boolean) {
pospinpad.createPayment(
type = TransactionType.AUTHORIZATION,
amount = valor, // ex.: 30000L = R$ 300,00
paymentMethod = PaymentMethod.CREDIT,
installments = parcelas,
installmentsInterest = comJuros
)
}

PIX

suspend fun cobrarPix(valor: Long) {
pospinpad.createPixPayment(amount = valor)
// QR Code chega em onPixQrCodeGenerated(...)
}

Validar limites antes do pagamento

val limits = pospinpad.getTransactionLimits(
transactionType = TransactionType.AUTHORIZATION,
paymentMethod = PaymentMethod.CREDIT,
installments = 3
)
if (limits != null && limits.maxTransactionValueCents != 0L &&
valor > limits.maxTransactionValueCents) {
showError("Valor acima do máximo permitido para ${limits.transactionName}")
return
}

Consulta de Transações

suspend fun listarVendasDoDia() {
val filters = QueryFilterBuilder()
.pagination(offset = 0, limit = 20, preloads = true)
.add(FilterField.STATUS, FilterOperator.EQ, "approved")
.add(FilterField.CREATED_AT, FilterOperator.GE, hojeIso())

val response = pospinpad.getTransactionSummaries(filters)
response.data.forEach { tx ->
Log.d("YBY", "${tx.id} ${tx.amountAsDouble} ${tx.cardBrand} ${tx.status} NSU=${tx.nsu}")
}
}

Configuração de Coordenadas do PIN (Compose)

A tela de senha mede a posição de cada botão com onGloballyPositioned + positionInWindow() e monta a lista na ordem esperada pelo SDK ([0-8]=1–9, [9]=0, [10]=CANCELAR, [11]=OK, [12]=backspace).

@Composable
fun PinInputScreen(
amount: String,
pin: String,
onCoordinatesCaptured: (List<String>) -> Unit
) {
val buttonCoordinates = remember { mutableStateMapOf<String, String>() }
val rows = remember {
listOf(
listOf("1", "2", "3"),
listOf("4", "5", "6"),
listOf("7", "8", "9"),
listOf("<", "0", "X"), // < = backspace, X = cancelar
listOf("OK")
)
}

Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Valor: $amount", fontSize = 32.sp)
Text("*".repeat(pin.length).ifEmpty { "Digite o PIN" }, fontSize = 64.sp)

rows.forEach { row ->
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
row.forEach { label ->
Surface(
modifier = Modifier
.weight(1f)
.height(100.dp)
.onGloballyPositioned { c ->
val pos = c.positionInWindow()
val left = pos.x.toInt(); val top = pos.y.toInt()
val right = left + c.size.width; val bottom = top + c.size.height
val key = when (label) {
"<" -> "BACKSPACE"
"X" -> "CANCELAR"
else -> label
}
buttonCoordinates[key] = "$left,$top,$right,$bottom"

// 13 chaves posicionadas → montar a lista na ordem do SDK
if (buttonCoordinates.size == 13) {
val list = ArrayList<String>()
for (i in 1..9) list.add(buttonCoordinates[i.toString()] ?: "")
list.add(buttonCoordinates["0"] ?: "") // [9] = 0
list.add(buttonCoordinates["CANCELAR"] ?: "") // [10] = CANCELAR
list.add(buttonCoordinates["OK"] ?: "") // [11] = OK
list.add(buttonCoordinates["BACKSPACE"] ?: "") // [12] = backspace
onCoordinatesCaptured(list)
}
}
) {
Box(contentAlignment = Alignment.Center) { Text(label, fontSize = 20.sp) }
}
}
}
}
}
}

Ligando à solicitação do SDK:

override fun onStartGetPinCoordinates(setPinCoordinates: (List<String>) -> Unit) {
showPinScreen(onCoordinatesCaptured = { coords -> setPinCoordinates(coords) })
}

Impressão do Comprovante

private fun handleApproved(result: PaymentResult) {
val receipt = result.receipt ?: return

pospinpad.printText("COMPROVANTE DE VENDA",
PrintAttributes(align = Align.CENTER, typeface = Typeface.BOLD))
pospinpad.printColumns(
listOf("Valor", formatCents(receipt.amount)),
listOf(PrintAttributes(weight = 1), PrintAttributes(align = Align.RIGHT, weight = 1))
)
pospinpad.printText("Cartão: ${receipt.cardBrand} ****${receipt.cardLastDigits}", PrintAttributes())
pospinpad.printText("Autorização: ${receipt.authCode}", PrintAttributes())
receipt.installments?.let { pospinpad.printText("Parcelas: ${it}x", PrintAttributes()) }

// slip pronto, se disponível
result.slip?.let { pospinpad.print(it) }

pospinpad.feedStep(1) // destaca a bobina
}

private fun formatCents(cents: Long): String =
"R$ %d,%02d".format(cents / 100, cents % 100)

Tela Secundária (OCTA 400)

if (pospinpad.hasSecondaryDisplay()) {
pospinpad.initSecondaryDisplay() // → onSecondaryDisplayReady()
}

// após onSecondaryDisplayReady():
pospinpad.setSecondaryDisplayBrightness(80)
pospinpad.showMessageOnSecondaryDisplay("Bem-vindo!")

// no onDestroy:
pospinpad.releaseSecondaryDisplay()

Mais Recursos

  • Código de referência: módulo :sample do projeto yby-android-mobile-pos-sdk (PaymentViewModel.kt, PinScreen.kt)
  • API PosPinpad — referência completa
  • Callbacks — todos os eventos
  • Modelos — estruturas de dados e códigos de erro