# Add Unique Link using in-app WebView

This document has been written assuming you're integrating a [Gamezop Unique Link](/publishers/gamezop/types-of-integration/integrate-unique-link.md) or individual games. However, the steps to follow are the same even if you're integrating Quizzop / Astrozop or any of our other products in your app.

***

### Integration guide

This guide applies to **Android WebView** integration only. Follow the steps given below closely to get a perfectly working WebView implementation in your app:

#### Step 1: Create the WebView via code (instead of XML)

We recommend creating the WebView in code for simplicity. To prevent memory leaks, [make sure to call `destroy()` on the WebView in the Activity’s `onDestroy()`.](#user-content-fn-1)[^1]

You may have a particular Android Activity where you place the Gamezop icon / banner. When a user taps on that icon / banner, you want to open your game URL within a WebView. Within the `onCreate` method of this Activity, you will have to create the WebView as mentioned below:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
private lateinit var myWebView: WebView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    myWebView = WebView(this)
    setContentView(myWebView)
}
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
// At the top of your Activity class:
private WebView myWebView;

// In onCreate():
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    myWebView = new WebView(this);
    setContentView(myWebView);
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Step 2: Make updates to `AndroidManifest.xml`

The primary objective of making these updates is to enable the following:

* Internet access for the WebView
* Screen orientation changes within the WebView (since games can be landscape as well)
* Hardware acceleration for better game performance

Here are the updates to be made within your app's `AndroidManifest.xml` file:

{% code lineNumbers="true" %}

```xml
<uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.WebViewSetup">
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize"
            android:hardwareAccelerated="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
```

{% endcode %}

{% hint style="info" %}
Using `android:configChanges="orientation|screenSize"` stops the activity from being recreated on rotation. Keep this attribute if your activity is simple and doesn’t need to restart during orientation changes, but for complex activities, it may be better to rely on the default configuration‑change handling.
{% endhint %}

#### Step 3: Setup a `WebViewClient` to intercept URLs

Create a `CustomWebViewClient` file within the same directory as where you have your `MainActivity` file. Java and Kotlin versions of this file are given below:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import java.net.MalformedURLException
import java.net.URI
import java.net.URISyntaxException

class CustomWebViewClientKotlin : WebViewClient() {

    private val TAG = "CustomWebViewClient"
    /*
     ********************************************************************************************
     * IMPORTANT
     *
     * It is CRITICAL that GAMEZOP_DOMAIN(S) correctly capture the host of the URL
     * provided to you by your Gamezop Account Manager.
     *
     * The domains "gamezop.com" and "umogames.com" are owned and operated by Gamezop.
     * However, from time to time, Gamezop may provide partners with DIFFERENT domains.
     *
     * If this value is incorrect, links may open outside the WebView unintentionally.
     * ALWAYS verify and update this before going live.
     ********************************************************************************************
     */
    private val GAMEZOP_DOMAINS = setOf("gamezop.com", "umogames.com")
    private val NO_APPLICATION_ERROR = "You do not have an application to run this."

    /** Allow exactly Gamezop domains or any subdomain like *.gamezop.com, *.umogames.com */
    private fun isGamezopHost(uri: Uri?): Boolean {
        val host = uri?.host ?: return false
        return GAMEZOP_DOMAINS.any { domain ->
            host.equals(domain, ignoreCase = true) ||
            host.endsWith(".${domain}", ignoreCase = true)
        }
    }

    private fun isHttpOrHttps(uri: Uri?): Boolean {
        val scheme = uri?.scheme ?: return false
        return scheme.equals("http", true) || scheme.equals("https", true)
    }

    override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
        val uri = request.url

        // Early exit: if both host & scheme are missing, let WebView handle it.
        if (uri.host == null && uri.scheme == null) {
            return false
        }

        // Handle custom (non-http/https) schemes externally (e.g., market://, intent://).
        if (!isHttpOrHttps(uri)) {
            return tryOpenExternally(view, uri)
        }

        // Keep Gamezop links in-WebView
        if (isGamezopHost(uri)) {
            // Do NOT call loadUrl here; returning false lets WebView handle it.
            return false
        }

        // Same-domain navigation: if current page host == target host, keep inside.
        val currentHost = try {
            view.url?.let { URI(it).toURL().host }
        } catch (_: URISyntaxException) { null }
          catch (_: MalformedURLException) { null }

        val targetHost = uri.host
        if (currentHost != null && targetHost != null &&
            currentHost.equals(targetHost, ignoreCase = true)) {
            return false
        }

        // External navigation: open in the user's default browser
        openOutside(view, uri)
        return true
    }

    // Fallback for older callers (String overload is deprecated but may still be invoked)
    @Deprecated("Deprecated in Android API, kept here for compatibility")
    override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
        val uri = runCatching { Uri.parse(url) }.getOrNull()
        if (uri == null) return false

        // Non-http(s) → try external app
        if (!isHttpOrHttps(uri)) {
            return tryOpenExternally(view, uri)
        }

        // Gamezop or same-domain → keep inside
        if (isGamezopHost(uri)) return false

        val currentHost = runCatching { view.url?.let { URI(it).toURL().host } }.getOrNull()
        val targetHost = uri.host
        if (currentHost != null && targetHost != null &&
            currentHost.equals(targetHost, ignoreCase = true)) {
            return false
        }

        // External https/http → default browser
        openOutside(view, uri)
        return true
    }

    /** Attempt to open non-http/https schemes in an external app. */
    private fun tryOpenExternally(view: WebView, uri: Uri): Boolean {
        return try {
            val external = Intent(Intent.ACTION_VIEW, uri)
            view.context.startActivity(external)
            true
        } catch (e: ActivityNotFoundException) {
            Log.d(TAG, "No app to handle custom scheme: $uri", e)
            Toast.makeText(view.context, NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show()
            true
        }
    }

    /** Open standard http/https external URLs in the user's default browser. */
    private fun openOutside(view: WebView, uri: Uri?) {
        if (uri == null) {
            Toast.makeText(view.context, NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show()
            return
        }
        val intent = Intent(Intent.ACTION_VIEW, uri)
        val pm: PackageManager = view.context.packageManager
        val activities = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
        if (activities.isNotEmpty()) {
            view.context.startActivity(intent)
        } else {
            Toast.makeText(view.context, NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show()
        }
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URI;
import java.util.List;

public class CustomWebViewClient extends WebViewClient {

    private static final String TAG = "CustomWebViewClient";
    /*
     ********************************************************************************************
     * IMPORTANT
     *
     * It is CRITICAL that GAMEZOP_DOMAIN(S) correctly capture the host of the URL
     * provided to you by your Gamezop Account Manager.
     *
     * The domains "gamezop.com" and "umogames.com" are owned and operated by Gamezop.
     * However, from time to time, Gamezop may provide partners with DIFFERENT domains.
     *
     * If this value is incorrect, links may open outside the WebView unintentionally.
     * ALWAYS verify and update this before going live.
     ********************************************************************************************
     */
    private static final String[] GAMEZOP_DOMAINS = new String[] { "gamezop.com", "umogames.com" };
    private static final String NO_APPLICATION_ERROR = "You do not have an application to run this.";

    private boolean isGamezopHost(Uri uri) {
        if (uri == null) return false;
        String host = uri.getHost();
        if (host == null) return false;
        // allow exactly gamezop.com / umogames.com or any subdomain like *.gamezop.com, *.umogames.com
        for (String domain : GAMEZOP_DOMAINS) {
            if (host.equalsIgnoreCase(domain) || host.toLowerCase().endsWith("." + domain.toLowerCase())) {
                return true;
            }
        }
        return false;
    }

    private boolean isHttpOrHttps(Uri uri) {
        if (uri == null || uri.getScheme() == null) return false;
        String s = uri.getScheme();
        return "http".equalsIgnoreCase(s) || "https".equalsIgnoreCase(s);
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        Uri uri = request != null ? request.getUrl() : null;

        // If both host and scheme are missing, let WebView handle it
        if (uri == null || (uri.getHost() == null && uri.getScheme() == null)) {
            return false;
        }

        // Handle non-http(s) custom schemes (e.g. market://, intent://)
        if (!isHttpOrHttps(uri)) {
            try {
                Intent external = new Intent(Intent.ACTION_VIEW, uri);
                view.getContext().startActivity(external);
            } catch (ActivityNotFoundException e) {
                Log.d(TAG, "No app to handle custom scheme: " + uri, e);
                Toast.makeText(view.getContext(), NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show();
            }
            return true;
        }

        // Stay within WebView for Gamezop or same-domain navigations
        if (isGamezopHost(uri)) {
            return false; // Let WebView load Gamezop URLs itself
        }

        String currentHost = null;
        try {
            String currentUrl = view.getUrl();
            if (currentUrl != null) {
                currentHost = new URI(currentUrl).toURL().getHost();
            }
        } catch (URISyntaxException | MalformedURLException ignore) { }
        String targetHost = uri.getHost();

        // Same-domain navigation — keep inside the WebView
        if (currentHost != null && targetHost != null && currentHost.equalsIgnoreCase(targetHost)) {
            return false;
        }

        // External navigation — open in the user’s default browser
        openOutside(view, uri);
        return true;
    }

    // Fallback for deprecated String overload
    @SuppressWarnings("deprecation")
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Uri uri = url != null ? Uri.parse(url) : null;
        if (uri == null) return false;
        if (isGamezopHost(uri)) {
            return false;
        } else {
            openOutside(view, uri);
            return true;
        }
    }

    private void openOutside(WebView view, Uri uri) {
        if (uri == null) {
            Toast.makeText(view.getContext(), NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show();
            return;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW, uri);
        PackageManager pm = view.getContext().getPackageManager();
        List<ResolveInfo> activities = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        if (!activities.isEmpty()) {
            view.getContext().startActivity(intent);
        } else {
            Toast.makeText(view.getContext(), NO_APPLICATION_ERROR, Toast.LENGTH_LONG).show();
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Step 4: Set this `CustomWebViewClient` as your WebView client

To do so, add the following line in the Activity where you created your WebView:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
myWebView.webViewClient = CustomWebViewClientKotlin()
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
myWebView.setWebViewClient(new CustomWebViewClient());
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Step 5: Configure WebView settings in the holder Activity

Add the following lines in the Activity where you created your WebView:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
val settings = myWebView.settings

settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.javaScriptCanOpenWindowsAutomatically = true
settings.setGeolocationEnabled(true)
settings.loadsImagesAutomatically = true
settings.useWideViewPort = true
settings.setSupportZoom(false)
settings.layoutAlgorithm = WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING

// Crucial for optimal ad monetisation
settings.mediaPlaybackRequiresUserGesture = false

myWebView.setSoundEffectsEnabled(true)
myWebView.scrollBarStyle = View.SCROLLBARS_INSIDE_OVERLAY
myWebView.setBackgroundColor(Color.argb(1, 0, 0, 0))

// Cookies
CookieManager.getInstance().setAcceptCookie(true)
// Enabling 3rd-party cookies is crucial for optimal ad monetisation
CookieManager.getInstance().setAcceptThirdPartyCookies(myWebView, true)
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
WebSettings settings = myWebView.getSettings();

settings.setJavaScriptEnabled(true);
settings.setDomStorageEnabled(true);
settings.setJavaScriptCanOpenWindowsAutomatically(true);
settings.setGeolocationEnabled(true);
settings.setLoadsImagesAutomatically(true);
settings.setUseWideViewPort(true);
settings.setSupportZoom(false);
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING);

// Crucial for optimal ad monetisation
settings.setMediaPlaybackRequiresUserGesture(false);

myWebView.setSoundEffectsEnabled(true);
myWebView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
myWebView.setBackgroundColor(Color.argb(1, 0, 0, 0));

// Cookies
CookieManager.getInstance().setAcceptCookie(true);
// Enabling 3rd-party cookies is crucial for optimal ad monetisation
CookieManager.getInstance().setAcceptThirdPartyCookies(myWebView, true);
```

{% endcode %}
{% endtab %}
{% endtabs %}

To prevent memory leaks, always destroy the WebView when the activity or fragment that hosts it is destroyed.

Add the following line inside your activity’s `onDestroy()` method:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
override fun onDestroy() {
    myWebView.destroy()
    super.onDestroy()
}
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
@Override
protected void onDestroy() {
    if (myWebView != null) {
        myWebView.destroy();
    }
    super.onDestroy();
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Destroying the WebView releases its internal resources (JavaScript threads, rendering cache, etc.) without clearing the shared application cache.

This keeps Gamezop games and other web content loading fast on subsequent visits while ensuring the app itself does not leak memory.
{% endhint %}

#### Step 6: Open your Gamezop Unique Link

Once you are done with the 5 steps mentioned above, you are ready to open your Gamezop game URL within your WebView. Add this line at the end of the Activity where you created your WebView:

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
myWebView.loadUrl("https://3025.play.gamezop.com") //ensure you replace this with the URL provided to you by your Gamezop Account Manager
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
myWebView.loadUrl("https://3025.play.gamezop.com"); //ensure you replace this with the URL provided to you by your Gamezop Account Manager
```

{% endcode %}
{% endtab %}
{% endtabs %}

#### Step 7: Integrate the WebView API for Ads

{% hint style="info" %}
You can avoid this step if you are integrating Gamezop games or any of our other products with ads disabled. However, if you are working with us on our advertising revenue-share model, **following this step is crucial for optimal ad monetisation.**
{% endhint %}

The WebView Ads API shares app signals with Google ad tags used within Gamezop, helping optimise ad delivery and increase monetisation — which in turn boosts your revenue share.

<details>

<summary>Prerequisites</summary>

* [Google Mobile Ads SDK](https://developers.google.com/ad-manager/mobile-ads-sdk/android/quick-start#import_the_mobile_ads_sdk) version 20.6.0 or higher
* Android API level 21 or higher
* Add the following `<meta-data>` tag in your `AndroidManifest.xml` file to bypass the check for the `APPLICATION_ID`. If you miss this step and don't provide the `<meta-data>` tag, the Google Mobile Ads SDK throws an `IllegalStateException` on app start.

{% code lineNumbers="true" %}

```xml
<!-- Bypass APPLICATION_ID check for web view APIs for ads -->
 <meta-data
     android:name="com.google.android.gms.ads.INTEGRATION_MANAGER"
     android:value="webview"/>
```

{% endcode %}

</details>

**Register the web view**

Call `registerWebView()` on the main thread to establish a connection with the JavaScript handlers in the AdSense code or Google Publisher Tag within each `WebView` instance. This should be done as early as possible, such as in the `onCreate()` method of your `MainActivity`.

{% tabs %}
{% tab title="Kotlin" %}
{% code lineNumbers="true" %}

```kotlin
import com.google.android.gms.ads.MobileAds

// After Step 5 (settings) and before/after Step 6 (loadUrl), simply register:
MobileAds.registerWebView(myWebView)
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}
{% code lineNumbers="true" %}

```java
import com.google.android.gms.ads.MobileAds;

// After Step 5 settings (and before/after Step 6 loadUrl), register the same WebView:
MobileAds.registerWebView(myWebView);
```

{% endcode %}
{% endtab %}
{% endtabs %}

***

### Cache and performance

WebView automatically caches Gamezop’s game assets and user cookies to make reloads much faster for returning users. To maintain this performance, avoid clearing the WebView cache or cookies unnecessarily.

✅ **Best practice:** Let WebView manage its own cache and cookie storage.

❌ **Avoid:**&#x20;

* `webView.clearCache(true)`
* `CookieManager.getInstance().removeAllCookies(null)`
* `WebStorage.getInstance().deleteAllData()`

{% hint style="info" %}
The WebView resource cache is shared across all WebViews within your app.

If you clear it frequently (for example, every time the WebView is destroyed), all downloaded game assets will need to be fetched again, increasing load times and data usage.
{% endhint %}

***

### Test ads performance

Use this URL to test if your WebView is properly optimised for ad monetisation:

```
https://google.github.io/webview-ads/test/
```

The test URL has success criteria for a complete integration if the following are observed:

**WebView settings**

* Third-party cookies work
* First-party cookies work
* JavaScript enabled
* DOM storage enabled

**Video ad**

* The video ad plays inline and does not open in the full screen built-in player
* The video ad plays automatically without clicking the play button
* The video ad is replayable

**WebView API for Ads**

* If you followed [Step 7 above](#step-7-integrate-the-webview-api-for-a-ds), visit [this link](https://google.github.io/webview-ads/test/#api-for-ads-tests) in your app's WebView to check if it successfully connected to the Google Mobile Ads SDK. The test URL should show green status bars for a successful integration.

**Optimal click behaviour**

* Open [this link](https://google.github.io/webview-ads/test/#click-behavior-tests) in your app's WebView. Click each of the different link types to see how they behave in your app. Here are some things to check:
  * Each link opens the intended URL.
  * When returning to the app, the test page's counter doesn't reset to zero to validate the page state was preserved.

After testing is complete, substitute the test URLs from Google with your Gamezop Unique Link.

***

### Useful resources

* Ad monetisation best practises (Android): <https://developers.google.com/ad-manager/mobile-ads-sdk/android/browser/webview>
* Ad monetisation best practises (iOS): <https://developers.google.com/ad-manager/mobile-ads-sdk/ios/browser/webview>
* Sample Android app (Kotlin) with the expected WebView implementation: <https://github.com/gamezop/gma-android-webview-example>

[^1]: Explained in Step 5 below.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.platform.gamezop.com/publishers/guides/implement-android-webview.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
