Skip to content

Android Integration

Display Falcon Perks in your Android app using WebView. No native SDK download required.

Requirements

  • Android API 24+ (Android 7.0)
  • A Falcon API key and placement ID

Quick Start

1. Add permissions

In your AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.INTERNET" />

For local development over HTTP, also add usesCleartextTraffic:

xml
<application
    android:usesCleartextTraffic="true"
    ... >

2. Create the WebView layout

xml
<!-- res/layout/activity_perks.xml -->
<WebView
    android:id="@+id/falconWebView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

3. Set up the WebView with the JavaScript bridge

kotlin
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
import java.util.UUID

class PerksActivity : AppCompatActivity() {

    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_perks)

        // Enable Chrome DevTools debugging
        WebView.setWebContentsDebuggingEnabled(true)

        webView = findViewById(R.id.falconWebView)
        webView.apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            webViewClient = WebViewClient()

            // Register the JavaScript bridge
            addJavascriptInterface(FalconBridge(), "Android")
        }
    }

    fun loadPerks(apiKey: String, placementId: String) {
        val sessionId = UUID.randomUUID().toString()
        val url = "https://promo.falconlabs.us/ui/webview" +
            "?placement=$placementId" +
            "&apiKey=$apiKey" +
            "&sessionId=$sessionId"

        webView.loadUrl(url)
    }

    inner class FalconBridge {
        @JavascriptInterface
        fun postMessage(message: String) {
            try {
                val json = JSONObject(message)
                val type = json.optString("type")
                val name = json.optString("name")

                if (type == "event" && name == "click") {
                    val data = json.optJSONObject("data")
                    val clickUrl = data?.optString("clickUrl") ?: ""
                    if (clickUrl.isNotEmpty()) {
                        // @JavascriptInterface runs on a background thread —
                        // dispatch UI work to the main thread
                        runOnUiThread {
                            startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl)))
                        }
                    }
                }

                if (type == "event" && name == "close") {
                    runOnUiThread { finish() }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }
}

Custom Attributes

Pass user and order attributes to improve offer targeting. Use Uri.Builder to safely encode all parameters:

kotlin
fun loadPerks(apiKey: String, placementId: String, email: String? = null, firstName: String? = null, orderId: String? = null) {
    val sessionId = UUID.randomUUID().toString()
    val uri = Uri.parse("https://promo.falconlabs.us/ui/webview").buildUpon()
        .appendQueryParameter("placement", placementId)
        .appendQueryParameter("apiKey", apiKey)
        .appendQueryParameter("sessionId", sessionId)
        .apply {
            email?.let { appendQueryParameter("at.email", it) }
            firstName?.let { appendQueryParameter("at.firstname", it) }
            orderId?.let { appendQueryParameter("at.orderId", it) }
        }
        .build()
    webView.loadUrl(uri.toString())
}

See the overview for the full list of supported at.* attributes.

Complete Example

Here is a complete activity with the bridge, attribute passing, and click handling via Intent(ACTION_VIEW) (opens in the system browser, outside the WebView):

kotlin
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
import org.json.JSONObject
import java.util.UUID

class FalconPerksActivity : AppCompatActivity() {

    companion object {
        private const val EXTRA_API_KEY = "api_key"
        private const val EXTRA_PLACEMENT_ID = "placement_id"
        private const val EXTRA_EMAIL = "email"
        private const val EXTRA_FIRST_NAME = "first_name"
        private const val EXTRA_ORDER_ID = "order_id"

        fun newIntent(
            context: Context,
            apiKey: String,
            placementId: String,
            email: String? = null,
            firstName: String? = null,
            orderId: String? = null
        ): Intent {
            return Intent(context, FalconPerksActivity::class.java).apply {
                putExtra(EXTRA_API_KEY, apiKey)
                putExtra(EXTRA_PLACEMENT_ID, placementId)
                putExtra(EXTRA_EMAIL, email)
                putExtra(EXTRA_FIRST_NAME, firstName)
                putExtra(EXTRA_ORDER_ID, orderId)
            }
        }
    }

    private lateinit var webView: WebView

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        WebView.setWebContentsDebuggingEnabled(true)

        webView = WebView(this).apply {
            settings.javaScriptEnabled = true
            settings.domStorageEnabled = true
            webViewClient = WebViewClient()
            addJavascriptInterface(FalconBridge(), "Android")
        }
        setContentView(webView)

        val apiKey = intent.getStringExtra(EXTRA_API_KEY) ?: return
        val placementId = intent.getStringExtra(EXTRA_PLACEMENT_ID) ?: return
        val email = intent.getStringExtra(EXTRA_EMAIL)
        val firstName = intent.getStringExtra(EXTRA_FIRST_NAME)
        val orderId = intent.getStringExtra(EXTRA_ORDER_ID)

        // Build URL with attributes
        val sessionId = UUID.randomUUID().toString()
        val uri = Uri.parse("https://promo.falconlabs.us/ui/webview").buildUpon()
            .appendQueryParameter("placement", placementId)
            .appendQueryParameter("apiKey", apiKey)
            .appendQueryParameter("sessionId", sessionId)
            .apply {
                email?.let { appendQueryParameter("at.email", it) }
                firstName?.let { appendQueryParameter("at.firstname", it) }
                orderId?.let { appendQueryParameter("at.orderId", it) }
            }
            .build()

        webView.loadUrl(uri.toString())
    }

    inner class FalconBridge {
        @JavascriptInterface
        fun postMessage(message: String) {
            try {
                val json = JSONObject(message)
                val type = json.optString("type")
                val name = json.optString("name")
                val data = json.optJSONObject("data")

                when {
                    type == "event" && name == "click" -> {
                        val clickUrl = data?.optString("clickUrl") ?: ""
                        if (clickUrl.isNotEmpty()) {
                            // @JavascriptInterface runs on a background thread —
                            // dispatch UI work to the main thread
                            runOnUiThread {
                                startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl)))
                            }
                        }
                    }
                    type == "event" && name == "close" -> {
                        runOnUiThread { finish() }
                    }
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }

    override fun onBackPressed() {
        if (webView.canGoBack()) webView.goBack() else super.onBackPressed()
    }
}

Usage from your app:

kotlin
// Launch the perks screen with attributes
val intent = FalconPerksActivity.newIntent(
    context = this,
    apiKey = "YOUR_API_KEY",
    placementId = "YOUR_PLACEMENT_ID",
    email = "user@example.com",
    firstName = "John",
    orderId = "ORD-123"
)
startActivity(intent)

Jetpack Compose

If you're using Compose, wrap the WebView in an AndroidView:

kotlin
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun FalconPerksWebView(apiKey: String, placementId: String) {
    val context = LocalContext.current
    val sessionId = remember { UUID.randomUUID().toString() }
    val url = "https://promo.falconlabs.us/ui/webview" +
        "?placement=$placementId&apiKey=$apiKey&sessionId=$sessionId"

    AndroidView(
        factory = { ctx ->
            WebView(ctx).apply {
                settings.javaScriptEnabled = true
                settings.domStorageEnabled = true
                webViewClient = WebViewClient()

                addJavascriptInterface(object {
                    @JavascriptInterface
                    fun postMessage(message: String) {
                        try {
                            val json = JSONObject(message)
                            val type = json.optString("type")
                            val name = json.optString("name")

                            if (type == "event" && name == "click") {
                                val data = json.optJSONObject("data")
                                val clickUrl = data?.optString("clickUrl") ?: ""
                                if (clickUrl.isNotEmpty()) {
                                    // @JavascriptInterface runs on a background thread
                                    android.os.Handler(android.os.Looper.getMainLooper()).post {
                                        ctx.startActivity(
                                            Intent(Intent.ACTION_VIEW, Uri.parse(clickUrl))
                                        )
                                    }
                                }
                            }
                        } catch (e: Exception) {
                            e.printStackTrace()
                        }
                    }
                }, "Android")

                WebView.setWebContentsDebuggingEnabled(true)
                loadUrl(url)
            }
        },
        modifier = Modifier.fillMaxSize()
    )
}

Debugging

  1. Enable Chrome DevTools: the code above calls WebView.setWebContentsDebuggingEnabled(true)
  2. Connect your device/emulator via USB
  3. Open chrome://inspect in Chrome on your computer
  4. Your WebView will appear under "Remote Target" - click "inspect" to open DevTools

ProGuard

If you use ProGuard/R8, keep the JavaScript interface:

proguard
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}