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 customWebViewClientto handle internal routing and intercept external links, overrideWebChromeClientfor native file uploads and progress bars, implementOnBackPressedCallbackfor 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_DISCONNECTEDcrashes 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 / Criterion | Trusted Web Activity (TWA) | Custom Native Android WebView | Impact on Production Approval |
|---|---|---|---|
| Browser Dependency | Relies on user's Chrome or system browser | Self-contained Android System WebView engine | WebView eliminates browser version conflicts |
| Address Bar Risk | Appears if assetlinks.json verification fails | Impossible to show; completely custom UI container | WebView guarantees a 100% native fullscreen look |
| Offline Handling | Relies solely on service workers; fails on cache miss | Native Android layout with custom retry button | Reviewers test offline; native error view passes |
| Hardware Back Button | Often terminates the app unexpectedly | Managed via native OnBackPressedDispatcher | Smooth, expected Android user navigation |
| Native Integration | Limited to web APIs | Direct 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:
- Select Empty Views Activity (or Empty Activity).
- Set Language to Kotlin.
- 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.
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.
Step 5: Conduct the New 14-Day Closed Test Compliantly
Once your native WebView App Bundle is generated (./gradlew bundleRelease):
- Increment your
versionCodeinbuild.gradle.kts. - Upload the new
.aabto your existing Closed testing track in Google Play Console. - Enroll 14 to 20 physical Android device testers.
- 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.