TWA App Rejected for Google Play Production Access? How to Convert It to WebView

If you used Bubblewrap, PWA2APK, or standard Trusted Web Activity (TWA) templates to bring your web application to Android, receiving a production access rejection from Google Play is an all-too-common frustration.

Reviewers often flag TWAs with generic messages like "App does not meet production quality standards" or "App provides insufficient utility." Behind the scenes, the root cause is almost always technical: Digital Asset Links (assetlinks.json) failed silent verification on tester hardware, causing Google Chrome's browser address bar to pop up at the top of your app. To a human Google reviewer, your app looks like a simple browser bookmark rather than a legitimate Android application.

Converting your rejected TWA into a custom native Android WebView eliminates browser dependencies, removes the URL bar permanently, gives you total control over navigation and offline states, and delivers the native feel Google demands.

Here is the complete, developer-tested walkthrough to convert your TWA into a production-grade native Android WebView.

Quick Answer: To convert a rejected TWA to a compliant WebView, create a native Android Studio project with an embedded WebView. Configure a custom WebViewClient to handle internal routing and intercept external links, override WebChromeClient for native file uploads and progress bars, implement OnBackPressedCallback for web history, and add a native offline error screen. This removes Chrome URL bar glitches and passes Google Play's Minimum Functionality review.

  • Kill the Address Bar: A native WebView never displays a browser URL bar.
  • Handle Offline States: Prevent net::ERR_INTERNET_DISCONNECTED crashes with a native retry screen.
  • Support Native Back Button: Navigate web history cleanly without accidental app termination.

TWA vs. Native WebView: Why Google Reviewers Prefer Custom WebViews

Before writing code, compare the architectural differences that influence Google Play review outcomes:

Feature / CriterionTrusted Web Activity (TWA)Custom Native Android WebViewImpact on Production Approval
Browser DependencyRelies on user's Chrome or system browserSelf-contained Android System WebView engineWebView eliminates browser version conflicts
Address Bar RiskAppears if assetlinks.json verification failsImpossible to show; completely custom UI containerWebView guarantees a 100% native fullscreen look
Offline HandlingRelies solely on service workers; fails on cache missNative Android layout with custom retry buttonReviewers test offline; native error view passes
Hardware Back ButtonOften terminates the app unexpectedlyManaged via native OnBackPressedDispatcherSmooth, expected Android user navigation
Native IntegrationLimited to web APIsDirect Java/Kotlin bridges (Biometrics, GPS, Storage)Satisfies Google's "Minimum Functionality" policy

By switching to WebView, you retain your existing web codebase while wrapping it in a robust native shell.


Step 1: Set Up the Android Studio Project

Open Android Studio and create a new project:

  1. Select Empty Views Activity (or Empty Activity).
  2. Set Language to Kotlin.
  3. Set Minimum SDK to API 24 (Android 7.0) to cover 99%+ of active devices.

Configure AndroidManifest.xml

Add required Internet permissions and configure hardware acceleration inside app/src/main/AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:allowBackup="true"
        android:hardwareAccelerated="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.AppCompat.Light.NoActionBar">
        
        <activity
            android:name=".MainActivity"
            android:configChanges="orientation|screenSize|keyboardHidden"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

The android:configChanges tag prevents the activity from recreating and reloading your web page when the user rotates their screen.


Step 2: Build the Layout with Progress Bar & Offline View

In app/src/main/res/layout/activity_main.xml, stack your WebView, a horizontal loading bar, and a native offline error layout inside a RelativeLayout or FrameLayout:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- Loading Indicator -->
    <ProgressBar
        android:id="@+id/progressBar"
        style="?android:attr/progressBarStyleHorizontal"
        android:layout_width="match_parent"
        android:layout_height="4dp"
        android:layout_alignParentTop="true"
        android:indeterminate="false"
        android:max="100"
        android:visibility="gone" />

    <!-- Main Web Container -->
    <WebView
        android:id="@+id/webView"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <!-- Native Offline Fallback View -->
    <LinearLayout
        android:id="@+id/offlineLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:orientation="vertical"
        android:padding="24dp"
        android:visibility="gone">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="No Internet Connection"
            android:textSize="20sp"
            android:textStyle="bold" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:text="Please check your network and try again."
            android:textColor="#666666" />

        <Button
            android:id="@+id/btnRetry"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Retry Connection" />
    </LinearLayout>
</RelativeLayout>

Need Experienced Android Testers for Your WebView App?

Don't risk a second rejection. AppConsoleLab provides 12+ real physical Android device testers with active engagement telemetry to pass Google Play review effortlessly.

Money-back compliance guarantee

Step 3: Implement MainActivity.kt with Robust Clients

Now configure the Kotlin logic to manage cookies, handle navigation, intercept deep links, and display the offline screen cleanly:

package com.yourapp.webview

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.webkit.*
import android.widget.Button
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var webView: WebView
    private lateinit var progressBar: ProgressBar
    private lateinit var offlineLayout: LinearLayout
    private lateinit var btnRetry: Button

    private val targetUrl = "https://yourdomain.com"
    private val appDomain = "yourdomain.com"

    @SuppressLint("SetJavaScriptEnabled")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        webView = findViewById(R.id.webView)
        progressBar = findViewById(R.id.progressBar)
        offlineLayout = findViewById(R.id.offlineLayout)
        btnRetry = findViewById(R.id.btnRetry)

        setupWebViewSettings()
        setupClients()
        handleBackNavigation()

        btnRetry.setOnClickListener {
            if (isNetworkAvailable()) {
                offlineLayout.visibility = View.GONE
                webView.visibility = View.VISIBLE
                webView.reload()
            }
        }

        if (isNetworkAvailable()) {
            webView.loadUrl(targetUrl)
        } else {
            showOfflineView()
        }
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun setupWebViewSettings() {
        val settings = webView.settings
        settings.javaScriptEnabled = true
        settings.domStorageEnabled = true
        settings.databaseEnabled = true
        settings.useWideViewPort = true
        settings.loadWithOverviewMode = true
        settings.cacheMode = WebSettings.LOAD_DEFAULT

        // Enable cookie persistence
        CookieManager.getInstance().apply {
            setAcceptCookie(true)
            setAcceptThirdPartyCookies(webView, true)
        }
    }

    private fun setupClients() {
        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
                val url = request?.url?.toString() ?: return false

                // Keep internal app links inside the WebView
                if (url.contains(appDomain)) {
                    return false
                }

                // Launch external links (e.g. WhatsApp, Maps, Social) in external apps
                try {
                    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                    startActivity(intent)
                } catch (e: Exception) {
                    // Fallback or ignore
                }
                return true
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                progressBar.visibility = View.VISIBLE
            }

            override fun onPageFinished(view: WebView?, url: String?) {
                progressBar.visibility = View.GONE
                CookieManager.getInstance().flush()
            }

            override fun onReceivedError(
                view: WebView?,
                request: WebResourceRequest?,
                error: WebResourceError?
            ) {
                // Only show offline view if the main page fails
                if (request?.isForMainFrame == true) {
                    showOfflineView()
                }
            }
        }

        webView.webChromeClient = object : WebChromeClient() {
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                progressBar.progress = newProgress
                if (newProgress == 100) {
                    progressBar.visibility = View.GONE
                }
            }
        }
    }

    private fun handleBackNavigation() {
        onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                if (webView.canGoBack()) {
                    webView.goBack() // Navigate web history
                } else {
                    isEnabled = false
                    onBackPressedDispatcher.onBackPressed() // Exit app
                }
            }
        })
    }

    private fun showOfflineView() {
        webView.visibility = View.GONE
        offlineLayout.visibility = View.VISIBLE
        progressBar.visibility = View.GONE
    }

    private fun isNetworkAvailable(): Boolean {
        val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
}

Step 4: Add File Chooser Support for Image & File Uploads

If your web application has image upload fields (e.g., uploading profile avatars or documents), bare WebViews fail silently when tapping <input type="file">.

To fix this, implement onShowFileChooser in your WebChromeClient:

private var filePathCallback: ValueCallback<Array<Uri>>? = null

private val filePickerLauncher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    val uri = result.data?.data
    val results = if (uri != null) arrayOf(uri) else null
    filePathCallback?.onReceiveValue(results)
    filePathCallback = null
}

// Inside WebChromeClient:
override fun onShowFileChooser(
    webView: WebView?,
    filePathCallback: ValueCallback<Array<Uri>>?,
    fileChooserParams: FileChooserParams?
): Boolean {
    this@MainActivity.filePathCallback?.onReceiveValue(null)
    this@MainActivity.filePathCallback = filePathCallback

    val intent = fileChooserParams?.createIntent() ?: Intent(Intent.ACTION_GET_CONTENT).apply {
        type = "*/*"
    }

    try {
        filePickerLauncher.launch(intent)
    } catch (e: Exception) {
        this@MainActivity.filePathCallback = null
        return false
    }
    return true
}

This simple addition transforms your web application into a fully integrated native tool that satisfies Google's deepest functionality audits.

Ready to Re-Submit Your App for 14-Day Testing?

Don't waste another testing cycle with uncertain results. AppConsoleLab provides verified physical device cohorts that test your WebView app daily.

Money-back compliance guarantee

Step 5: Conduct the New 14-Day Closed Test Compliantly

Once your native WebView App Bundle is generated (./gradlew bundleRelease):

  1. Increment your versionCode in build.gradle.kts.
  2. Upload the new .aab to your existing Closed testing track in Google Play Console.
  3. Enroll 14 to 20 physical Android device testers.
  4. Run the full 14-day test, ensuring testers interact with the new native navigation, offline retry view, and forms.

When drafting your questionnaire after the test, explicitly mention the conversion:

"Following our previous review, we rebuilt the application from a TWA container into a native Android WebView architecture. We integrated native OnBackPressedDispatcher navigation, local network capability monitoring with an offline recovery layout, and native file chooser intents. 16 testers verified stability across diverse devices with zero ANRs or crashes."

For detailed questionnaire preparation, review our production access questionnaire checklist.


Professional Closed Testing Services for WebView Apps

Ensure your new WebView build completes its mandatory 14-day closed test with guaranteed opt-in retention:

One Cycle. Complete Approval.

Select the plan that fits your release complexity.

Loading Packages

Please wait while we fetch the best options for your region...


Frequently Asked Questions

Why does Google Play reject Trusted Web Activities (TWA) for production access?

TWAs are frequently rejected when Digital Asset Links (assetlinks.json) fail verification, causing Chrome's browser URL bar to appear inside the app. Reviewers also reject bare-bones TWAs under Google's "Minimum Functionality" policy if they lack native device integration or fail offline.

Is a native WebView allowed on Google Play?

Yes. Google Play permits WebView apps provided they comply with Webview Spam policies: the web content must be owned by the developer, provide a high-quality app-like user interface, handle offline states gracefully, and include native platform features.

How does converting a TWA to WebView help pass the 14-day closed test?

A native WebView gives you complete control over the Android lifecycle. You eliminate the URL address bar permanently, add native progress indicators, handle device back navigation properly, and build native offline fallback screens that impress Google reviewers.

Will converting to WebView cause session logins to be lost when the app closes?

Not if you configure CookieManager correctly. By enabling DOM storage and calling CookieManager.getInstance().flush(), your users remain logged in across app restarts just like in a native application.

Can a WebView app handle native camera and photo uploads?

Yes, by overriding onShowFileChooser in WebChromeClient, the WebView seamlessly triggers Android's native file picker or camera intent whenever the user clicks an HTML file input.


Ship with Confidence

Moving from a rigid TWA to a custom native Android WebView is one of the highest-leverage engineering upgrades you can make for web-based mobile apps. You eliminate browser URL bar glitches, prevent white-screen offline failures, and present Google reviewers with a clean, responsive, production-ready Android experience.

Need 12 Testers for Your Android WebView App?

Pass Google Play closed testing without dropouts. AppConsoleLab provides 12+ real human testers on verified physical devices with guaranteed approval.

Money-back compliance guarantee

Stuck in Closed Testing? We provide the 12 real testers you need for 14 days.

Get 12 Testers Now!
TWA App Rejected for Google Play Production Access? How to Convert It to WebView