Skip to main content
Version: 3.x

In-app WebViews

The standard way to use the Hosted Link is to open it in the device's browser, which needs no extra work. If you prefer to use an in-app WebView, the following additional setup is strongly recommended.

Keeping users in your app? Consider the SDK

On iOS and Android, the SDK keeps users in your app and handles leaving surveys and closing for you. Use an in-app WebView only if the SDK isn't an option.

⚠️ Prerequisites​

Your app must be enabled for in-app WebView mode. Ask your account manager or developers@tapresearch.com to switch it on before you load TapResearch in a WebView. With it, surveys open inside the same WebView, and the survey wall sends your app the messages below so your close button can take a user out of a survey and back to the wall, instead of out of the experience.

Platforms​

iOS​

  • Use a WKWebView. SFSafariViewController can't receive messages from the page.
  • Register a message handler named onTapResearchMessage with WKUserContentController (Swift example below).
  • Our survey wall sends JSON messages to your handler via postMessage.

Android​

  • Use an android.webkit.WebView with JavaScript enabled. Chrome Custom Tabs can't receive messages from the page.
  • Expose an object named exactly Android with addJavascriptInterface, with a @JavascriptInterface method onTapResearchMessage(String) (Kotlin example below).
  • Our survey wall sends JSON messages to that method as a string.
  • Make the system back button do the same as your close button.

React Native​

  • Use react-native-webview, and pass an onMessage handler (example below).
  • Our survey wall sends JSON messages through window.ReactNativeWebView.postMessage, so event.nativeEvent.data is the JSON string.
  • Every page in the WebView can use that same channel, so only accept messages whose event.nativeEvent.url is on https://www.tapresearch.com.
  • On Android, make the hardware back button do the same as your close button.

Messages​

Each message is a JSON string with these fields:

FieldTypeDescription
refererStringAlways TapResearch.
nameStringThe message: setAbandonUrl or clearAbandonUrl.
abandonUrlStringsetAbandonUrl only. The URL to load if the user leaves this survey.

Pages from survey partners load in the same WebView, so only accept messages whose referer is TapResearch, and only store an abandonUrl on https://www.tapresearch.com. Ignore any other name: more messages may be added.

setAbandonUrl​

The user is entering a survey. Store abandonUrl, replacing any URL you already hold.

{
"referer": "TapResearch",
"name": "setAbandonUrl",
"abandonUrl": "https://www.tapresearch.com/router/offers/8f14e45fceea167a5a36dedd4bea2543/sdk_abandon?cp_identifier=37939e4ede350f3a8d5149d2fcaa025e"
}

clearAbandonUrl​

The user is back on the survey wall. Clear the stored URL.

{
"referer": "TapResearch",
"name": "clearAbandonUrl"
}

Closing the WebView​

Give the WebView your own close button. When the user taps it:

  • If you're holding an abandon URL, the user is in the middle of a survey. Clear the stored URL and load the abandon URL in the WebView. That ends the survey and takes the user back to the survey wall, where they can pick another survey or tap close again.
  • Otherwise, close the WebView.

Only load the abandon URL when the user chooses to leave, never automatically (on a timer, or when your app goes to the background): ending a survey that's still finishing can cost the user their reward.

Examples​

iOS (Swift)​

import UIKit
import WebKit

final class TapResearchWebViewController: UIViewController, WKScriptMessageHandler {

private let entryURL: URL
private var abandonURL: URL?
private var webView: WKWebView!

init(entryURL: URL) {
self.entryURL = entryURL
super.init(nibName: nil, bundle: nil)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
super.viewDidLoad()

let configuration = WKWebViewConfiguration()
// WKUserContentController keeps a strong reference, so register a weak proxy.
configuration.userContentController.add(WeakMessageHandler(self), name: "onTapResearchMessage")
webView = WKWebView(frame: view.bounds, configuration: configuration)
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
view.addSubview(webView)

// Shown in the navigation bar, so present this controller in a UINavigationController.
navigationItem.leftBarButtonItem = UIBarButtonItem(
barButtonSystemItem: .close, target: self, action: #selector(closeTapped))
// Swiping the sheet away would skip the abandon handling below.
isModalInPresentation = true

webView.load(URLRequest(url: entryURL))
}

@objc private func closeTapped() {
if let abandonURL {
// Mid-survey: leave the survey and go back to the survey wall.
self.abandonURL = nil
webView.load(URLRequest(url: abandonURL))
} else {
dismiss(animated: true)
}
}

func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
guard
let json = message.body as? String,
let data = json.data(using: .utf8),
let payload = try? JSONDecoder().decode(TapResearchMessage.self, from: data),
payload.referer == "TapResearch"
else { return }

switch payload.name {
case "setAbandonUrl":
if let url = payload.abandonUrl.flatMap(URL.init(string:)),
url.scheme == "https", url.host == "www.tapresearch.com" {
abandonURL = url
}
case "clearAbandonUrl":
abandonURL = nil
default:
break
}
}
}

private struct TapResearchMessage: Decodable {
let referer: String
let name: String
let abandonUrl: String?
}

private final class WeakMessageHandler: NSObject, WKScriptMessageHandler {
weak var delegate: WKScriptMessageHandler?

init(_ delegate: WKScriptMessageHandler) {
self.delegate = delegate
}

func userContentController(_ controller: WKUserContentController, didReceive message: WKScriptMessage) {
delegate?.userContentController(controller, didReceive: message)
}
}

Present it inside a UINavigationController, which gives the close button its navigation bar:

let controller = TapResearchWebViewController(entryURL: url)
present(UINavigationController(rootViewController: controller), animated: true)

Android (Kotlin)​

class TapResearchWebViewActivity : AppCompatActivity() {

private lateinit var webView: WebView
private var abandonUrl: String? = null

@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
webView = WebView(this)
setContentView(webView)

webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
// Keep navigation inside the WebView instead of handing links to the browser.
webView.webViewClient = WebViewClient()
// The page calls window.Android.onTapResearchMessage(json), so the name must be "Android".
webView.addJavascriptInterface(TapResearchBridge(), "Android")

onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() = close()
})

if (savedInstanceState != null) {
// Recreated: restore the page and the stored abandon URL instead of starting over.
abandonUrl = savedInstanceState.getString(STATE_ABANDON_URL)
webView.restoreState(savedInstanceState)
} else {
webView.loadUrl(requireNotNull(intent.getStringExtra(EXTRA_ENTRY_URL)))
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(STATE_ABANDON_URL, abandonUrl)
webView.saveState(outState)
}

// Call this from your close button too.
private fun close() {
val url = abandonUrl
if (url != null) {
// Mid-survey: leave the survey and go back to the survey wall.
abandonUrl = null
webView.loadUrl(url)
} else {
finish()
}
}

private fun handleMessage(json: String) {
val message = runCatching { JSONObject(json) }.getOrNull() ?: return
if (message.optString("referer") != "TapResearch") return

when (message.optString("name")) {
"setAbandonUrl" -> {
val url = Uri.parse(message.optString("abandonUrl"))
if (url.scheme == "https" && url.host == "www.tapresearch.com") {
abandonUrl = url.toString()
}
}
"clearAbandonUrl" -> abandonUrl = null
}
}

private inner class TapResearchBridge {
// Called on a background thread.
@JavascriptInterface
fun onTapResearchMessage(json: String) {
runOnUiThread { handleMessage(json) }
}
}

companion object {
const val EXTRA_ENTRY_URL = "entry_url"
private const val STATE_ABANDON_URL = "abandon_url"
}
}

Declare configChanges on the activity so rotating the device keeps the survey open instead of recreating the activity and reloading the page. If the activity is recreated anyway, for example after the system reclaims your app in the background, the saved state restores the page and the stored abandon URL.

<activity
android:name=".TapResearchWebViewActivity"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden" />

React Native (TypeScript)​

import { useEffect, useRef, useState } from 'react';
import { BackHandler, Button } from 'react-native';
import { WebView, type WebViewMessageEvent } from 'react-native-webview';

const TAPRESEARCH_ORIGIN = 'https://www.tapresearch.com';

// Matches the origin itself (some Android WebViews report only the sender's origin)
// or any page on it, but not look-alike hosts such as www.tapresearch.com.example.
const isTapResearch = (url: string) =>
url === TAPRESEARCH_ORIGIN || url.startsWith(`${TAPRESEARCH_ORIGIN}/`);

export function TapResearchWebView({ entryUrl, onClose }: { entryUrl: string; onClose: () => void }) {
const [source, setSource] = useState({ uri: entryUrl });
const abandonUrlRef = useRef<string | null>(null);

const onMessage = (event: WebViewMessageEvent) => {
const { url, data } = event.nativeEvent;
if (!isTapResearch(url)) return; // only the survey wall talks to us

let msg;
try {
msg = JSON.parse(data);
} catch {
return;
}
if (msg?.referer !== 'TapResearch') return;

if (msg.name === 'setAbandonUrl' && typeof msg.abandonUrl === 'string' && isTapResearch(msg.abandonUrl)) {
abandonUrlRef.current = msg.abandonUrl;
} else if (msg.name === 'clearAbandonUrl') {
abandonUrlRef.current = null;
}
};

const close = () => {
const abandonUrl = abandonUrlRef.current;
if (abandonUrl) {
// Mid-survey: leave the survey and go back to the survey wall.
abandonUrlRef.current = null;
setSource({ uri: abandonUrl });
} else {
onClose();
}
};

// Android's back button closes the same way.
useEffect(() => {
const subscription = BackHandler.addEventListener('hardwareBackPress', () => {
close();
return true;
});
return () => subscription.remove();
});

return (
<>
<Button title="Close" onPress={close} />
<WebView source={source} onMessage={onMessage} />
</>
);
}

Test it​

Once in-app WebView mode is on, use a test device and check that:

  1. Entering a survey delivers a setAbandonUrl message to your handler.
  2. Tapping close mid-survey takes you out of the survey and back to the survey wall.
  3. Finishing a survey returns you to the wall, and tapping close there closes the WebView.