Android
Open PayTaka checkout from an Android app with Custom Tabs and handle the return link — without a secret key in your APK.
Your app never talks to PayTaka's payments API. It talks to your small backend, and that backend talks to PayTaka:
Android app ──▶ your small backend ──(PayTaka secret key)──▶ PayTaka
▲ │
└──── checkoutUrl ──┘
Android app ──▶ PayTakaCheckout.open(checkoutUrl) ──▶ customer pays ──▶ your return link ──▶ app
Android app ──▶ your backend ──▶ "is my order paid?" (the authoritative answer)The PayTaka secret key stays on your backend. This SDK never stores or uses it. Don't have a backend yet? Read I only have an app.
1. Add the SDK#
The Android library is com.paytaka.checkout (minSdk 21). It has one job: open the checkout page and read the return link.
2. Open checkout#
Ask your backend for a checkout URL for an order (it decides the amount), then open it:
PayTakaCheckout.open(activity = this, checkoutUrl = checkoutUrl)The SDK opens a Chrome Custom Tab (falling back to the default browser). It only accepts real https URLs and refuses javascript:, data:, file:, intent: and custom schemes. For local development only, PayTakaCheckout.allowInsecureLocalhostForDebugging() also accepts http://localhost and the emulator host 10.0.2.2 — never call it in a release build.
3. Receive the return link#
Have your backend set the payment's return_url to your app's link. Register it in the manifest — a custom scheme is the simplest; an Android App Link (an https link you own, verified with assetlinks.json) is preferred once you have a domain:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Only needed to call YOUR backend. PayTaka's checkout opens in the browser. -->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="PayTaka Sample"
android:allowBackup="false"
android:theme="@android:style/Theme.DeviceDefault.Light">
<!-- singleTask: the return link brings the existing Activity back to the
front and delivers the link through onNewIntent. -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Custom scheme: the simplest way to receive the return link.
Your backend sets return_url = "paytakasample://payment-return". -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="paytakasample" android:host="payment-return" />
</intent-filter>
<!-- Android App Links (preferred once you own a domain): an https link
that opens your app directly, verified via assetlinks.json.
Uncomment and use your own domain.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="shop.example.com" android:pathPrefix="/payment-return" />
</intent-filter>
-->
</activity>
</application>
</manifest>Then read it in the activity that handles the link:
val result = PayTakaCheckout.parseReturn(intent.data)
when (result?.statusHint) {
PayTakaStatusHint.PAID -> {
// Only a hint. Refresh the order from YOUR backend before showing anything as paid.
}
else -> Unit
}parseReturn returns null for any link that isn't a PayTaka return, so you can pass every incoming link to it. The result has paymentId, statusHint and returnUri.
Lifecycle: what closing checkout means#
The user can press back, close the browser, or your app can be killed while they pay. The SDK never treats any of that as a failed payment — it holds no payment state. Keep your order id (in saved instance state, or on your server), and when the app comes back — through the link or by the user reopening it — ask your backend for the order's status.
Sample app#
package com.paytaka.sample
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import com.paytaka.checkout.PayTakaCheckout
import com.paytaka.checkout.PayTakaStatusHint
import kotlin.concurrent.thread
class MainActivity : Activity() {
// Swap for HttpMerchantBackend("https://your-backend.example.com") to try a real payment.
private val backend: MerchantBackend = FakeMerchantBackend()
// The order being paid. Kept across process death: the user may be in the browser for a while.
private var orderId = "order-1001"
private lateinit var status: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.getString("orderId")?.let { orderId = it }
status = TextView(this).apply { text = "Tap Pay to start a test payment for $orderId." }
val pay = Button(this).apply {
text = "Pay for $orderId"
setOnClickListener { startPayment() }
}
setContentView(LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 48, 48, 48)
addView(pay)
addView(status)
})
handleReturn(intent) // the app may have been started by the return link
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("orderId", orderId)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleReturn(intent)
}
private fun startPayment() {
thread {
try {
// 1. YOUR backend loads the order, decides the amount, creates the payment with its
// secret key and returns checkout_url. The app sends only the order id.
val checkoutUrl = backend.createPaymentCheckoutUrl(orderId)
// 2. The app only opens it.
runOnUiThread { PayTakaCheckout.open(activity = this, checkoutUrl = checkoutUrl) }
} catch (e: Exception) {
runOnUiThread { status.text = "Could not start payment: ${e.message}" }
}
}
}
private fun handleReturn(intent: Intent?) {
val result = PayTakaCheckout.parseReturn(intent?.data) ?: return
// statusHint is only a HINT. Never fulfil anything from it: show a neutral message and
// refresh the order from YOUR backend.
status.text = when (result.statusHint) {
PayTakaStatusHint.CANCELLED -> "Checkout was cancelled. Checking your order…"
PayTakaStatusHint.PAID, PayTakaStatusHint.UNKNOWN -> "Checking your payment…"
}
thread {
val current = backend.orderStatus(orderId) // authoritative
runOnUiThread { status.text = "Order status: $current" }
}
}
}The sample contains no PayTaka key. It uses a mock backend so it runs offline, and can point at a real one.