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.
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.SFSafariViewControllercan't receive messages from the page. - Register a message handler named
onTapResearchMessagewithWKUserContentController(Swift example below). - Our survey wall sends JSON messages to your handler via
postMessage.
Android
- Use an
android.webkit.WebViewwith JavaScript enabled. Chrome Custom Tabs can't receive messages from the page. - Expose an object named exactly
AndroidwithaddJavascriptInterface, with a@JavascriptInterfacemethodonTapResearchMessage(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 anonMessagehandler (example below). - Our survey wall sends JSON messages through
window.ReactNativeWebView.postMessage, soevent.nativeEvent.datais the JSON string. - Every page in the WebView can use that same channel, so only accept messages whose
event.nativeEvent.urlis onhttps://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:
| Field | Type | Description |
|---|---|---|
referer | String | Always TapResearch. |
name | String | The message: setAbandonUrl or clearAbandonUrl. |
abandonUrl | String | setAbandonUrl 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:
- Entering a survey delivers a
setAbandonUrlmessage to your handler. - Tapping close mid-survey takes you out of the survey and back to the survey wall.
- Finishing a survey returns you to the wall, and tapping close there closes the WebView.