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. Out of the box users swipe, and the offers loop rather than run out. If your app can act on a close, you can turn on an X that asks it to.
  • 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, Unity.call in a gree/unity-webview. 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

Unity — nothing to register. gree/unity-webview exposes window.Unity.call to the page on its own, and we send the click through it as a string. The callback you already pass to WebViewObject.Init receives it; step 2 is what goes inside.

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>;
}

Unity — the callback from step 1, in C#. The message arrives as one JSON string:

csharp
using UnityEngine;

[System.Serializable] class FalconEvent { public string type; public string name; public FalconEventData data; }
[System.Serializable] class FalconEventData { public string clickUrl; public string kind; }

void OnFalconMessage(string message)
{
    var falconEvent = JsonUtility.FromJson<FalconEvent>(message);
    if (falconEvent == null || falconEvent.type != "event" || falconEvent.name != "click") return;
    if (string.IsNullOrEmpty(falconEvent.data?.clickUrl)) return;

    Application.OpenURL(falconEvent.data.clickUrl);
}

Application.OpenURL hands the URL to the system browser. That is a fine first version: the tap gets an immediate response instead of the webview sitting on the old screen while the advertiser loads, and tracking is unaffected, since the URL is opened as given. What it does not give you is the way back — the user is in another app, and returning to the offers is up to them. An in-app browser (SFSafariViewController, Custom Tabs) keeps them in yours, but inside Unity that means a native plugin, so treat it as a later step rather than a requirement.

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" } }

iOS receives it as an object through iosNativeListener. Android and Unity receive it as a JSON string, through Android.postMessage and Unity.call respectively. We try them in that order and send to the first one present, so a host with more than one bridge hears about each tap once.

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 ​

This is the other close: the whole placement, not the advertiser's page. We can show an X and tell you when the user is done with the offers, but the unit cannot dismiss itself — the webview is yours, so all it can do is ask.

Off by default, because an X nobody acts on is worse than no X at all. Turn it on by telling us how your app wants to hear about it.

1. Pick how we tell you ​

Add hostClose to the URL you load:

ValueWhat we do
hostClose=bridgeSend a close event over the same bridge as click.
hostClose=schemeNavigate to falcon://close, for you to catch where you already intercept non-http(s) URLs.
hostClose=bothBoth, message first.
text
https://pr.falconlabs.us/partners-rewards?placementId=YOUR_PLACEMENT_ID&publicApiKey=YOUR_PUBLIC_API_KEY&sessionId=SESSION_ID&hostClose=bridge

Independent of nativeClick: opening a URL natively and dismissing your own webview are different pieces of code, and you can have either without the other. Dropping the parameter switches this back off at any time, without waiting for a release from us.

Pick scheme only if you already intercept non-http(s) navigations

It is the same handler as the app-install section above. An Android WebView that does not intercept it will load falcon://close as a page and paint an ERR_UNKNOWN_URL_SCHEME error over the unit.

2. Handle it ​

With the flag on, an X appears on the unit. Tapping it asks your app to close. The offers still loop after the last one, so the X is the only thing that ends the unit unless you ask us for the rest, which is the last section here.

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() }
}

With hostClose=scheme there is no message at all — one if in the navigation handler you already have:

kotlin
// Match on scheme and host, not the whole string: URL parsing is free to
// hand back "falcon://close/" for the same link.
if (request.url.scheme == "falcon" && request.url.host == "close") {
    runOnUiThread { finish() }
    return true
}

The message we send ​

json
{ "type": "event", "name": "close", "data": { "index": 3, "closeType": 2 } }
FieldDescription
indexWhich offer was on screen.
closeTypeWhy it closed. 1 is the X, which is the only one you get by default. 2 is the last offer being declined, if you turn that on below. Close the webview on either, and treat an unfamiliar value as a close too, since it leaves room for a reason we add later.

Closing on the last offer too ​

By default the offers keep looping and only the X ends the unit. We can also have the last "No thanks" close it, so the user is not handed the first offer again after turning all of them down.

That one is set per placement on our side, so ask your account manager rather than changing anything yourself. Worth doing once you have confirmed the X works in your build, since it is the same close event either way and only the closeType differs.

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.