UseDocumentation Index
Fetch the complete documentation index at: https://dev.moonpay.com/llms.txt
Use this file to discover all available pages before exploring further.
WebView to embed frames in native Android applications. The WebView communicates with frames via JavaScript interfaces and evaluateJavascript.
Read the manual integration
overview for core concepts
before you continue.
Setup
Dependencies
The examples below use Tink for cryptographic operations, but you can use any library that supports X25519 and AES-GCM.// build.gradle.kts (app level)
dependencies {
implementation("com.google.crypto.tink:tink-android:1.12.0")
}
Key generation
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
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: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, the Apple Pay frame renders a mock button and useswindow.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:
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").
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.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 for event details.Check fragment
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")
// Check payload.capabilities.ramps.requirements.paymentDisclosures
// to determine if payment disclosures are required before transacting
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")
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
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 for event details.Connect fragment
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")
// Check payload.capabilities.ramps.requirements.paymentDisclosures
// to determine if payment disclosures are required before transacting
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
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 for event details.What you’ll need
Before you initialize the add card frame, you need:- A
clientTokenfrom a successful connect flow
Add Card fragment
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
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 achallenge event with a URL you open in a separate challenge frame. See buy frame reference for event details.
What you’ll need
Before you initialize the buy frame, you need:- A
clientTokenfrom a successful connect flow - A valid quote signature for the transaction
Buy fragment
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 aMoonPayChallengeFragment to present the 3-D Secure flow. On completion, cancellation, or error, remove both the challenge and buy fragments:
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
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()
}
}
Widget frame
Apple Pay is not available on Android. Use the widget frame 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 for a full walkthrough.What you’ll need
Before you initialize the widget frame, you need:- A
clientTokenfrom a successful connect flow - A valid quote signature for the transaction
Widget fragment
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
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()
}
}