Skip to content
SDK preview. Packages are not publicly published yet — use REST / cURL, or the local development artifacts.

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:

Text
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:

Kotlin
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.

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:

AndroidManifest.xml · XML
<?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:

Kotlin
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#

MainActivity.kt · Kotlin
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.