Guides

Native WebView (iOS & Android)

Embed Playgent inside a native iOS or Android app — and receive lifecycle events through a JavaScript bridge.

The web SDK targets browser iframes. For native apps you load the play shell directly into a WebView and exchange events through the platform's JavaScript bridge.

Enable WebView on the player

Native WebView embedding requires allow_webview: true on the player. Toggle it in Hub → Configuration → Players → (your player)Allow embedding in iOS / Android WebViews.

How it works

  1. Your app loads the shell URL with a bridge=ios or bridge=android query parameter.
  2. The shell detects the bridge and routes lifecycle events to the matching native JS interface instead of window.parent.postMessage.
  3. Your native code receives the events and reacts (close the WebView, log to analytics, navigate, etc.).

Shell URL

text
https://static.playgent.com/play/v2/?player=plyr_acme&game=sudoku&mode=daily&bridge=ios

Required parameters: player, game, bridge. Optional: mode, content, lang, theme, externalUserId, externalUsername, difficulty, date.

See the full SDK init options reference for everything you can pass — most map 1-to-1 to query parameters.

iOS (WKWebView)

Register a script message handler named playgent before loading the URL. The shell calls window.webkit.messageHandlers.playgent.postMessage(payload) for every lifecycle event.

swift
import WebKit

final class PlaygentViewController: UIViewController, WKScriptMessageHandler {
  private var webView: WKWebView!

  override func viewDidLoad() {
    super.viewDidLoad()

    let config = WKWebViewConfiguration()
    config.userContentController.add(self, name: "playgent")

    webView = WKWebView(frame: view.bounds, configuration: config)
    view.addSubview(webView)

    let url = URL(string:
      "https://static.playgent.com/play/v2/?player=plyr_acme&game=sudoku&mode=daily&bridge=ios"
    )!
    webView.load(URLRequest(url: url))
  }

  func userContentController(
    _ userContentController: WKUserContentController,
    didReceive message: WKScriptMessage
  ) {
    guard message.name == "playgent",
          let payload = message.body as? [String: Any],
          let type = payload["type"] as? String else { return }

    switch type {
    case "pg_ready":    print("ready")
    case "pg_start":    print("started")
    case "pg_hint":     print("hint used", payload["hintsUsed"] ?? "")
    case "pg_complete": print("done", payload["score"] ?? "")
    case "pg_end":      print("CTA pressed")
    case "pg_error":    print("error", payload["code"] ?? "")
    default: break
    }
  }
}

message.body arrives as a Swift dictionary — keys match the SDK event payloads.

Android (WebView)

Add a JavaScript interface named PlaygentAndroidBridge before loading the URL. The shell calls window.PlaygentAndroidBridge.postMessage(jsonString) for every lifecycle event.

kotlin
import android.webkit.JavascriptInterface
import android.webkit.WebView
import org.json.JSONObject

class PlaygentBridge {
  @JavascriptInterface
  fun postMessage(json: String) {
    val payload = JSONObject(json)
    when (payload.optString("type")) {
      "pg_ready"    -> println("ready")
      "pg_start"    -> println("started")
      "pg_hint"     -> println("hint used: ${payload.optInt("hintsUsed")}")
      "pg_complete" -> println("done: ${payload.optInt("score")}")
      "pg_end"      -> println("CTA pressed")
      "pg_error"    -> println("error: ${payload.optString("code")}")
    }
  }
}

class PlaygentActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val webView = WebView(this)
    webView.settings.javaScriptEnabled = true
    webView.addJavascriptInterface(PlaygentBridge(), "PlaygentAndroidBridge")
    setContentView(webView)

    webView.loadUrl(
      "https://static.playgent.com/play/v2/?player=plyr_acme&game=sudoku&mode=daily&bridge=android"
    )
  }
}

The payload arrives as a JSON string — parse it with JSONObject (or kotlinx.serialization). Keys match the SDK event payloads.

Identity from your native auth

Pass the signed-in user via URL parameters:

text
…&externalUserId=u_12345&externalUsername=alice

These flow into every lifecycle event payload — useful for tying completions back to your user records.

Native WebView gotchas

Bridge must exist before the page loads

Register the iOS message handler (or the Android JS interface) before calling webView.load(...). If the shell finishes loading before the bridge is attached, the first events (notably pg_ready) won't reach native code.

Android: @JavascriptInterface security

Only expose the postMessage method shown above — Android's addJavascriptInterface injects whatever you give it into the JS context. Don't expose database handles, file I/O, or anything that grants the WebView access to your app's internals.

iOS: shared WKProcessPool

If you want multiple games to share session state (rare, but possible), create them with a shared WKProcessPool on their configuration. Most apps don't need this — the default isolated process pool is fine.

Origin allowlisting doesn't apply

Origin allowlists are an iframe-embedding control. WebView traffic bypasses them entirely — the allow_webview toggle on the player is what gates native embedding. If you're testing in a browser and the embed is rejected, it's the origin allowlist; if you're in a WebView and it's rejected, it's the WebView toggle.

Network latency

WebViews don't share the user's Safari/Chrome cache. The first load downloads the shell + game assets fresh. For optimistic UX consider pre-warming the WebView during a previous screen.

Origin allowlist (still required for desktop / mobile web)

The allow_webview toggle is independent from the origin allowlist. If you also embed the same player on your marketing site or in a browser, the origin allowlist still applies there.

Players reference →