Застосунок Tapaya Terminal
Приймайте безконтактні платежі карткою, передаючи взаємодію з карткою застосунку-плагіну Tapaya Terminal.
Приймайте безконтактні платежі карткою у своєму застосунку для Android, передаючи взаємодію з карткою застосунку-плагіну
Tapaya Terminal. Продаж залишається під контролем вашого застосунку; плагін бере на себе прикладання картки через NFC, введення PIN і обробку
картки, а потім результат повертається через потік подій Accept.payments.pay().
Використовуйте цю інтеграцію, якщо вам потрібен Tap to Pay без вбудовування платіжного процесора у ваш власний застосунок. Якщо для вашої платформи натомість увімкнено вбудований Tap to Pay, див. посібник Embedded Tap to Pay.
Варіанти інтеграції
| Варіант | Найкраще підходить для | Отримання результату |
|---|---|---|
| Accept SDK (рекомендовано) | Нативні застосунки для Android | Accept.payments.pay() надсилає Flow<PaymentEvent>: створення, запуск і кінцевий результат із термінала обробляються за вас |
| Deeplink | WebView, кросплатформні фреймворки, QR-коди | Ви створюєте платіж через Tapaya API та періодично опитуєте його статус; плагін відкривається через deeplink |
Спершу описано шлях через SDK; контракт deeplink див. у розділі Інтеграція без SDK.
Як це працює
- Ваш застосунок починає платіж викликом
Accept.payments.pay(amount, currency). - SDK реєструє платіж у бекенді Tapaya і запускає плагін Tapaya Terminal на тому самому пристрої.
- Клієнт прикладає свою картку або телефон до екрана Tapaya Terminal.
- Tapaya Terminal завершує обробку платежу з бекендом Tapaya.
- SDK відстежує платіж, доки той не досягне кінцевого стану, і надсилає
PaymentEvent.ResultізPayResult. Якщо платіж узагалі неможливо створити, натомість надсилаєтьсяPaymentEvent.CreationFailed, зазвичай протягом секунди.
Передумови
- Плагін Tapaya Terminal, установлений на пристрої та активований для вашого мерчанта (див. Установлення й активація термінала).
- Пристрій із NFC під керуванням Android 11 (API 30) або новішої версії.
- Accept SDK установлено й ініціалізовано, а мерчант увійшов у систему.
- Дозвіл на доступ до геолокації (
ACCESS_FINE_LOCATION), наданий вашому застосунку під час виконання.
Установлення й активація термінала
Активацію виконують один раз для кожного пристрою. Активація пов'язує плагін Tapaya Terminal із тим самим обліковим записом мерчанта, під яким автентифіковано ваш застосунок.
if (!Accept.plugin.isInstalled()) {
// Opens the store listing so the user can install Tapaya Terminal
Accept.plugin.install()
return
}
// suspend; launches the plugin's activation flow and waits for the outcome
when (val result = Accept.plugin.activateTerminal()) {
ActivateTerminalResult.Success -> { /* device linked, ready to take payments */ }
ActivateTerminalResult.Canceled -> { /* user backed out */ }
is ActivateTerminalResult.Failed -> result.reason // ActivateTerminalFailureReason
}ActivateTerminalFailureReason набуває одного зі значень INVALID_INTENT, HANDOVER_FAILED, CONFIRM_FAILED,
MERCHANT_NOT_ONBOARDED або UNKNOWN.
Примітка
Accept.plugin.isInstalled() повертає false, коли плагін Tapaya Terminal відсутній. Поточний стан плагіна
можна будь-коли дізнатися за допомогою Accept.plugin.status(); відповідно до нього захистіть свій платіжний UI.
activateTerminal() також викидає NoTidsAvailableForMerchant, коли в мерчанта не залишилося жодного непризначеного термінала
(TID) для активації, окремо від наведених вище результатів ActivateTerminalResult.Failed. Обгорніть виклик у
try-catch, якщо хочете окремо показувати цей випадок, наприклад запропонувати мерчанту звільнити або
запросити ще один TID.
Перевірка оновлень плагіна
Accept.plugin.updateInfo() повідомляє, чи доступна в Google Play новіша збірка плагіна Tapaya Terminal,
щоб ви могли запропонувати мерчанту оновитися до того, як платіж наштовхнеться на розбіжність версій.
when (val info = Accept.plugin.updateInfo()) {
PluginUpdateInfo.UpToDate -> { /* nothing to do */ }
is PluginUpdateInfo.UpdateAvailable -> promptUpdate(info.priority) // Int, higher is more urgent
PluginUpdateInfo.Unknown -> { /* not installed, or an older plugin build that predates this check */ }
}Примітка
SDK ніколи не звертається до Google Play напряму; застосунок-плагін сам перевіряє оновлення через Play Core і повідомляє результат
своїм наявним каналом статусу. Збірка плагіна, старіша за ту, у якій з'явилася ця функція, завжди повідомляє
Unknown.
Створення платежу
Суми завжди вказуються в найменшій одиниці валюти: гелерах для CZK, центах для EUR. 1500 з
валютою "CZK" стягує 15,00 Kč. Валюти вказуються кодами ISO 4217.
pay() повертає Flow<PaymentEvent>; збирайте його, щоб стежити за перебігом і кінцевим результатом. Він ніколи не викидає винятків;
збої надходять як PaymentEvent.CreationFailed.
Accept.payments.pay(amount = 1500, currency = "CZK") // 15.00 Kč
.collect { event ->
when (event) {
PaymentEvent.Creating -> { /* registering the payment */ }
is PaymentEvent.Created -> { /* backend payment created: event.paymentToken */ }
PaymentEvent.Launched -> { /* Tapaya Terminal is in the foreground */ }
is PaymentEvent.Result -> when (val r = event.payResult) {
is PayResult.Success -> showReceipt(r.paymentToken, r.authCode)
is PayResult.Declined -> showDeclined(r.metadata)
is PayResult.Canceled -> showCancelled(r.metadata)
is PayResult.Failed -> showUnexpected(r.reason) // PayFailureReason
}
is PaymentEvent.CreationFailed -> handleError(event.cause) // AcceptException
}
}Параметри:
| Параметр | Тип | Обов'язковий | Опис |
|---|---|---|---|
amount | Long | ✓ | Сума до стягнення в найменшій одиниці валюти currency (наприклад, центи для USD, гелери для CZK). |
currency | String | ✓ | Код валюти ISO 4217. |
paymentToken | String? | Відновлює наявну платіжну сесію замість того, щоб починати нову. Значення за замовчуванням: null. | |
receiptConfig | PaymentReceiptConfig? | Перевизначає екран чека, що показується після схваленого платежу, лише для цього виклику. Значення за замовчуванням: null, тоді використовується типове налаштування, задане через Accept.payments.setOptions(). | |
nfcPosition | NfcPositionConfig? | Перевизначає, куди вказує анімація прикладання картки, лише для цього виклику. Значення за замовчуванням: null, тоді використовується типове налаштування, задане через Accept.payments.setOptions(). | |
metadata | Map<String, String> | Дані у форматі «ключ-значення», прикріплені до платежу. Надсилаються плагіну Tapaya Terminal в intent PAY і повертаються у відповідному PayResult, якщо плагін їх повертає. Значення за замовчуванням: emptyMap(). | |
tip | Long? | Фіксовані чайові в найменшій одиниці валюти currency. Якщо задано, плагін пропускає власний інтерактивний екран чайових і стягує amount + tip. Значення за замовчуванням: null (плагін запитує платника про чайові). | |
transactionTimeout | Duration? | Перевизначає, скільки часу термінал чекає на прикладання картки, лише для цього виклику. Значення за замовчуванням: null, тоді використовується типове налаштування, задане через Accept.payments.setOptions(). Див. Налаштування тайм-ауту транзакції. | |
display | PaymentDisplay? | Перевизначає, на якому екрані працює платіжний UI плагіна, лише для цього виклику. Значення за замовчуванням: null, тоді використовується типове налаштування, задане через Accept.payments.setOptions(). Див. Вибір дисплея. |
Поки платіж триває, плагін Tapaya Terminal перебуває на передньому плані. Коли платіж завершується, ваш застосунок
автоматично відновлює роботу. Щоб відновити перерваний платіж, передайте його paymentToken назад у
pay(amount, currency, paymentToken).
Повернення metadata не гарантоване
metadata повертаються в PayResult лише тоді, коли плагін їх повертає, тож розглядайте їх як додатковий контекст без гарантій
(наприклад, ID замовлення для власного логування), а не як дані, від отримання яких залежить ваш застосунок.
Вибір валюти
pay() стягує оплату у валюті currency, яку ви передаєте як код ISO 4217 великими літерами, а amount вказується в найменшій
одиниці цієї валюти. На сторінці Підтримувані валюти наведено всі валюти, які підтримує Tapaya, але мерчанту
зазвичай увімкнено лише деякі з них.
Викличте Accept.merchant.availableCurrencies(), щоб отримати коди ISO 4217, увімкнені для мерчанта. Платіж у будь-якій іншій
валюті завершується помилкою CurrencyNotAvailableForMerchant.
Кожна валюта також має мінімальну суму платежу, яка діє на рівні всієї платформи. Поточні мінімуми можна отримати за допомогою
Accept.sdk.minimumAmounts(), який повертає List<MinimumAmount> (currency: String, amount: Long, у найменшій
одиниці валюти). Виклик pay() із сумою, нижчою за цей мінімум, завершується помилкою
PaymentEvent.CreationFailed(AmountBelowMinimum(minimum, currency)). Повний перелік винятків див. у розділі
Обробка помилок.
Налаштування екрана чека
receiptConfig визначає, що плагін Tapaya Terminal показує платнику після схваленого платежу, як довго
це залишається на екрані та чи друкується чек. Він не впливає на відхилені й сторновані платежі чи платежі, що очікують
на сторнування; для них завжди показується власний екран результату незалежно від цього налаштування.
PaymentReceiptConfig | Опис |
|---|---|
DismissImmediately | Платнику нічого не показується. |
Show(showQrCodeReceipt, dismissBehavior, printBehavior) | Показує екран чека. showQrCodeReceipt: Boolean = true вмикає або вимикає QR-код із посиланням на чек. dismissBehavior: DismissBehavior = DismissBehavior.Manual визначає, як екран закривається: Manual (залишається, доки платник його не закриє) або Delayed(delay: Duration) (закривається автоматично після delay, якщо платник не закрив його раніше). printBehavior: ReceiptPrintBehavior = ReceiptPrintBehavior.OnDemand керує паперовим чеком: Automatic (друкується одразу, щойно з'являється екран), OnDemand (лише коли друк запускає касир) або Disabled (ніколи не друкується). |
Задайте типове налаштування для всього пристрою один раз за допомогою Accept.payments.setOptions() і перевизначайте його для окремого платежу
через pay(..., receiptConfig = ...). Доки setOptions() не викликано, типовим значенням є Show із QR-чеком, ручним
закриттям і друком на вимогу, що відповідає власному резервному налаштуванню плагіна.
// Set a default, e.g. once at app startup
Accept.payments.setOptions(
receiptConfig = PaymentReceiptConfig.Show(
showQrCodeReceipt = true,
dismissBehavior = DismissBehavior.Delayed(30.seconds),
printBehavior = ReceiptPrintBehavior.Automatic,
),
)
// Override it for a single payment
Accept.payments.pay(
amount = 1500,
currency = "CZK",
receiptConfig = PaymentReceiptConfig.DismissImmediately,
)Розташування анімації прикладання NFC
nfcPosition є підказкою, яка повідомляє плагіну Tapaya Terminal, де розташована NFC-антена цього пристрою, щоб він міг
правильно розмістити анімацію «прикладіть тут». Плагін спершу сам визначає розташування антени (виявленням на платформі в реальному часі,
потім за вбудованою базою даних пристроїв, а потім за будь-якими налаштуваннями, які мерчант задав безпосередньо в плагіні); вказуйте це значення лише
тоді, коли потрібно перевизначити результат цього визначення, наприклад на пристрої, який плагін не може розпізнати або для якого в його базі даних указано неправильні дані.
NfcPositionConfig | Опис |
|---|---|
Unspecified | Без перевизначення (за замовчуванням). Плагін сам визначає розташування й зберігає будь-яке розташування, яке мерчант уже налаштував у ньому. |
OnDevice(surface, xFraction, yFraction) | Антена розташована на самому пристрої; анімація вказує на точку (xFraction, yFraction) на поверхні surface, якою є OnDeviceSurface.BACK або OnDeviceSurface.FRONT_SCREEN. |
ExternalAbove | Окремий модуль зчитувача розташований над телефоном (конструкція smart-POS); на пристрої немає точки, на яку слід указувати. |
Задайте типове налаштування для всього пристрою за допомогою Accept.payments.setOptions(nfcPosition = ...) або перевизначте його для окремого виклику
через pay(..., nfcPosition = ...). setOptions() змінює лише передані вами параметри, тож окремий виклик setOptions(nfcPosition = ...)
не порушить раніше задане типове значення receiptConfig, і навпаки.
Налаштування тайм-ауту транзакції
transactionTimeout визначає, скільки часу плагін Tapaya Terminal чекає, поки клієнт прикладе картку, перш ніж
припинити спробу. Він приймає будь-яке значення Duration від нуля до 120 секунд; будь-яке значення поза цим діапазоном надходить як
TransactionTimeoutOutOfRange(transactionTimeout) через PaymentEvent.CreationFailed, оскільки pay() ніколи не викидає винятків
синхронно.
Accept.payments.pay(
amount = 1500,
currency = "CZK",
transactionTimeout = 90.seconds,
)Залиште типове значення null, щоб використовувати власний резервний тайм-аут супутнього застосунку (близько 60 секунд), або задайте типове
значення для всього пристрою за допомогою Accept.payments.setOptions(transactionTimeout = ...).
Вибір дисплея
На двосторонньому («double-sided») пристрої одна панель повернута до мерчанта, а друга, клієнтська панель, повернута
до платника. Accept.displays повідомляє, які дисплеї пристрій має зараз, а параметр display вказує плагіну,
на якій панелі запускати свій платіжний UI.
if (Accept.displays.isDualScreen) {
val customerPanel = Accept.displays.customerFacing() // AcceptDisplay?, null on single-screen devices
}
Accept.payments.pay(
amount = 1500,
currency = "CZK",
display = PaymentDisplay.CustomerFacing,
)PaymentDisplay | Опис |
|---|---|
Default | Поточний дисплей застосунку-хоста (типова поведінка, без змін на пристроях з одним екраном). |
CustomerFacing | Вторинна панель двостороннього пристрою. |
Specific(displayId) | Конкретний дисплей за id, який повертає Accept.displays.all(). |
Ціль, яку неможливо застосувати, наприклад CustomerFacing на пристрої з одним екраном, непомітно замінюється на
Default. Задайте типове налаштування для всього пристрою за допомогою Accept.payments.setOptions(display = ...) або перевизначте його для окремого виклику
через pay(..., display = ...).
Задайте ціль перед автентифікацією
Попередня підготовка термінала (warm-up), яку запускає Accept.auth.authenticate(), використовує ту саму ціль, тож викличте
setOptions(display = ...) перед authenticate(), якщо хочете, щоб і сама підготовка не відбувалася на
екрані мерчанта.
Демонстраційний застосунок :dualscreen показує повний процес оплати на двох панелях: мерчант працює на основній панелі, а
PaymentDisplay.CustomerFacing виводить платіжний UI плагіна на екран клієнта.
Результат платежу
Кінцева подія PaymentEvent.Result містить PayResult. Кожен варіант містить metadata, повертаючи те, що ви
передали в pay(), якщо плагін їх повертає (може бути null):
PayResult | Значення |
|---|---|
Success(paymentToken, metadata, authCode, receiptDetails) | Авторизовано й кошти списано. Кошти буде зараховано мерчанту під час розрахунку. authCode є кодом авторизації процесора, який повідомляє термінал; має інформаційний характер і залежно від процесора може бути відсутнім. |
Declined(metadata, receiptDetails) | Відхилено платіжним процесором або банком-емітентом картки. |
Canceled(metadata) | Скасовано клієнтом або мерчантом до завершення. |
Failed(reason, metadata) | Не завершено; reason має тип PayFailureReason. |
PayFailureReason набуває одного зі значень INVALID_INTENT, NOT_ACTIVATED, TERMINAL_INIT_FAILED, PAYMENT_SETUP_FAILED,
PLUGIN_UNAVAILABLE або UNKNOWN.
Отримання даних чека
Success і Declined містять необов'язкове поле receiptDetails: ReceiptDetails? зі структурованими даними карткового чека,
які повідомляє термінал. Воно призначене для тих, хто друкує власний офіційний картковий чек, а не покладається лише на
розміщену сторінку PaymentStatus.receiptUrl.
| Поле | Тип | Опис |
|---|---|---|
merchantId | String? | ID мерчанта, який повідомляє процесор. |
terminalId | String? | ID термінала, який повідомляє процесор. |
cardBrand | String? | Бренд картки, наприклад Visa або Mastercard. |
appLabel | String? | Мітка застосунку EMV. |
aid | String? | Ідентифікатор застосунку EMV. |
maskedPan | String? | Маскований номер картки. |
responseCode | String? | Код відповіді процесора. |
transactionTimestamp | String? | Позначка часу транзакції у форматі ISO-8601. |
Кожне поле є необов'язковим і заповнюється без гарантій; код, що викликає, має бути готовим до того, що будь-яке з них або саме receiptDetails
буде відсутнім.
Отримання даних платежу
PayResult.Success надає вам paymentToken. Повний запис, зокрема receiptUrl, запитану
суму й суму розрахунку, а також позначки часу, можна отримати за допомогою Accept.payments.status():
val status: PaymentStatus = Accept.payments.status(paymentToken)
status.state // PENDING, SUCCESS, CANCELLED, REFUNDED, FAILED, ACTION_NEEDED
status.receiptUrl // for your receipt UIЗупинити платіж, що триває, можна за допомогою Accept.payments.cancel(paymentToken), який повертає підсумковий PaymentState.
Повернення коштів
Повернути кошти за завершеним платежем можна за допомогою Accept.payments.refund(paymentToken, reason), який також повертає підсумковий
PaymentState.
val state: PaymentState = Accept.payments.refund(paymentToken, RefundReason.CUSTOMER_REQUEST)RefundReason набуває одного зі значень CUSTOMER_REQUEST, DUPLICATE, FRAUDULENT або EXPIRED_CHARGE.
Обробка помилок
Платіж, який дійшов до термінала, але не був успішним, не є винятком; він надходить як
PaymentEvent.Result із PayResult.Declined, Canceled або Failed(reason). Збої, що не дають
створити платіж, надходять як PaymentEvent.CreationFailed(cause), де cause є типізованим
AcceptException. Винятки, специфічні для цієї інтеграції:
| Виняток | Коли виникає | Що робити |
|---|---|---|
PluginUnavailable | Плагін Tapaya Terminal не встановлено або не вдалося підключитися до його сервісу. | Запропонуйте встановлення за допомогою Accept.plugin.install(). |
CurrencyNotAvailableForMerchant(code) | Для мерчанта не налаштовано запитану валюту. | Стягуйте оплату в увімкненій валюті або оновіть конфігурацію мерчанта. |
AmountBelowMinimum(minimum, currency) | Сума нижча за налаштований мінімум. | Стягуйте суму, що дорівнює minimum або перевищує його. |
TransactionTimeoutOutOfRange(transactionTimeout) | transactionTimeout виходив за межі діапазону від 0 до 120 секунд. | Передайте значення в межах діапазону або не вказуйте його, щоб використати значення за замовчуванням. |
LocationPermissionRequired | Вашому застосунку не надано дозволу на доступ до геолокації. | Запросіть ACCESS_FINE_LOCATION, а потім повторіть спробу. |
LocationUnavailable / LocationTimeout | Не вдалося визначити геолокацію пристрою. | Попросіть користувача ввімкнути геолокацію та повторіть спробу. |
SessionExpired | Строк дії автентифікованої сесії минув. | Повторно автентифікуйтеся за допомогою Accept.auth.authenticate(). |
NoInternetConnection | Немає підключення до мережі. | Попросіть користувача знову підключитися та повторіть спробу. |
PaymentInProgress | Попередній виклик pay() досі очікує на результат. | Зачекайте, доки поточний платіж завершиться. |
Accept.payments.status(), cancel() і refund() є функціями suspend і викидають ці винятки
(а не надсилають події); обгортайте їх у try-catch. status()/cancel()/refund() також викидають
PaymentNotFound для невідомого токена. Повний перелік див. у розділі
Обробка помилок.
Тайм-аути
Платіж карткою потрібно завершити на терміналі невдовзі після його початку. Якщо клієнт так і не прикладе картку або
плагін Tapaya Terminal закриється посеред платежу, платіж завершиться як PayResult.Canceled або
PayResult.Failed, а відповідний платіж буде скасовано на стороні сервера, і він ніколи не буде проведений. Завжди перевіряйте
кінцевий стан за допомогою Accept.payments.status(), перш ніж вважати платіж завершеним.
Інтеграція без SDK
Якщо ви не можете вбудувати Accept SDK (кросплатформний фреймворк, тонка POS-оболонка, вебкаса), ви можете керувати Tapaya Terminal напряму за допомогою deeplink. Розподіл роботи змінюється: ви створюєте платіж через Tapaya API і ви опитуєте його статус, щоб отримати результат; Tapaya Terminal відповідає лише за взаємодію з карткою.
Послідовність:
- Створіть платіж зі свого бекенда через Payments API і збережіть
його
paymentId. - Відкрийте Tapaya Terminal із цим
paymentIdза допомогою платіжного deeplink. - Опитуйте статус платежу через Payments API, доки він не вийде зі стану
PENDING. Deeplink не має зворотного каналу, тому опитування є єдиним надійним джерелом результату.
Примітка
Tapaya Terminal має бути встановлено, у ньому має бути виконано вхід в обліковий запис мерчанта, і його має бути налаштовано для валюти платежу. У цьому режимі немає кроку активації; мерчантом, відповідальним за транзакцію (merchant of record), є обліковий запис, у який виконано вхід. За потреби виконайте вхід мерчанта за допомогою deeplink для входу.
Схеми deeplink
Схема визначає, яка збірка Tapaya Terminal обробляє посилання:
| Середовище | Схема |
|---|---|
| Робоче середовище | tapaya-accept |
| Пісочниця (sandbox) | tapaya-accept-sandbox |
Якщо посилання не відкривається, цю збірку Tapaya Terminal не встановлено.
Платіжний deeplink
tapaya-accept://pay?paymentId=<paymentId>&amount=<amount>¤cy=<ISO4217>| Параметр запиту | Обов'язковий | Примітки |
|---|---|---|
paymentId | Так | Платіж, створений на кроці 1. |
amount | Так | Сума в основних одиницях як ціле число: amount=15 стягує 15,00. Зверніть увагу, що це відрізняється від SDK, який приймає найменші одиниці. |
currency | Так | Код валюти ISO 4217, наприклад CZK. |
val uri = Uri.parse("tapaya-accept://pay?paymentId=$paymentId&amount=15¤cy=CZK")
try {
startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} catch (e: ActivityNotFoundException) {
// Tapaya Terminal is not installed, send the user to Google Play
}Deeplink для входу
Виконайте вхід мерчанта в Tapaya Terminal за допомогою його merchantToken, ідентифікатора мерчанта, який ви задаєте через POST /merchant/auth/register:
tapaya-accept://login?token=<merchantToken>| Параметр запиту | Обов'язковий | Примітки |
|---|---|---|
token | Так | merchantToken мерчанта. Це ваш незмінний ID мерчанта, а не короткоживучий токен SDK з POST /merchant/auth/login. |
activationCode | Ні | Попередньо заповнює код активації термінала. |
Тестування інтеграції
- Ініціалізуйте з
isProduction = false(значення за замовчуванням); платежі в пісочниці ніколи не переказують реальні гроші. - Установіть плагін Tapaya Terminal на тестовий пристрій, увійдіть як мерчант пісочниці та виконайте активацію.
- Проведіть платіж на невелику суму й перевірте весь сценарій
Successвід початку до кінця, зокрема ваш UI чека. - Перевірте сценарії збоїв: відхиліть платіж на терміналі, скасуйте інший і почніть ще один у непідтримуваній
валюті; ваша обробка
PayResult/CreationFailedмає охоплювати кожен із них.
Коли будете готові перейти в робочий режим, задайте isProduction = true в initialize і автентифікуйтеся з обліковими даними
робочого середовища. Код платежів не змінюється.