> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Open cards in a mobile WebView

> Open the cards React experience in a WebView on iOS and Android

Card prebuilt components ship in the Privy React SDK. iOS and Android apps open a hosted copy of that experience in a WebView.

Host the page from the [React integration](https://docs.privy.io/financial-flows/cards/pre-built-components/react-integration.md) on a domain in the app's [allowed origins](https://dashboard.privy.io/apps?setting=domains\&page=settings). Use the same Privy app ID as the mobile app.

The WebView has its own Privy session so the user signs in on the hosted page.

## How it works

1. The user opens cards in the native app.
2. The app loads the hosted React page in a WebView.
3. The user signs in on that page when the WebView has no Privy session.
4. The page runs card signup and displays the card summary.

## Render the card views

For the best experience, render the card views full screen in the WebView.

> [!NOTE]
>
> Host the page on HTTPS. Add that origin in the [Privy
> Dashboard](https://dashboard.privy.io/apps?setting=domains\&page=settings) before the WebView loads
> it.

## Handling window popups

Card signup calls `window.open` for provider terms and identity verification. Return that window to the page, and release it on close so the page can read `closed`.

> [tab] iOS
>
> ```swift theme={"system"}
> func webView(
>     _ webView: WKWebView,
>     createWebViewWith configuration: WKWebViewConfiguration,
>     for navigationAction: WKNavigationAction,
>     windowFeatures: WKWindowFeatures
> ) -> WKWebView? {
>     guard navigationAction.targetFrame == nil else { return nil }
>
>     let popup = WKWebView(frame: view.bounds, configuration: configuration)
>     popup.uiDelegate = self
>     view.addSubview(popup)
>     return popup
> }
>
> func webViewDidClose(_ webView: WKWebView) {
>     webView.removeFromSuperview()
> }
> ```

> [tab] Android
>
> ```kotlin theme={"system"}
> settings.javaScriptCanOpenWindowsAutomatically = true
> settings.setSupportMultipleWindows(true)
>
> override fun onCreateWindow(
>     view: WebView,
>     isDialog: Boolean,
>     isUserGesture: Boolean,
>     resultMsg: Message
> ): Boolean {
>     val popup = WebView(view.context)
>     val transport = resultMsg.obj as WebView.WebViewTransport
>     transport.webView = popup
>     resultMsg.sendToTarget()
>     return true
> }
>
> override fun onCloseWindow(window: WebView) {
>     window.destroy()
> }
> ```

## Open the hosted page

The app loads the hosted React page in the WebView. On Android, enable JavaScript, DOM storage, and third-party cookies, and match the parent height so the cards layout keeps its size.

> [tab] iOS
>
> ```swift theme={"system"}
> let config = WKWebViewConfiguration()
> config.allowsInlineMediaPlayback = true
>
> let webView = WKWebView(frame: view.bounds, configuration: config)
> webView.uiDelegate = self
> view.addSubview(webView)
>
> if let url = URL(string: "https://your-domain.com/cards") {
>     webView.load(URLRequest(url: url))
> }
> ```

> [tab] Android
>
> ```kotlin theme={"system"}
> val webView = WebView(this).apply {
>     layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
>     settings.javaScriptEnabled = true
>     settings.domStorageEnabled = true
>     CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
> }
>
> webView.loadUrl("https://your-domain.com/cards")
> ```

## Handling webview permissions

Identity verification uses the camera and microphone. Declare that access, then grant the capture request to the WebView.

> [tab] iOS
>
> Add these keys to `Info.plist`.
>
> ```xml theme={"system"}
> <key>NSCameraUsageDescription</key>
> <string>This app uses the camera to verify the cardholder's identity.</string>
> <key>NSMicrophoneUsageDescription</key>
> <string>This app uses the microphone to verify the cardholder's identity.</string>
> ```
>
> ```swift theme={"system"}
> func webView(
>     _ webView: WKWebView,
>     requestMediaCapturePermissionFor origin: WKSecurityOrigin,
>     initiatedByFrame frame: WKFrameInfo,
>     type: WKMediaCaptureType,
>     decisionHandler: @escaping (WKPermissionDecision) -> Void
> ) {
>     decisionHandler(.grant)
> }
> ```

> [tab] Android
>
> Add these permissions to `AndroidManifest.xml`.
>
> ```xml theme={"system"}
> <uses-permission android:name="android.permission.CAMERA" />
> <uses-permission android:name="android.permission.RECORD_AUDIO" />
> ```
>
> ```kotlin theme={"system"}
> registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted ->
>     if (granted.values.all { it }) webView.loadUrl(cardPageUrl)
> }.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
>
> override fun onPermissionRequest(request: PermissionRequest) {
>     request.grant(request.resources)
> }
> ```

## Complete example

> [tab] iOS
>
> ```swift theme={"system"}
> import UIKit
> import WebKit
>
> class CardViewController: UIViewController, WKUIDelegate {
>     private var webView: WKWebView!
>     private var popupWebView: WKWebView?
>     private let cardPageUrl = "https://your-domain.com/cards"
>     private let popupCloseButton: UIButton = {
>         let button = UIButton(type: .system)
>         button.setTitle("Close", for: .normal)
>         button.isHidden = true
>         return button
>     }()
>
>     override func viewDidLoad() {
>         super.viewDidLoad()
>
>         let config = WKWebViewConfiguration()
>         config.allowsInlineMediaPlayback = true
>
>         webView = WKWebView(frame: view.bounds, configuration: config)
>         webView.uiDelegate = self
>         webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
>         view.addSubview(webView)
>
>         popupCloseButton.addTarget(self, action: #selector(closePopup), for: .touchUpInside)
>         popupCloseButton.frame = CGRect(x: 16, y: 16, width: 72, height: 32)
>         view.addSubview(popupCloseButton)
>
>         if let url = URL(string: cardPageUrl) {
>             webView.load(URLRequest(url: url))
>         }
>     }
>
>     func webView(
>         _ webView: WKWebView,
>         createWebViewWith configuration: WKWebViewConfiguration,
>         for navigationAction: WKNavigationAction,
>         windowFeatures: WKWindowFeatures
>     ) -> WKWebView? {
>         guard navigationAction.targetFrame == nil else { return nil }
>
>         popupWebView?.removeFromSuperview()
>         // Use this configuration so JavaScript receives the new window.
>         let popup = WKWebView(frame: view.bounds, configuration: configuration)
>         popup.uiDelegate = self
>         popup.autoresizingMask = [.flexibleWidth, .flexibleHeight]
>         view.addSubview(popup)
>         popupWebView = popup
>         popupCloseButton.isHidden = false
>         view.bringSubviewToFront(popupCloseButton)
>         return popup
>     }
>
>     func webViewDidClose(_ webView: WKWebView) {
>         guard webView == popupWebView else { return }
>         webView.removeFromSuperview()
>         popupWebView = nil
>         popupCloseButton.isHidden = true
>     }
>
>     @objc private func closePopup() {
>         popupWebView?.removeFromSuperview()
>         popupWebView = nil
>         popupCloseButton.isHidden = true
>     }
>
>     func webView(
>         _ webView: WKWebView,
>         requestMediaCapturePermissionFor origin: WKSecurityOrigin,
>         initiatedByFrame frame: WKFrameInfo,
>         type: WKMediaCaptureType,
>         decisionHandler: @escaping (WKPermissionDecision) -> Void
>     ) {
>         decisionHandler(.grant)
>     }
> }
> ```

> [tab] Android
>
> ```kotlin theme={"system"}
> import android.Manifest
> import android.annotation.SuppressLint
> import android.content.pm.PackageManager
> import android.os.Bundle
> import android.os.Message
> import android.view.ViewGroup.LayoutParams.MATCH_PARENT
> import android.webkit.CookieManager
> import android.webkit.PermissionRequest
> import android.webkit.WebChromeClient
> import android.webkit.WebView
> import android.webkit.WebViewClient
> import android.widget.FrameLayout
> import androidx.activity.OnBackPressedCallback
> import androidx.activity.result.contract.ActivityResultContracts
> import androidx.appcompat.app.AppCompatActivity
> import androidx.core.content.ContextCompat
>
> class CardActivity : AppCompatActivity() {
>     private val cardPageUrl = "https://your-domain.com/cards"
>     private lateinit var root: FrameLayout
>     private var webView: WebView? = null
>     private var popupWebView: WebView? = null
>     private var popupLayer: FrameLayout? = null
>
>     private val requestCapture = registerForActivityResult(
>         ActivityResultContracts.RequestMultiplePermissions()
>     ) {
>         webView?.loadUrl(cardPageUrl)
>     }
>
>     private val backCallback = object : OnBackPressedCallback(true) {
>         override fun handleOnBackPressed() {
>             when {
>                 popupWebView != null -> dismissPopup(popupWebView)
>                 webView?.canGoBack() == true -> webView?.goBack()
>                 else -> {
>                     isEnabled = false
>                     onBackPressedDispatcher.onBackPressed()
>                 }
>             }
>         }
>     }
>
>     @SuppressLint("SetJavaScriptEnabled")
>     override fun onCreate(savedInstanceState: Bundle?) {
>         super.onCreate(savedInstanceState)
>         onBackPressedDispatcher.addCallback(this, backCallback)
>         root = FrameLayout(this)
>         setContentView(root)
>
>         val wv = newWebView(onCreatePopup = { resultMsg ->
>             val popup = newWebView(onCreatePopup = null, onClosePopup = { dismissPopup(popupWebView) })
>             showPopup(popup)
>             val transport = resultMsg.obj as WebView.WebViewTransport
>             transport.webView = popup
>             resultMsg.sendToTarget()
>         })
>
>         webView = wv
>         root.addView(wv, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
>
>         val needed = arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)
>         val missing = needed.any {
>             ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED
>         }
>
>         if (missing) {
>             requestCapture.launch(needed)
>         } else {
>             wv.loadUrl(cardPageUrl)
>         }
>     }
>
>     override fun onResume() {
>         super.onResume()
>         webView?.onResume()
>         popupWebView?.onResume()
>     }
>
>     override fun onPause() {
>         webView?.onPause()
>         popupWebView?.onPause()
>         super.onPause()
>     }
>
>     @SuppressLint("SetJavaScriptEnabled")
>     private fun newWebView(
>         onCreatePopup: ((Message) -> Unit)?,
>         onClosePopup: (() -> Unit)? = null
>     ): WebView {
>         return WebView(this).apply {
>             layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
>             settings.javaScriptEnabled = true
>             settings.domStorageEnabled = true
>             settings.javaScriptCanOpenWindowsAutomatically = true
>             settings.setSupportMultipleWindows(true)
>             settings.mediaPlaybackRequiresUserGesture = false
>             CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)
>             webViewClient = WebViewClient()
>             webChromeClient = object : WebChromeClient() {
>                 override fun onPermissionRequest(request: PermissionRequest) {
>                     request.grant(request.resources)
>                 }
>
>                 override fun onCreateWindow(
>                     view: WebView,
>                     isDialog: Boolean,
>                     isUserGesture: Boolean,
>                     resultMsg: Message
>                 ): Boolean {
>                     val create = onCreatePopup ?: return false
>                     create(resultMsg)
>                     return true
>                 }
>
>                 override fun onCloseWindow(window: WebView) {
>                     onClosePopup?.invoke()
>                 }
>             }
>         }
>     }
>
>     private fun showPopup(popup: WebView) {
>         dismissPopup(popupWebView)
>         popup.onResume()
>         val layer = FrameLayout(this)
>         layer.addView(popup, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
>         root.addView(layer, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
>         popupLayer = layer
>         popupWebView = popup
>     }
>
>     private fun dismissPopup(popup: WebView?) {
>         val layer = popupLayer ?: return
>         root.removeView(layer)
>         popupLayer = null
>         popupWebView = null
>         popup?.destroy()
>     }
> }
> ```
