Skip to content

Partner Rewards: Webview Integration

Before you begin

The placement uses two identifiers, both preconfigured on the Falcon side and provided by your Falcon Labs account manager:

IdentifierDescription
placementIdIdentifies your specific Partner Rewards placement.
publicApiKeyIdentifies your publisher account. Safe to use in client-side code.

Integration

Open the following URL inside your webview:

text
https://pr.falconlabs.us/partners-rewards?placementId=YOUR_PLACEMENT_ID&publicApiKey=YOUR_PUBLIC_API_KEY&sessionId=SESSION_ID
ParameterRequiredDescription
placementIdRequiredYour Falcon placement, provided by Falcon.
publicApiKeyRequiredYour public account key. Safe to use client-side.
sessionIdRequiredA unique identifier for this session, generated by your app — a UUID is fine. Generate a new one every time you open the placement, and never reuse one across users.

To test before launch, use the same URL on staging (staging credentials are provided separately):

text
https://staging-pr.falconlabs.us/partners-rewards?placementId=YOUR_PLACEMENT_ID&publicApiKey=YOUR_PUBLIC_API_KEY&sessionId=SESSION_ID

Behavior

  • Renders full-screen. Give it a full-height webview.
  • Tapping an offer opens the advertiser and the unit moves to the next one, looping after the last. Where the advertiser opens is the next section.
  • The unit cannot dismiss itself: the webview is yours, so only your app can close it. Users swipe. An X that asks your app to close it is available on request.
  • If placementId or publicApiKey is missing, the page reads "These offers are not available right now." That looks the same as having no offers, so check the URL first.

By default, tapping an offer opens the advertiser in the webview itself: a new tab if your webview allows one, otherwise in place of the unit. Either way the offers are gone after one click.

You can take the click instead and open it in an in-app browser over the webview. The user gets a progress bar and a way out of it — Done on older iOS, an X on newer. It closes the advertiser's page, not the unit, so they land back on the offers and one session can produce several clicks.

The click reaches you over the same bridge as our mobile integration: iosNativeListener on iOS, the Android interface on Android. Payload at the end of this section. If that bridge is already wired up, only the click branch and step 3 are new.

1. Register the message handler

iOS — the content controller retains the handler, so release it in deinit:

swift
import WebKit

class FalconRewardsViewController: UIViewController, WKScriptMessageHandler {

    private var webView: WKWebView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let controller = WKUserContentController()
        controller.add(self, name: "iosNativeListener")

        let config = WKWebViewConfiguration()
        config.userContentController = controller

        webView = WKWebView(frame: view.bounds, configuration: config)
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)
    }

    deinit {
        // WKUserContentController retains its message handlers - break the cycle
        webView.configuration.userContentController
            .removeScriptMessageHandler(forName: "iosNativeListener")
    }
}

Android — the in-app browser comes from AndroidX Browser:

kotlin
// build.gradle.kts
dependencies {
    implementation("androidx.browser:browser:1.8.0")
}
kotlin
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(FalconBridge(), "Android") // FalconBridge: step 2

2. Open the URL when a click arrives

iOS — the handler method, on the class from step 1:

swift
import SafariServices
import WebKit

extension FalconRewardsViewController {
    func userContentController(
        _ controller: WKUserContentController,
        didReceive message: WKScriptMessage
    ) {
        guard message.name == "iosNativeListener",
              let body = message.body as? [String: Any],
              body["type"] as? String == "event",
              body["name"] as? String == "click",
              let data = body["data"] as? [String: Any],
              let string = data["clickUrl"] as? String,
              let url = URL(string: string)
        else { return }

        present(SFSafariViewController(url: url), animated: true)
    }
}

Android — an inner class of the activity that owns the webview:

kotlin
import android.net.Uri
import android.webkit.JavascriptInterface
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import org.json.JSONObject

class FalconRewardsActivity : AppCompatActivity() {

    // webview setup from step 1

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

                val clickUrl = json.optJSONObject("data")?.optString("clickUrl").orEmpty()
                if (clickUrl.isEmpty()) return

                // @JavascriptInterface runs on a background thread.
                runOnUiThread {
                    CustomTabsIntent.Builder().build()
                        .launchUrl(this@FalconRewardsActivity, Uri.parse(clickUrl))
                }
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }
    }
}

Open it inside your app, as both snippets do. The system browser tracks the same, but it drops the user into another app and the way back to the offers is gone.

With ProGuard or R8, keep the interface methods, or the bridge goes silent in release builds:

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

3. Opt in with nativeClick=1

Add the flag to the URL you load:

text
https://pr.falconlabs.us/partners-rewards?placementId=YOUR_PLACEMENT_ID&publicApiKey=YOUR_PUBLIC_API_KEY&sessionId=SESSION_ID&nativeClick=1

The flag tells us you handle the click. A registered handler alone is not enough, since an app may already own one under that name and ignore our messages. Without the flag we keep opening offers ourselves, so dropping the parameter switches this off at any time.

The message we send

json
{ "type": "event", "name": "click", "data": { "clickUrl": "https://...", "kind": "offer" } }
FieldDescription
clickUrlThe URL to open. Always open it as given — it carries click tracking.
kindoffer for the main call to action, link for Terms and Privacy Policy. Open both: handling only offer leaves those links dead.

App-install offers (required)

Some offers are app-install campaigns. Selecting one navigates to a non-http(s) scheme that resolves to the app store or a deep link (market:// on Android, itms-apps:// on iOS). A webview cannot resolve those, so the user lands on a blank page.

Intercept every navigation and hand any non-http(s) URL to the operating system instead of loading it. Attribution is unaffected: the store gets the same referrer as a direct launch.

Add this even if you do the native handoff above. With the flag on, clicks leave through the bridge and the in-app browser follows store redirects itself — but this is what covers you the moment the flag comes off.

Android

kotlin
webView.webViewClient = object : WebViewClient() {
    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
        val scheme = request.url.scheme?.lowercase()
        if (scheme == "http" || scheme == "https") return false // normal page, let the WebView load it
        return try {
            val intent = if (scheme == "intent")
                Intent.parseUri(request.url.toString(), Intent.URI_INTENT_SCHEME)
            else Intent(Intent.ACTION_VIEW, request.url)
            // Safety: let the OS pick a normal app, never a named internal component.
            intent.addCategory(Intent.CATEGORY_BROWSABLE)
            intent.component = null
            intent.selector = null
            startActivity(intent) // opens Play Store
            true
        } catch (e: ActivityNotFoundException) { false }
    }
}

iOS

swift
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    if let url = navigationAction.request.url, let s = url.scheme?.lowercased(),
       s != "http", s != "https" {
        UIApplication.shared.open(url) // itms-apps://, app deep links
        decisionHandler(.cancel)
        return
    }
    decisionHandler(.allow)
}

In-unit close control (on request)

This is the other close: the whole placement, not the advertiser's page. We can show an X, but the unit cannot act on it — the webview is yours, so the tap has to come back to you.

Ask us and we will render it and send a close event. It arrives in the handler you already have, so you switch on the event name instead of guarding for click. On iOS, inside userContentController from step 2:

swift
guard message.name == "iosNativeListener",
      let body = message.body as? [String: Any],
      body["type"] as? String == "event"
else { return }

switch body["name"] as? String {
case "click": break // open the URL, as in step 2
case "close": dismiss(animated: true)
default: break
}

On Android, inside FalconBridge.postMessage:

kotlin
when (json.optString("name")) {
    "click" -> { /* open the URL, as in step 2 */ }
    "close" -> runOnUiThread { finish() }
}

If you already intercept non-http(s) navigations, we can send the close as falcon://close instead: one if in that handler, no bridge at all.

Optional attributes

To sharpen match rate and payout you can add user or order attributes to the URL, each with an at. prefix. URL-encode any special characters.

text
.../partners-rewards?placementId=...&publicApiKey=...&sessionId=...&at.country=US&at.email=user%40example.com

Full list: https://docs.falconlabs.com/integration-guide/overlay.html#custom-attributes

That page documents the Web SDK, which takes the same attributes as an object. Same names and formats: read the table, then prefix each one with at.

Reporting

Impressions, clicks, and conversions are tracked on the Falcon side. No tracking code goes into your app.