# Android

> Open Devzity 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 Devzity's payments API. It talks to **your small backend**, and that backend talks to Devzity:

```text
Android app ──▶ your small backend ──(Devzity secret key)──▶ Devzity
     ▲                   │
     └──── checkoutUrl ──┘
Android app ──▶ DevzityCheckout.open(checkoutUrl) ──▶ customer pays ──▶ your return link ──▶ app
Android app ──▶ your backend ──▶ "is my order paid?"      (the authoritative answer)
```

**The Devzity 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](https://docs.devzity.com/v1/mobile-backend.md).

## 1. Add the SDK

The Android library is `com.devzity.checkout` (minSdk 21). It has one job: open the checkout page and read the return link.

**Android SDK preview:** This package is not publicly published yet, so there is nothing to install from a registry.

- Build the AAR from the repository (`./gradlew :checkout:assembleRelease` in sdk/android), or include the `checkout` module in your project.

## 2. Open checkout

Ask your backend for a checkout URL for an **order** (it decides the amount), then open it:

```kotlin
DevzityCheckout.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, `DevzityCheckout.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:

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. Devzity's checkout opens in the browser. -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:label="Devzity 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 = "devzitysample://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="devzitysample" 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 = DevzityCheckout.parseReturn(intent.data)

when (result?.statusHint) {
    DevzityStatusHint.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 Devzity return, so you can pass every incoming link to it. The result has `paymentId`, `statusHint` and `returnUri`.

> **Warning:** `statusHint` is a **hint, never proof of payment.** Anyone can open your link with `devzity_status=PAID`. Never ship or unlock anything because of it. Refresh the order from **your backend** (which asks Devzity or receives its webhook).

## 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.devzity.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.devzity.checkout.DevzityCheckout
import com.devzity.checkout.DevzityStatusHint
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 { DevzityCheckout.open(activity = this, checkoutUrl = checkoutUrl) }
            } catch (e: Exception) {
                runOnUiThread { status.text = "Could not start payment: ${e.message}" }
            }
        }
    }

    private fun handleReturn(intent: Intent?) {
        val result = DevzityCheckout.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) {
            DevzityStatusHint.CANCELLED -> "Checkout was cancelled. Checking your order…"
            DevzityStatusHint.PAID, DevzityStatusHint.UNKNOWN -> "Checking your payment…"
        }
        thread {
            val current = backend.orderStatus(orderId) // authoritative
            runOnUiThread { status.text = "Order status: $current" }
        }
    }
}
```

The sample contains **no Devzity key**. It uses a mock backend so it runs offline, and can point at a real one.
