> ## Documentation Index
> Fetch the complete documentation index at: https://dev.moonpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Android

> Manual frame integration for Android using WebView and Kotlin.

Use `WebView` to embed frames in native Android applications. The WebView communicates with frames via JavaScript interfaces and `evaluateJavascript`.

<Callout icon="book" iconType="regular">
  Read the [manual integration
  overview](/platform/guides/manual-integration/overview) for core concepts
  before you continue.
</Callout>

## Setup

### Dependencies

The examples below use [Tink](https://github.com/google/tink) for cryptographic operations, but you can use any library that supports X25519 and AES-GCM.

```kotlin theme={null}
// build.gradle.kts (app level)
dependencies {
    implementation("com.google.crypto.tink:tink-android:1.12.0")
}
```

### Key generation

```kotlin theme={null}
import com.google.crypto.tink.subtle.X25519

data class MoonPayKeyPair(
    val privateKey: ByteArray,
    val publicKeyHex: String
)

object MoonPayCrypto {
    fun generateKeyPair(): MoonPayKeyPair {
        val privateKey = X25519.generatePrivateKey()
        val publicKey = X25519.publicFromPrivate(privateKey)
        val publicKeyHex = publicKey.joinToString("") { "%02x".format(it) }

        return MoonPayKeyPair(privateKey, publicKeyHex)
    }
}
```

### Decryption utility

```kotlin theme={null}
import android.util.Base64
import com.google.crypto.tink.subtle.Hkdf
import com.google.crypto.tink.subtle.X25519
import org.json.JSONObject
import javax.crypto.Cipher
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

object MoonPayDecryptor {
    fun decrypt(encryptedValue: String, privateKey: ByteArray): String {
        // The frame returns the encrypted payload as a base64-encoded
        // JSON string. Decode the base64 first, then parse the JSON.
        val decodedJson = String(Base64.decode(encryptedValue, Base64.DEFAULT), Charsets.UTF_8)
        val encrypted = JSONObject(decodedJson)

        val iv = encrypted.getString("iv").hexToByteArray()
        val ephemeralPublicKey = encrypted.getString("ephemeralPublicKey").hexToByteArray()
        val ciphertext = encrypted.getString("ciphertext").hexToByteArray()

        // Derive shared secret using X25519
        val sharedSecret = X25519.computeSharedSecret(privateKey, ephemeralPublicKey)

        // Derive AES key using HKDF
        val aesKey = Hkdf.computeHkdf(
            "HMACSHA256",
            sharedSecret,
            ByteArray(0),
            ByteArray(0),
            32
        )

        // Decrypt using AES-GCM
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        val keySpec = SecretKeySpec(aesKey, "AES")
        val gcmSpec = GCMParameterSpec(128, iv)

        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec)
        val decrypted = cipher.doFinal(ciphertext)

        return String(decrypted, Charsets.UTF_8)
    }

    private fun String.hexToByteArray(): ByteArray {
        return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
    }
}
```

### Base frame fragment

Create a reusable base fragment for frame communication:

```kotlin theme={null}
import android.annotation.SuppressLint
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.fragment.app.Fragment
import org.json.JSONObject
import java.util.UUID

abstract class MoonPayFrameFragment : Fragment() {
    protected lateinit var webView: WebView
    protected val channelId: String = UUID.randomUUID().toString()

    private val handler = Handler(Looper.getMainLooper())

    companion object {
        const val FRAME_ORIGIN = "https://blocks.moonpay.com"
    }

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        webView = WebView(requireContext()).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true

            addJavascriptInterface(MoonPayBridge(), "MoonPayBridge")
            // Required for the test-mode Apple Pay flow — see "Handle
            // JavaScript dialogs" below.
            webChromeClient = MoonPayChromeClient(this@MoonPayFrameFragment)
        }

        return webView
    }

    protected fun loadFrame(path: String, params: Map<String, String>) {
        val queryString = params.entries.joinToString("&") { "${it.key}=${it.value}" }
        val url = "$FRAME_ORIGIN$path?$queryString"

        webView.loadUrl(url)
    }

    protected fun sendMessage(kind: String, payload: JSONObject? = null) {
        val message = JSONObject().apply {
            put("version", 2)
            put("meta", JSONObject().put("channelId", channelId))
            put("kind", kind)
            payload?.let { put("payload", it) }
        }

        // Re-encode the JSON as a string literal. The frame's bridge
        // listens for `MessageEvent`s whose `data` is a string and
        // ignores anything else, so we must post a string — not an
        // object literal.
        val stringLiteral = JSONObject.quote(message.toString())

        val script = "window.postMessage($stringLiteral, '*');"
        webView.evaluateJavascript(script, null)
    }

    private fun handleMessage(data: JSONObject) {
        val meta = data.optJSONObject("meta") ?: return
        if (meta.optString("channelId") != channelId) return

        val kind = data.optString("kind")

        if (kind == "handshake") {
            sendMessage("ack")
            onFrameHandshakeComplete()
        }

        onFrameMessage(kind, data.optJSONObject("payload"))
    }

    // Abstract methods for subclasses
    protected abstract fun onFrameMessage(kind: String, payload: JSONObject?)
    protected abstract fun onFrameHandshakeComplete()

    override fun onDestroyView() {
        super.onDestroyView()
        webView.destroy()
    }

    inner class MoonPayBridge {
        @JavascriptInterface
        fun postMessage(data: String) {
            handler.post {
                try {
                    val json = JSONObject(data)
                    handleMessage(json)
                } catch (e: Exception) {
                    // Ignore malformed messages
                }
            }
        }
    }
}
```

### Handle JavaScript dialogs

In [test mode](/overview/test-mode#apple-pay), the Apple Pay frame renders a mock button and uses `window.confirm` to simulate the Apple Pay payment sheet. Android's `WebView` returns `false` for `window.confirm`, `alert`, and `prompt` unless you attach a `WebChromeClient` that handles them — so without this, every test transaction silently comes back with `status: "failed"`.

Surface the simulated payment sheet with an `AlertDialog`:

```kotlin theme={null}
import android.app.AlertDialog
import android.webkit.JsResult
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.fragment.app.Fragment

class MoonPayChromeClient(private val fragment: Fragment) : WebChromeClient() {
    override fun onJsConfirm(
        view: WebView?,
        url: String?,
        message: String?,
        result: JsResult
    ): Boolean {
        val context = fragment.context ?: return false
        AlertDialog.Builder(context)
            .setTitle("Test Mode")
            .setMessage(message?.takeIf { it.isNotEmpty() } ?: "Simulate Apple Pay?")
            .setPositiveButton("OK") { _, _ -> result.confirm() }
            .setNegativeButton("Cancel") { _, _ -> result.cancel() }
            .setOnCancelListener { result.cancel() }
            .show()
        return true
    }
}
```

`OK` simulates a successful test transaction (the frame emits `complete` with a non-failed status); `Cancel` simulates a failed transaction (the frame emits `complete` with `status: "failed"`).

<Callout icon="info" iconType="regular">
  Attach the `WebChromeClient` even if you only plan to ship live mode. The
  default `WebView` behaviour applies to any `window.confirm`, `alert`, or
  `prompt` the frame might surface, and makes test-mode debugging impossible
  without it.
</Callout>

***

## Check frame

The check frame verifies whether a customer already has an active connection. It's headless — no UI is rendered. Use it to skip the connect flow for returning customers. See [check frame reference](/platform/frames/check) for event details.

### Check fragment

```kotlin theme={null}
import org.json.JSONObject

interface CheckFrameListener {
    fun onCheckActive(accessToken: String, clientToken: String, expiresAt: String)
    fun onCheckConnectionRequired(accessToken: String, clientToken: String)
    fun onCheckFailed(status: String, reason: String?)
    fun onCheckError(code: String, message: String)
}

class MoonPayCheckFragment : MoonPayFrameFragment() {
    private lateinit var keyPair: MoonPayKeyPair
    private var sessionToken: String? = null
    var listener: CheckFrameListener? = null

    companion object {
        private const val ARG_SESSION_TOKEN = "sessionToken"

        fun newInstance(sessionToken: String): MoonPayCheckFragment {
            return MoonPayCheckFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_SESSION_TOKEN, sessionToken)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sessionToken = arguments?.getString(ARG_SESSION_TOKEN)
        keyPair = MoonPayCrypto.generateKeyPair()
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/check-connection", mapOf(
            "sessionToken" to sessionToken!!,
            "publicKey" to keyPair.publicKeyHex,
            "channelId" to channelId
        ))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "complete" -> handleComplete(payload)
            "error" -> {
                listener?.onCheckError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete, checking connection status
    }

    private fun handleComplete(payload: JSONObject?) {
        val status = payload?.optString("status") ?: return

        when (status) {
            "active" -> {
                val credentials = payload.optString("credentials")
                val expiresAt = payload.optString("expiresAt")
                val customer = payload.optJSONObject("customer")
                val country = customer?.optString("country")
                val administrativeArea = customer?.optString("administrativeArea")
                val area = customer?.optString("area")
                // payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.

                try {
                    val decryptedPayload = MoonPayDecryptor.decrypt(credentials, keyPair.privateKey)
                    val credentials = JSONObject(decryptedPayload)
                    val accessToken = credentials.getString("accessToken")
                    val clientToken = credentials.getString("clientToken")
                    listener?.onCheckActive(accessToken, clientToken, expiresAt)
                } catch (e: Exception) {
                    listener?.onCheckError("decryption", "Failed to decrypt credentials")
                }
            }
            "connectionRequired" -> {
                val encryptedCredentials = payload.optString("credentials")
                // payload.optBoolean("mismatch") is true when the session's email and phone resolve to different customers — route through the connect flow first.

                try {
                    val decryptedPayload = MoonPayDecryptor.decrypt(encryptedCredentials, keyPair.privateKey)
                    val anonymousCredentials = JSONObject(decryptedPayload)
                    val accessToken = anonymousCredentials.getString("accessToken")
                    val clientToken = anonymousCredentials.getString("clientToken")
                    listener?.onCheckConnectionRequired(accessToken, clientToken)
                } catch (e: Exception) {
                    listener?.onCheckError("decryption", "Failed to decrypt anonymous credentials")
                }
            }
            "pending", "unavailable", "failed" -> {
                listener?.onCheckFailed(status, payload.optString("reason"))
            }
        }
    }
}
```

### Usage

```kotlin theme={null}
class SplashActivity : AppCompatActivity(), CheckFrameListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        val fragment = MoonPayCheckFragment.newInstance("your-session-token")
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .replace(R.id.frame_container, fragment)
            .commit()
    }

    override fun onCheckActive(accessToken: String, clientToken: String, expiresAt: String) {
        // Customer is already connected — skip to payment
        CredentialsManager.accessToken = accessToken
        CredentialsManager.clientToken = clientToken

        startActivity(Intent(this, PaymentActivity::class.java))
        finish()
    }

    override fun onCheckConnectionRequired(accessToken: String, clientToken: String) {
        // Store both tokens in memory, then show connect frame with the anonymous clientToken
        CredentialsManager.accessToken = accessToken
        CredentialsManager.clientToken = clientToken

        val intent = Intent(this, ConnectActivity::class.java).apply {
            putExtra("clientToken", clientToken)
        }
        startActivity(intent)
        finish()
    }

    override fun onCheckFailed(status: String, reason: String?) {
        // Handle terminal statuses (pending, unavailable, failed)
    }

    override fun onCheckError(code: String, message: String) {
        // Handle check errors
    }
}
```

***

## Connect frame

The connect frame establishes a customer connection to your application. See [connect frame reference](/platform/frames/connect) for event details.

### Connect fragment

```kotlin theme={null}
import org.json.JSONObject

interface ConnectFrameListener {
    fun onConnectComplete(accessToken: String, clientToken: String, expiresAt: String)
    fun onConnectFailed(status: String, reason: String?)
    fun onConnectError(code: String, message: String)
}

class MoonPayConnectFragment : MoonPayFrameFragment() {
    private lateinit var keyPair: MoonPayKeyPair
    private var clientToken: String? = null
    var listener: ConnectFrameListener? = null

    companion object {
        private const val ARG_CLIENT_TOKEN = "clientToken"

        fun newInstance(clientToken: String): MoonPayConnectFragment {
            return MoonPayConnectFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CLIENT_TOKEN, clientToken)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
        keyPair = MoonPayCrypto.generateKeyPair()
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/connect", mapOf(
            "clientToken" to clientToken!!,
            "publicKey" to keyPair.publicKeyHex,
            "channelId" to channelId
        ))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "complete" -> handleComplete(payload)
            "error" -> {
                listener?.onConnectError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete, waiting for customer interaction
    }

    private fun handleComplete(payload: JSONObject?) {
        val status = payload?.optString("status") ?: return

        when (status) {
            "active" -> {
                val credentials = payload.optString("credentials")
                val expiresAt = payload.optString("expiresAt")
                val customer = payload.optJSONObject("customer")
                val country = customer?.optString("country")
                val administrativeArea = customer?.optString("administrativeArea")
                val area = customer?.optString("area")
                // payload.capabilities.ramps.requirements.paymentDisclosures is deprecated.

                try {
                    val decryptedPayload = MoonPayDecryptor.decrypt(credentials, keyPair.privateKey)
                    val credentials = JSONObject(decryptedPayload)
                    val accessToken = credentials.getString("accessToken")
                    val clientToken = credentials.getString("clientToken")
                    listener?.onConnectComplete(accessToken, clientToken, expiresAt)
                } catch (e: Exception) {
                    listener?.onConnectError("decryption", "Failed to decrypt credentials")
                }
            }
            "pending", "unavailable", "failed" -> {
                listener?.onConnectFailed(status, payload.optString("reason"))
            }
        }
    }
}
```

### Usage with Activity

```kotlin theme={null}
class ConnectActivity : AppCompatActivity(), ConnectFrameListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        // The clientToken is the anonymous token passed from the check frame's
        // `connectionRequired` response.
        val clientToken = intent.getStringExtra("clientToken")!!
        val fragment = MoonPayConnectFragment.newInstance(clientToken)
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .replace(R.id.frame_container, fragment)
            .commit()
    }

    // ConnectFrameListener implementation

    override fun onConnectComplete(accessToken: String, clientToken: String, expiresAt: String) {
        // Store credentials in memory
        CredentialsManager.accessToken = accessToken
        CredentialsManager.clientToken = clientToken

        Toast.makeText(this, "Connected!", Toast.LENGTH_SHORT).show()

        // Navigate to next screen
        startActivity(Intent(this, PaymentActivity::class.java))
        finish()
    }

    override fun onConnectFailed(status: String, reason: String?) {
        AlertDialog.Builder(this)
            .setTitle("Connection $status")
            .setMessage(reason ?: "Please try again later.")
            .setPositiveButton("OK") { _, _ -> finish() }
            .show()
    }

    override fun onConnectError(code: String, message: String) {
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ -> finish() }
            .show()
    }
}
```

***

## Add Card frame

The add card frame lets a customer save a new card to their account. See [add card frame reference](/platform/frames/add-card) for event details.

### What you'll need

Before you initialize the add card frame, you need:

1. A `clientToken` from a successful [connect flow](#connect-frame)

### Add Card fragment

```kotlin theme={null}
import org.json.JSONObject

interface AddCardFrameListener {
    fun onAddCardComplete(cardId: String, brand: String, last4: String)
    fun onAddCardError(code: String, message: String)
}

class MoonPayAddCardFragment : MoonPayFrameFragment() {
    private var clientToken: String? = null
    var listener: AddCardFrameListener? = null

    companion object {
        private const val ARG_CLIENT_TOKEN = "clientToken"

        fun newInstance(clientToken: String): MoonPayAddCardFragment {
            return MoonPayAddCardFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CLIENT_TOKEN, clientToken)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/add-card", mapOf(
            "clientToken" to clientToken!!,
            "channelId" to channelId
        ))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "complete" -> {
                val card = payload?.optJSONObject("card")
                listener?.onAddCardComplete(
                    card?.optString("id") ?: "",
                    card?.optString("brand") ?: "",
                    card?.optString("last4") ?: ""
                )
            }
            "error" -> {
                listener?.onAddCardError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete
    }
}
```

### Usage with Activity

```kotlin theme={null}
class SaveCardActivity : AppCompatActivity(), AddCardFrameListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        val fragment = MoonPayAddCardFragment.newInstance(CredentialsManager.clientToken!!)
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .replace(R.id.frame_container, fragment)
            .commit()
    }

    override fun onAddCardComplete(cardId: String, brand: String, last4: String) {
        Toast.makeText(this, "Card saved: $brand •••• $last4", Toast.LENGTH_SHORT).show()
        // Proceed to payment with the saved card
        finish()
    }

    override fun onAddCardError(code: String, message: String) {
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ -> finish() }
            .show()
    }
}
```

***

## Buy frame

The buy frame processes a card payment for a quote. It is headless — rendered at zero size — while the customer completes payment. If 3-D Secure is required, the frame emits a `challenge` event with a URL you open in a separate challenge frame. See [buy frame reference](/platform/frames/buy) for event details.

### What you'll need

Before you initialize the buy frame, you need:

1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction

### Buy fragment

```kotlin theme={null}
import org.json.JSONObject

interface BuyFrameListener {
    fun onBuyComplete(transactionId: String, status: String)
    fun onBuyChallenge(url: String)
    fun onBuyError(code: String, message: String)
}

class MoonPayBuyFragment : MoonPayFrameFragment() {
    private var clientToken: String? = null
    private var quoteSignature: String? = null
    var listener: BuyFrameListener? = null

    companion object {
        private const val ARG_CLIENT_TOKEN = "clientToken"
        private const val ARG_QUOTE_SIGNATURE = "quoteSignature"

        fun newInstance(clientToken: String, quoteSignature: String): MoonPayBuyFragment {
            return MoonPayBuyFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CLIENT_TOKEN, clientToken)
                    putString(ARG_QUOTE_SIGNATURE, quoteSignature)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
        quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/buy", mapOf(
            "clientToken" to clientToken!!,
            "channelId" to channelId,
            "signature" to quoteSignature!!
        ))
    }

    fun updateQuote(signature: String) {
        quoteSignature = signature
        sendMessage("setQuote", JSONObject().put("quote", JSONObject().put("signature", signature)))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "complete" -> {
                val transaction = payload?.optJSONObject("transaction")
                listener?.onBuyComplete(
                    transaction?.optString("id") ?: "",
                    transaction?.optString("status") ?: ""
                )
            }
            "challenge" -> {
                val url = payload?.optString("url") ?: ""
                listener?.onBuyChallenge(url)
            }
            "error" -> {
                listener?.onBuyError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete
    }
}
```

### Challenge handling

When the buy fragment receives a challenge URL, add a `MoonPayChallengeFragment` to present the 3-D Secure flow. On completion, cancellation, or error, remove both the challenge and buy fragments:

```kotlin theme={null}
import org.json.JSONObject

interface ChallengeFrameListener {
    fun onChallengeComplete(transactionId: String, status: String)
    fun onChallengeCancelled()
    fun onChallengeError(code: String, message: String)
}

class MoonPayChallengeFragment : MoonPayFrameFragment() {
    private var challengeUrl: String? = null
    var listener: ChallengeFrameListener? = null

    companion object {
        private const val ARG_CHALLENGE_URL = "challengeUrl"

        fun newInstance(challengeUrl: String): MoonPayChallengeFragment {
            return MoonPayChallengeFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CHALLENGE_URL, challengeUrl)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        challengeUrl = arguments?.getString(ARG_CHALLENGE_URL)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        val urlWithChannel = Uri.parse(challengeUrl)
            .buildUpon()
            .appendQueryParameter("channelId", channelId)
            .toString()
        webView.loadUrl(urlWithChannel)
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "complete" -> {
                val transaction = payload?.optJSONObject("transaction")
                listener?.onChallengeComplete(
                    transaction?.optString("id") ?: "",
                    transaction?.optString("status") ?: ""
                )
            }
            "cancelled" -> {
                listener?.onChallengeCancelled()
            }
            "error" -> {
                listener?.onChallengeError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete
    }
}
```

### Usage with Activity

```kotlin theme={null}
class BuyPaymentActivity : AppCompatActivity(), BuyFrameListener, ChallengeFrameListener {
    private var buyFragment: MoonPayBuyFragment? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        val fragment = MoonPayBuyFragment.newInstance(
            clientToken = CredentialsManager.clientToken!!,
            quoteSignature = currentQuote.signature
        )
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .add(R.id.frame_container, fragment, "buy")
            .commit()

        buyFragment = fragment
    }

    // BuyFrameListener implementation

    override fun onBuyComplete(transactionId: String, status: String) {
        Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
        // Navigate to transaction status screen or poll for updates
    }

    override fun onBuyChallenge(url: String) {
        val challengeFragment = MoonPayChallengeFragment.newInstance(url)
        challengeFragment.listener = this

        supportFragmentManager.beginTransaction()
            .add(R.id.frame_container, challengeFragment, "challenge")
            .commit()
    }

    override fun onBuyError(code: String, message: String) {
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK", null)
            .show()
    }

    // ChallengeFrameListener implementation

    override fun onChallengeComplete(transactionId: String, status: String) {
        removeFragments()
        Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
        // Navigate to transaction status screen or poll for updates
    }

    override fun onChallengeCancelled() {
        removeFragments()
    }

    override fun onChallengeError(code: String, message: String) {
        removeFragments()
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK", null)
            .show()
    }

    private fun removeFragments() {
        val transaction = supportFragmentManager.beginTransaction()
        supportFragmentManager.findFragmentByTag("challenge")?.let { transaction.remove(it) }
        supportFragmentManager.findFragmentByTag("buy")?.let { transaction.remove(it) }
        transaction.commit()
    }
}
```

***

## Google Pay frame

The Google Pay frame renders the Google Pay button and handles the payment flow. See [Google Pay frame reference](/platform/frames/google-pay) for event details.

### Enable the Payment Request API

Google Pay relies on the [Payment Request API](https://developer.mozilla.org/en-US/docs/Web/API/Payment_Request_API), which is **disabled by default** in Android WebView. Enable it before loading the frame.

The embedding requirements (minimum versions, the `androidx.webkit:webkit:1.14.0` dependency, and the `AndroidManifest.xml` `<queries>` entries) live on the [Google Pay frame reference](/platform/frames/google-pay#android-webview). Meet those first.

Enable the Payment Request API on the WebView in your base fragment's `onCreateView`:

```kotlin theme={null}
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature

// Inside onCreateView, after settings.javaScriptEnabled = true
if (WebViewFeature.isFeatureSupported(WebViewFeature.PAYMENT_REQUEST)) {
    WebSettingsCompat.setPaymentRequestEnabled(webView.settings, true)
}
```

### What you'll need

Before you initialize the Google Pay frame, you need:

1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction

### Google Pay fragment

```kotlin theme={null}
import org.json.JSONObject

interface GooglePayFrameListener {
    fun onGooglePayReady()
    fun onGooglePayComplete(transactionId: String, status: String)
    fun onGooglePayFailed(reason: String)
    fun onGooglePayError(code: String, message: String)
    fun onGooglePayChallenge(url: String)
}

class MoonPayGooglePayFragment : MoonPayFrameFragment() {
    private var clientToken: String? = null
    private var quoteSignature: String? = null
    var listener: GooglePayFrameListener? = null

    companion object {
        private const val ARG_CLIENT_TOKEN = "clientToken"
        private const val ARG_QUOTE_SIGNATURE = "quoteSignature"

        fun newInstance(clientToken: String, quoteSignature: String): MoonPayGooglePayFragment {
            return MoonPayGooglePayFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CLIENT_TOKEN, clientToken)
                    putString(ARG_QUOTE_SIGNATURE, quoteSignature)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
        quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/google-pay", mapOf(
            "clientToken" to clientToken!!,
            "channelId" to channelId,
            "signature" to quoteSignature!!
        ))
    }

    fun updateQuote(signature: String) {
        quoteSignature = signature
        sendMessage("setQuote", JSONObject().put("quote", JSONObject().put("signature", signature)))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "ready" -> {
                listener?.onGooglePayReady()
            }
            "complete" -> {
                val transaction = payload?.optJSONObject("transaction")
                val status = transaction?.optString("status") ?: ""

                if (status == "failed") {
                    listener?.onGooglePayFailed(
                        transaction?.optString("failureReason") ?: "Transaction failed"
                    )
                } else {
                    listener?.onGooglePayComplete(
                        transaction?.optString("id") ?: "",
                        status
                    )
                }
            }
            "challenge" -> {
                val url = payload?.optString("url") ?: ""
                listener?.onGooglePayChallenge(url)
            }
            "error" -> {
                listener?.onGooglePayError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete
    }
}
```

### Usage with Activity

```kotlin theme={null}
class GooglePayActivity : AppCompatActivity(), GooglePayFrameListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        val fragment = MoonPayGooglePayFragment.newInstance(
            CredentialsManager.clientToken!!,
            currentQuote.signature
        )
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .replace(R.id.frame_container, fragment)
            .commit()
    }

    override fun onGooglePayReady() {
        // Google Pay button is visible and ready
    }

    override fun onGooglePayComplete(transactionId: String, status: String) {
        Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
        // Navigate to transaction status screen
    }

    override fun onGooglePayFailed(reason: String) {
        AlertDialog.Builder(this)
            .setTitle("Payment Failed")
            .setMessage(reason)
            .setPositiveButton("OK", null)
            .show()
    }

    override fun onGooglePayError(code: String, message: String) {
        if (code == "quoteExpired") {
            // Fetch new quote and update the fragment
            return
        }
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ -> finish() }
            .show()
    }

    override fun onGooglePayChallenge(url: String) {
        // Open the challenge frame — see "Challenge handling" section
    }
}
```

***

## Widget frame

Apple Pay is not available on Android. Use the [widget frame](/platform/frames/widget) instead to render the full MoonPay buy experience — including payment-method selection (credit/debit card, Google Pay, bank transfers, and more) and transaction confirmation. See [pay with widget](/platform/guides/pay-with-widget) for a full walkthrough.

### What you'll need

Before you initialize the widget frame, you need:

1. A `clientToken` from a successful [connect flow](#connect-frame)
2. A valid [quote signature](/api-reference/platform/endpoints/quotes/get) for the transaction

### Widget fragment

```kotlin theme={null}
import org.json.JSONObject

interface WidgetFrameListener {
    fun onWidgetReady()
    fun onWidgetTransactionCreated(transactionId: String, status: String)
    fun onWidgetComplete(transactionId: String, status: String)
    fun onWidgetFailed(failureReason: String)
    fun onWidgetError(code: String, message: String)
}

class MoonPayWidgetFragment : MoonPayFrameFragment() {
    private var clientToken: String? = null
    private var quoteSignature: String? = null
    var listener: WidgetFrameListener? = null

    companion object {
        private const val ARG_CLIENT_TOKEN = "clientToken"
        private const val ARG_QUOTE_SIGNATURE = "quoteSignature"

        fun newInstance(clientToken: String, quoteSignature: String): MoonPayWidgetFragment {
            return MoonPayWidgetFragment().apply {
                arguments = Bundle().apply {
                    putString(ARG_CLIENT_TOKEN, clientToken)
                    putString(ARG_QUOTE_SIGNATURE, quoteSignature)
                }
            }
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        clientToken = arguments?.getString(ARG_CLIENT_TOKEN)
        quoteSignature = arguments?.getString(ARG_QUOTE_SIGNATURE)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        loadFrame("/platform/v1/widget", mapOf(
            "flow" to "buy",
            "clientToken" to clientToken!!,
            "quoteSignature" to quoteSignature!!,
            "channelId" to channelId
        ))
    }

    override fun onFrameMessage(kind: String, payload: JSONObject?) {
        when (kind) {
            "ready" -> {
                listener?.onWidgetReady()
            }
            "transactionCreated" -> {
                val transaction = payload?.optJSONObject("transaction")
                listener?.onWidgetTransactionCreated(
                    transaction?.optString("id") ?: "",
                    transaction?.optString("status") ?: ""
                )
            }
            "complete" -> handleComplete(payload)
            "error" -> {
                listener?.onWidgetError(
                    payload?.optString("code") ?: "unknown",
                    payload?.optString("message") ?: "Unknown error"
                )
            }
        }
    }

    override fun onFrameHandshakeComplete() {
        // Handshake complete, widget loading
    }

    private fun handleComplete(payload: JSONObject?) {
        val transaction = payload?.optJSONObject("transaction") ?: return
        val status = transaction.optString("status")

        if (status == "failed") {
            val reason = transaction.optString("failureReason", "Transaction failed")
            listener?.onWidgetFailed(reason)
        } else {
            val transactionId = transaction.optString("id")
            listener?.onWidgetComplete(transactionId, status)
        }
    }
}
```

### Usage with Activity

```kotlin theme={null}
class PaymentActivity : AppCompatActivity(), WidgetFrameListener {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_frame_container)

        val fragment = MoonPayWidgetFragment.newInstance(
            clientToken = CredentialsManager.clientToken!!,
            quoteSignature = currentQuote.signature
        )
        fragment.listener = this

        supportFragmentManager.beginTransaction()
            .replace(R.id.frame_container, fragment)
            .commit()
    }

    // WidgetFrameListener implementation

    override fun onWidgetReady() {
        // Widget is loaded and visible
    }

    override fun onWidgetTransactionCreated(transactionId: String, status: String) {
        // Transaction initiated — customer may need to complete 3-D Secure
        Toast.makeText(this, "Transaction created: $transactionId", Toast.LENGTH_SHORT).show()
    }

    override fun onWidgetComplete(transactionId: String, status: String) {
        Toast.makeText(this, "Transaction $transactionId: $status", Toast.LENGTH_SHORT).show()
        // Navigate to transaction status screen or poll for updates
    }

    override fun onWidgetFailed(failureReason: String) {
        AlertDialog.Builder(this)
            .setTitle("Payment Failed")
            .setMessage(failureReason)
            .setPositiveButton("OK", null)
            .show()
    }

    override fun onWidgetError(code: String, message: String) {
        AlertDialog.Builder(this)
            .setTitle("Error")
            .setMessage(message)
            .setPositiveButton("OK", null)
            .show()
    }
}
```
