Exemplos Práticos
Exemplos de integração com o SDK, baseados no app de exemplo (:sample).
Os métodos de I/O do
PosPinpadsãosuspend— chame-os de dentro de umCoroutineScope(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
:sampledo projetoyby-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