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

# Mobile Bridge

> Program × Developer: how the Paylead WebApp (MFP) calls native iOS/Android capabilities from inside your WebView.

The **Mobile Bridge** lets the Paylead WebApp (MFP) running inside your native
WebView invoke native device capabilities: opening a URL in the system browser,
writing to the clipboard, downloading a file. It uses the **JSON-RPC 2.0**
protocol over `postMessage`.

Your native app receives requests from the WebApp and responds by calling back
into JavaScript.

```mermaid theme={null}
%%{init: {'themeVariables': {'actorBorder':'#0465ff','signalColor':'#378add'}}}%%
sequenceDiagram
    participant W as Paylead WebApp<br/>(WebView)
    participant N as Native app

    W->>N: postMessage({ jsonrpc, method, params, id })
    Note right of N: Validate envelope<br/>Validate params<br/>Execute action

    alt Success
        N-->>W: response({ jsonrpc, result: {}, id })
        Note left of W: Promise resolves
    else Failure (invalid params,<br/>permission denied, runtime error…)
        N-->>W: response({ jsonrpc, error: { code, message }, id })
        Note left of W: Promise rejects<br/>with error.code
    else No response within timeout<br/>(5s / 30s for file.download)
        Note left of W: Promise rejects<br/>with timeout error
    end
```

<Note>
  The bridge JavaScript is versioned (`v1`, `v2`, …). The machine-readable
  contract for each version is published as a `schema.json`. See the
  [Methods](#methods) reference below for the current version, and the
  [Versions & download](/program/webapp/bridge/releases) page to download a
  schema or browse older versions.
</Note>

## Transport

<Tabs>
  <Tab title="iOS (WKWebView)">
    **WebApp → Native**

    The WebApp posts to a message handler; WKWebView auto-deserializes the JSON,
    so your handler receives a `[String: Any]` dictionary.

    ```js WebApp theme={null}
    window.webkit.messageHandlers.payleadBridge.postMessage(msg)
    ```

    **Native → WebApp**

    Call back with argument binding; never interpolate the JSON into the source.

    ```swift Native theme={null}
    webView.callAsyncJavaScript(
      "window.payleadBridge.response(jsonString)",
      arguments: ["jsonString": jsonStr],
      ...
    )
    ```
  </Tab>

  <Tab title="Android (WebView)">
    **WebApp → Native**

    The WebApp posts a JSON string that you parse yourself, because Android's
    `@JavascriptInterface` only accepts primitives.

    ```js WebApp theme={null}
    window.PayleadBridge.postMessage(JSON.stringify(msg))
    ```

    **Native → WebApp**

    Quote the payload with `JSONObject.quote()`; never interpolate the JSON
    into the source.

    ```kotlin Native theme={null}
    webView.evaluateJavascript(
      "window.payleadBridge.response(" + JSONObject.quote(jsonStr) + ")",
      null
    )
    ```
  </Tab>
</Tabs>

<Warning>
  **Never interpolate JSON strings into JavaScript source.** Use argument binding
  (`callAsyncJavaScript` on iOS) or `JSONObject.quote()` (Android). Direct
  interpolation such as `"response('${json}')"` creates a JS injection vector.
  This rule applies every time you call back into the WebApp.
</Warning>

## Message format

### Request (WebApp → Native)

Your handler receives JSON-RPC 2.0 request objects:

```json theme={null}
{
  "jsonrpc": "2.0",
  "method": "browser.open",
  "params": { "url": "https://example.com" },
  "id": "89d89a45-4f57-4e68-9f61-dc71434b2f26"
}
```

### Success response (Native → WebApp)

Call `window.payleadBridge.response(jsonString)` with a serialized JSON-RPC 2.0 result:

```json theme={null}
{ "jsonrpc": "2.0", "result": {}, "id": "89d89a45-4f57-4e68-9f61-dc71434b2f26" }
```

### Error response (Native → WebApp)

If the action fails, respond with a JSON-RPC 2.0 error (see [Error codes](#error-codes)):

```json theme={null}
{
  "jsonrpc": "2.0",
  "error": { "code": -32000, "message": "Download failed" },
  "id": "89d89a45-4f57-4e68-9f61-dc71434b2f26"
}
```

### Envelope constraints

* **`jsonrpc` must equal `"2.0"`.** Reject any other value with error code `-32600`. If the envelope cannot be parsed at all, respond with `"id": null` and error code `-32600`.
* **`id` must be a UUID v4 string.** The WebApp always sends a randomly generated UUID v4; non-string ids are silently dropped by the WebApp.
* **The `id` in the response must match the `id` from the request.**
* **`result` and `error` are mutually exclusive.** On success include `result` and omit `error`; on failure include `error` and omit `result`. If the WebApp receives both, `error` takes precedence and the call rejects.
* **Timeouts.** The WebApp rejects requests that don't receive a response within **5 seconds** for `browser.open` and `clipboard.write`, and **30 seconds** for `file.download`. For `file.download`, respond once the download has been *triggered* (consent granted, transfer started), not once it completes.

## Methods

### `browser.open`

Opens a URL in the device's native browser (outside the WebView).

<ParamField body="url" type="string" required>
  URL to open. Must use the https\:// scheme.

  Pattern `^https://` · max length 2048
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "method": "browser.open",
    "params": {
      "url": "https://www.paylead.fr"
    },
    "id": 1
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "result": {},
    "id": 1
  }
  ```
</CodeGroup>

### `clipboard.write`

Copies text to the device clipboard.

<ParamField body="text" type="string" required>
  Text to copy to clipboard.

  max length 4096
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "method": "clipboard.write",
    "params": {
      "text": "voucher:89d89a45-4f57-4e68-9f61-dc71434b2f26"
    },
    "id": 1
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "result": {},
    "id": 1
  }
  ```
</CodeGroup>

### `file.download`

Downloads a file and presents it to the user.

<ParamField body="url" type="string" required>
  URL of the file to download. Must use the https\:// scheme.

  Pattern `^https://` · max length 200
</ParamField>

<ParamField body="filename" type="string">
  Suggested filename. Allowed characters: letters, digits, underscore, hyphen, dot. Must start with a letter or digit. The native app must still sanitise before writing to disk.

  Pattern `^[a-zA-Z0-9][a-zA-Z0-9_\-.]*$` · max length 255
</ParamField>

<ParamField body="mimeType" type="string">
  MIME type hint (e.g. application/pdf).

  one of `application/pdf`, `text/csv`, `image/jpeg`, `image/jpg`, `image/png`
</ParamField>

<CodeGroup>
  ```json Request theme={null}
  {
    "jsonrpc": "2.0",
    "method": "file.download",
    "params": {
      "url": "https://api.paylead.fr/voucher/89d89a45-4f57-4e68-9f61-dc71434b2f26.pdf",
      "filename": "89d89a45-4f57-4e68-9f61-dc71434b2f26.pdf",
      "mimeType": "application/pdf"
    },
    "id": 1
  }
  ```

  ```json Response theme={null}
  {
    "jsonrpc": "2.0",
    "result": {},
    "id": 1
  }
  ```
</CodeGroup>

## Error codes

When a method fails, the native app responds with a JSON-RPC 2.0 error object
carrying one of these codes in `error.code`. `error.message` must never contain
stack traces, filesystem paths, or other sensitive information.

| Code     | Description                                                                                                             |
| -------- | ----------------------------------------------------------------------------------------------------------------------- |
| `-32600` | Invalid JSON-RPC 2.0 request                                                                                            |
| `-32601` | Method not found                                                                                                        |
| `-32602` | Invalid or missing params                                                                                               |
| `-32000` | Runtime error (scheme blocked, download failed, etc.) — error.message must not contain stack traces or filesystem paths |
| `-32001` | Permission denied (location, etc.) — error.message must not contain stack traces or filesystem paths                    |

## Integration guide

<Tabs>
  <Tab title="iOS (Swift / WKWebView)">
    <Steps>
      <Step title="Register the message handler">
        Register a `WKScriptMessageHandler` named **`payleadBridge`**.
      </Step>

      <Step title="Receive the request">
        Receive JSON-RPC requests in `userContentController(_:didReceive:)`.
      </Step>

      <Step title="Execute and respond">
        Execute the action and call back with `callAsyncJavaScript` using named argument binding.
      </Step>
    </Steps>

    ```swift theme={null}
    let config = WKWebViewConfiguration()
    config.userContentController.add(self, name: "payleadBridge")
    // Recommended (iOS 14+): restrict navigation to your app's domains
    config.limitsNavigationsToAppBoundDomains = true

    // Hosts allowed to reach the bridge: any sub-domain of paylead.fr / .tech / .eu
    let allowedHost = #"\.paylead\.(fr|tech|eu)$"#

    func userContentController(_ controller: WKUserContentController,
                               didReceive message: WKScriptMessage) {
        // Reject messages from sub-frames or unknown origins
        guard message.frameInfo.isMainFrame,
              let host = message.frameInfo.request.url?.host,
              host.range(of: allowedHost, options: .regularExpression) != nil else { return }

        guard message.name == "payleadBridge",
              let body = message.body as? [String: Any],
              let jsonrpc = body["jsonrpc"] as? String, jsonrpc == "2.0",
              let method = body["method"] as? String,
              let id = body["id"] as? String else { return }

        let params = body["params"] as? [String: Any] ?? [:]
        // Validate params against the method's JSON Schema (schema.json).
        // On failure respond with error code -32602 (Invalid params).
        guard validateParams(method: method, params: params) else { return }

        handleBridgeMethod(method, params: params) { result in
            let response: [String: Any] = ["jsonrpc": "2.0", "result": result, "id": id]
            guard let data = try? JSONSerialization.data(withJSONObject: response),
                  let jsonStr = String(data: data, encoding: .utf8) else { return }
            // Safe: jsonStr is passed as a named argument, no interpolation into JS
            webView.callAsyncJavaScript(
                "window.payleadBridge.response(jsonString)",
                arguments: ["jsonString": jsonStr],
                in: nil, in: .defaultClient, completionHandler: nil
            )
        }
    }
    ```

    <Warning>
      * Check `message.frameInfo.isMainFrame` and verify the host against your allowlist before processing any message. Cross-origin iframes inside the WebView otherwise share access to `window.webkit.messageHandlers.payleadBridge`.
      * Use `limitsNavigationsToAppBoundDomains = true` and declare your domains under `WKAppBoundDomains` in `Info.plist` (iOS 14+).
      * Block navigations to hosts outside your allowlist in `decidePolicyFor`.
    </Warning>
  </Tab>

  <Tab title="Android (Kotlin / WebView)">
    Android's `addJavascriptInterface` only takes effect on the next page load and,
    once registered, is exposed to **every frame** until removed and reloaded. The
    safest approach is to **block navigations to untrusted origins entirely**,
    register the interface once, and re-check the origin on every call as defense
    in depth.

    ```kotlin theme={null}
    // Hosts allowed to reach the bridge: any sub-domain of paylead.fr / .tech / .eu
    val allowedHost = Regex("""\.paylead\.(fr|tech|eu)$""")

    webView.addJavascriptInterface(PayleadBridgeInterface(webView), "PayleadBridge")

    webView.webViewClient = object : WebViewClient() {
        override fun shouldOverrideUrlLoading(
            view: WebView, request: WebResourceRequest
        ): Boolean {
            val host = request.url.host ?: return true
            return !allowedHost.containsMatchIn(host) // true = navigation blocked
        }
    }

    class PayleadBridgeInterface(private val webView: WebView) {
        @JavascriptInterface
        fun postMessage(json: String) {
            webView.post {
                // Defense in depth: re-check the current URL on every call
                val host = Uri.parse(webView.url).host ?: return@post
                if (!allowedHost.containsMatchIn(host)) return@post

                val request = try { JSONObject(json) } catch (_: Exception) { return@post }
                if (request.optString("jsonrpc") != "2.0") return@post
                val method = request.optString("method").ifEmpty { return@post }
                val id = request.optString("id").ifEmpty { return@post }
                val params = request.optJSONObject("params") ?: JSONObject()
                // Validate params against the method's JSON Schema (schema.json).
                // On failure respond with error code -32602 (Invalid params).
                if (!validateParams(method, params)) return@post

                handleBridgeMethod(method, params) { result ->
                    val response = JSONObject().apply {
                        put("jsonrpc", "2.0"); put("result", result); put("id", id)
                    }
                    webView.post {
                        // Safe: JSONObject.quote() properly escapes the JSON string
                        webView.evaluateJavascript(
                            "window.payleadBridge.response(${JSONObject.quote(response.toString())})", null
                        )
                    }
                }
            }
        }
    }
    ```

    <Warning>
      * Block navigations outside your allowlist in `shouldOverrideUrlLoading`. Removing the interface in `onPageStarted`/`onPageFinished` does **not** take effect until the next reload.
      * Re-check the current URL from inside `postMessage` itself.
      * Disable file access: `setAllowFileAccess(false)`, `setAllowFileAccessFromFileURLs(false)`, `setAllowUniversalAccessFromFileURLs(false)`, `setAllowContentAccess(false)`.
      * Enforce HTTPS only: `setMixedContentMode(WebSettings.MIXED_CONTENT_NEVER_ALLOW)`.
    </Warning>
  </Tab>
</Tabs>

## Security checklist for partners

Before shipping your integration, verify each item:

* [ ] **HTTPS only:** every URL received from the bridge is validated to start with `https://` before opening or downloading. `javascript:`, `data:`, `file:`, and `intent://` schemes are explicitly rejected.
* [ ] **Filename sanitisation:** the schema restricts filenames to a safe character set (`^[a-zA-Z0-9][a-zA-Z0-9_\-.]*$`); as defense in depth, the native app re-validates and strips null bytes / OS-reserved names before writing to disk.
* [ ] **Origin allowlist:** the bridge is only reachable from pages whose host matches your trusted allowlist (`*.paylead.fr`, `*.paylead.tech`, `*.paylead.eu`). Navigations outside the allowlist are blocked.
* [ ] **User consent:** `file.download` triggers a native prompt so the user can confirm or cancel.
* [ ] **App-bound domains** (iOS 14+): `limitsNavigationsToAppBoundDomains = true` and `WKAppBoundDomains` are configured in `Info.plist`.

## General recommendations

* **Back/forward gestures:** disable `allowsBackForwardNavigationGestures` (iOS) and handle the back button explicitly (Android) to avoid accidental off-domain navigations.
* **Loading indicators:** `file.download` can take up to 30 seconds; surface progress in your native UI.
* **Correlation logging:** when logging bridge errors natively, include the request `id` so failures can be cross-referenced with the WebApp's client-side logs.

## What's next

<CardGroup cols={2}>
  <Card title="Page URLs" icon="link" href="/program/webapp/page-urls">
    The WebApp URLs your app can open to land on a specific screen.
  </Card>

  <Card title="Troubleshooting" icon="life-buoy" href="/program/webapp/troubleshooting">
    Diagnose a recurring auth screen, lost cookies, or hard-to-trace behaviour.
  </Card>
</CardGroup>
