Appearance
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
- Enable Chrome DevTools: the code above calls
WebView.setWebContentsDebuggingEnabled(true) - Connect your device/emulator via USB
- Open
chrome://inspectin Chrome on your computer - 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>;
}