Integration Guide

Integrate CoinmeBridge in Your Android App

Add Coinme's cryptocurrency purchase flow to your native Android app in under 10 minutes.

TL;DR

import com.coinme.bridge.*

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
)

val fragment = CoinmeFragment.newInstance(
    orchestratorUrl = "https://widget.coinme.com",
    config = config,
)
fragment.listener = this
supportFragmentManager.beginTransaction()
    .replace(R.id.container, fragment)
    .commit()

Read on for the full step-by-step walkthrough.


Prerequisites

  • Android API 24+ (Android 7.0) minimum SDK
  • Kotlin 2.2+
  • Your rampId and partnerKey (provided by Coinme)
  • Your Orchestrator URL (e.g. https://widget.coinme.com)

Step 1: Add the Dependency

Add the Cloudsmith Maven repository to your project-level settings.gradle.kts (your Coinme integration specialist will provide the entitlement token):

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("https://maven.cloudsmith.io/coinme/coinme-sdk-frontend/")
            credentials {
                username = "token"
                password = "<ENTITLEMENT_TOKEN>"
            }
        }
    }
}

Then add the dependency to your app module's build.gradle.kts:

dependencies {
    implementation("com.coinme:coinme-bridge:1.0.0")
}

Step 2: Add Manifest Permissions

Add these to your AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
ℹ️

Runtime permissions:

The SDK requests camera, location, and photo library permissions automatically when the fragment is displayed. Partners only need to declare them in the manifest.

Step 3: Configure the Hosting Activity

The Activity that hosts CoinmeFragment must declare android:configChanges so that the WebView is not destroyed when the keyboard opens or the device rotates:

<activity android:name=".BuyActivity" android:configChanges="orientation screenSize|keyboardHidden|keyboard|screenLayout|smallestScreenSize" />
⚠️

Why?

Without this, Android destroys and recreates the Activity on configuration changes. This destroys the WebView and loses the Orchestrator session state, causing UI issues such as sheets opening unexpectedly or inputs losing focus.

Step 4: Create a Configuration

Build a CoinmeConfig with your credentials:

import com.coinme.bridge.CoinmeConfig

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
    environment = "prod",                // "dev", "stage", or "prod"
    destinationCurrency = "BTC",         // optional: pre-select crypto
)

Step 5: Implement the Listener

Implement CoinmeListener to receive events and errors:

import com.coinme.bridge.*

class BuyActivity : AppCompatActivity(), CoinmeListener {

    override fun onCoinmeEvent(fragment: CoinmeFragment, event: CoinmeEvent) {
        when (event) {
            is CoinmeEvent.TransactionComplete -> {
                Log.d("Coinme", "Transaction succeeded")
            }
            is CoinmeEvent.TransactionFailed -> {
                Log.d("Coinme", "Transaction failed")
            }
            is CoinmeEvent.UserCancelled -> {
                finish()
            }
            is CoinmeEvent.KycRequired -> {
                Log.d("Coinme", "KYC verification required")
            }
            is CoinmeEvent.SheetOpened -> { }
            is CoinmeEvent.SheetClosed -> { }
            is CoinmeEvent.Unknown -> {
                Log.d("Coinme", "Unhandled event: ${event.type}")
            }
        }
    }

    override fun onCoinmeError(fragment: CoinmeFragment, error: CoinmeError) {
        Log.e("Coinme", "Bridge error: ${error.message}")
        finish()
    }
}

Step 6: Add the Fragment

Add a container to your layout:

<!-- activity_buy.xml -->
<FrameLayout
    android:id="@+id/coinme_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Then add the fragment:

val fragment = CoinmeFragment.newInstance(
    orchestratorUrl = "https://widget.coinme.com",
    config = config,
)
fragment.listener = this

supportFragmentManager.beginTransaction()
    .replace(R.id.coinme_container, fragment)
    .commit()

That's it. CoinmeFragment handles the WebView, bridge handshake, and session lifecycle automatically.


Minimal Complete Example

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.coinme.bridge.*

class BuyActivity : AppCompatActivity(), CoinmeListener {

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

        val config = CoinmeConfig(
            rampId = "your-ramp-id",
            partnerKey = "your-partner-key",
            destinationCurrency = "BTC",
        )

        val fragment = CoinmeFragment.newInstance(
            orchestratorUrl = "https://widget.coinme.com",
            config = config,
        )
        fragment.listener = this

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

    override fun onCoinmeEvent(fragment: CoinmeFragment, event: CoinmeEvent) {
        when (event) {
            is CoinmeEvent.TransactionComplete -> finish()
            is CoinmeEvent.UserCancelled -> finish()
            else -> { }
        }
    }

    override fun onCoinmeError(fragment: CoinmeFragment, error: CoinmeError) {
        Log.e("Coinme", "Error: ${error.message}")
        finish()
    }
}

Customization

Permission Flags

Camera and location default to true to provide the full Coinme experience. Screen capture protection is on by default (allowsCapture = false).

FlagDefaultWhat it controlsManifest permissions required
allowsCameratrueCamera access for KYC document/selfie captureCAMERA
allowsLocationtrueBrowser geolocation API (e.g. store finders)ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
allowsCapturefalseWhen false, FLAG_SECURE prevents screen recording and screenshotsNone

To opt out:

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
    allowsCamera = false,    // Disables camera access
    allowsLocation = false,  // Disables navigator.geolocation in the WebView
    allowsCapture = true,    // Disables screen capture protection
)
ℹ️

Trade-offs of disabling permissions:

  • allowsCamera = false: Identity-document workflows are disabled. Your widget integration will be unavailable in jurisdictions that require documentary KYC.
  • allowsLocation = false: Users will not see nearby retail locations automatically and must specify their location manually.
⚠️

Important:

You need to communicate your decision about camera access and geolocation to your Coinme business development team so that your ramp can be properly configured. Not doing so may result in poor UX for a subset of your customers.

Theme and Language

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
    theme = CoinmeTheme.DARK,    // LIGHT (default) or DARK
    language = "es",              // BCP 47 tag; defaults to "en"
)

Transaction Pre-fill

Lock fields to prevent user changes:

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
    walletAddress = "bc1q...",
    walletLock = true,
    sourceAmount = 100.0,
    sourceAmountLock = true,
    destinationCurrency = "BTC",
    destinationCurrencyLock = true,
)

SSO / Authentication Passthrough

Pass credentials at any time — they're queued until the session is ready:

fragment.setAuth(
    token = "jwt-token",
    userId = "user-123",
    userEmail = "[email protected]",
)

Metadata and Tracking

val config = CoinmeConfig(
    rampId = "your-ramp-id",
    partnerKey = "your-partner-key",
    metadata = CoinmeMetadata(
        source = "android-app",
        campaign = "summer-promo",
    ),
    externalSessionId = "session-abc",
    externalTransactionId = "tx-456",
)

Optional Listener Methods

These have default no-op implementations:

override fun onCoinmeSessionReady(fragment: CoinmeFragment) {
    // Bridge handshake complete, session is active
}

override fun onCoinmeSessionEnded(fragment: CoinmeFragment) {
    // Orchestrator signaled session end
    finish()
}

Troubleshooting

ProblemCauseFix
Bridge never connectsWrong Orchestrator URL or environmentVerify the URL matches your environment (dev, stage, prod)
Events not receivedListener not set, or set after fragment addedSet listener before adding the fragment to the activity
Camera not workingMissing manifest permission or runtime denialAdd CAMERA permission and handle runtime permission request
Blank white screenNetwork or URL errorCheck onCoinmeError for WebViewLoadFailed and inspect the underlying error
Screen capture blockedallowsCapture defaults to falseSet allowsCapture = true in config if capture is needed
KYC document upload failsMissing CAMERA or photo library permissionAdd CAMERA, READ_MEDIA_IMAGES (API 33+), and READ_EXTERNAL_STORAGE (API 32 and below) to manifest
Photo picker shows emptyMissing READ_MEDIA_IMAGES / READ_EXTERNAL_STORAGE permissionAdd photo library permissions to manifest (the SDK requests them at runtime)
Sheets misbehave / inputs lose focusActivity missing android:configChangesAdd the full android:configChanges attribute (orientation, screenSize, keyboard, etc.) to the hosting Activity — see Step 3
Links open blank pagetarget="_blank" links open externallyThis is expected behavior — external links open in the device browser

Debugging with Chrome DevTools

In debug builds the SDK enables WebView debugging automatically. To inspect:

  1. Open Chrome on your computer
  2. Navigate to chrome://inspect
  3. Find your device and tap Inspect next to the WebView
  4. Use the console to see CoinmeBridge log messages and Orchestrator JavaScript output

Bridge Protocol Reference

The SDK handles all protocol details internally. This section is for reference only.

Message Flow

┌─────────────┐                           ┌──────────────┐
│   Native    │                           │ Orchestrator │
│     App     │                           │   (WebView)  │
└──────┬──────┘                           └──────┬───────┘
       │                                         │
       │  1. Load Orchestrator URL               │
       │────────────────────────────────────────>│
       │                                         │
       │  2. Handshake                           │
       │<────────────────────────────────────────│
       │     { type: "handshake" }               │
       │                                         │
       │  3. HandshakeAck                        │
       │────────────────────────────────────────>│
       │     { type: "handshakeAck" }            │
       │                                         │
       │  4. bridge-session-ready                │
       │<────────────────────────────────────────│
       │     { type: "event" }                   │
       │                                         │
       │  5. updateConfig                        │
       │────────────────────────────────────────>│
       │                                         │
       │  6. mount                               │
       │────────────────────────────────────────>│
       │                                         │

Key Differences from iOS

  • Message transport: Android uses WebViewCompat.addWebMessageListener with platform-enforced origin validation (falls back to @JavascriptInterface on older WebView versions) instead of WKScriptMessageHandler
  • Capture protection: Android uses FLAG_SECURE on the Activity window instead of the iOS UITextField secure container
  • Embedding: Fragment-based instead of ViewController-based
  • File uploads: Handled via WebChromeClient.onShowFileChooser + ActivityResultLauncher instead of WKUIDelegate