packages

Web Browser

Open any http or https page in the system's in-app browser (Custom Tabs on Android, SFSafariViewController on iOS) with your toolbar colors, a share item, reader mode or bar collapsing. For OAuth sign-in, open_auth_session_async waits for the redirect to your app scheme and returns the final URL. On Android you can warm up the browser for a faster first paint. On the desktop the system browser opens, so you can develop without a phone.

  • pyxpo-web-browser
  • v0.1.0a1
  • Native: Swift + Kotlin
  • Like expo-web-browser
The in-app browser on Android and iOS.
The in-app browser on Android and iOS.

Install

Install the package into the same environment as pyxpo.

$pip install pyxpo-web-browser

Add web-browser to modules in your app config, then import it in your code.

pyproject.toml
[tool.pyxpo]
modules = ["web-browser"]
python
import pyxpo_web_browser as WebBrowser

Needs Python 3.9+ and pyxpo>=0.1.0a2.

This module has native code. After you add it, run pyxpo run or pyxpo build again so it is compiled into the app. Hot reload alone is not enough.

Platforms

iOSiOS 15+. SFSafariViewController and ASWebAuthenticationSession. Resolves when the browser closes.
AndroidAPI 24+. Custom Tabs. Returns {"type": "opened"} at once; sign-in comes back as a deep link.
Desktop previewOpens the system browser. Sign-in raises NotSupportedError.
WebSame as the desktop.

Quick start

app/index.py
from pyxpo import Button, Text, View, component, use_async, use_state
import pyxpo_web_browser as WebBrowser


@component
def Links():
    taps, set_taps = use_state(0)
    result, set_result = use_state(None)

    async def run():
        if taps > 0:
            set_result(await WebBrowser.open_browser_async(
                "https://github.com/pyxpo/pyxpo", toolbar_color="#1a73e8", controls_color="#ffffff"))

    use_async(run, [taps])
    return View(
        Button("Open pyxpo on GitHub", lambda: set_taps(taps + 1)),
        Text("result: %s" % (result,)),
    )

Configuration

App scheme for sign-in

scheme in [tool.pyxpo] is the deep-link scheme for sign-in redirects such as myapp://callback. It is only needed for open_auth_session_async. The package adds an Android queries entry so the app can find browsers and Custom Tabs services.

pyproject.toml
[tool.pyxpo]
scheme = "myapp"
modules = ["web-browser"]

Permissions

pyxpo adds these to Info.plist and AndroidManifest.xml for you when the module is listed.

iOSNone. No Info.plist texts.
AndroidNone.

API reference

Browser

On iOS the awaited result arrives when the user closes the browser ({"type": "cancel"}). On Android it arrives at once ({"type": "opened"}). Never use lambda: WebBrowser.open_browser_async(...) as a handler: the coroutine would not be awaited.

await open_browser_async(url, **options)→ dict

Opens an http or https page. Any other scheme raises ValueError. Use pyxpo.Linking for tel: and others.

await dismiss_browser()→ dict

Closes the open page; returns {"type": "dismiss"}.

Sign-in

redirect_url must start with the app's scheme. On iOS the system watches for the redirect. On Android the page opens in the app's task and the redirect arrives as a deep link that closes it.

await open_auth_session_async(url, redirect_url=None, **options)→ dict

{"type": "success", "url": ...} with the redirect URL and its query. If the user returns without the redirect, the type is dismiss (Android) or cancel.

dismiss_auth_session()→ None

Closes an open sign-in page.

maybe_complete_auth_session()→ dict

{"type": "failed", ...}. Only Expo's web target uses it.

Warm-up (Android)

On iOS and the desktop the warm-up functions return {}.

await warm_up_async(browser_package=None)→ dict

Start the browser's service early.

await may_init_with_url_async(url, browser_package=None)→ dict

Preload a page the user will probably open.

await cool_down_async(browser_package=None)→ dict

Release the warmed-up service.

await get_custom_tabs_supporting_browsers_async()→ dict

browserPackages, servicePackages, defaultBrowserPackage, preferredBrowserPackage. Raises NotSupportedError on iOS.

Types

Options are keyword arguments; camelCase names work too, such as toolbarColor=... and openBrowserAsync.

Options

KeyTypeDescription
toolbar_colorbothAny pyxpo color, such as "#202020" or "orange".
secondary_toolbar_colorAndroidThe bottom toolbar color.
controls_coloriOSTint of the buttons.
enable_bar_collapsingbothHide the bar while scrolling.
show_titleAndroidShow the page title.
enable_default_share_menu_itemAndroidAdd a Share item to the menu.
show_in_recentsAndroidKeep the tab in the recent apps list. Default off.
create_taskAndroidOpen the tab in its own task. Default on.
browser_packageAndroidUse this browser instead of the preferred one.
dismiss_button_styleiOS"done", "close" or "cancel". Anything else raises ValueError.
reader_modeiOSEnter Reader when available.
presentation_styleiOSA WebBrowserPresentationStyle. Default OVER_FULL_SCREEN.
prefer_ephemeral_sessioniOS, sign-inShare no cookies with Safari. Also skips the "wants to use ... to sign in" prompt.

WebBrowserResultType

KeyTypeDescription
openedstrAndroid: the tab was opened. Desktop: the system browser was started.
cancelstriOS: the user closed the browser, or closed the sign-in page.
dismissstrYour code dismissed it. On Android sign-in, the user came back without finishing.
lockedstrAnother browser or session is already open. Wait for it to close.
successstrSign-in only. result["url"] is the redirect URL.

WebBrowserPresentationStyle

KeyTypeDescription
FULL_SCREEN, PAGE_SHEET, FORM_SHEET, CURRENT_CONTEXT, OVER_FULL_SCREEN, OVER_CURRENT_CONTEXT, POPOVER, AUTOMATICiOSThe UIKit modal style. OVER_FULL_SCREEN is the default.

Errors

KeyTypeDescription
ValueErrorbad callAn empty URL, a scheme other than http or https, or a bad dismiss_button_style.
NotSupportedErrorpyxpo.module_apiSign-in on the desktop, or get_custom_tabs_supporting_browsers_async on iOS.
ModuleNotLinkedErrorpyxpo.module_api"web-browser" is missing from modules. Add it and rebuild.

Examples

Recipe: sign in with an OAuth provider

Open the provider's page, wait for the redirect to myapp://callback and read the code. Exchange the code on your server, never in the app.

The in-app browser open on github.com, on Android and iOS.
The in-app browser open on github.com, on Android and iOS.
python
from urllib.parse import parse_qs, urlencode, urlparse

from pyxpo import Button, Text, View, component, use_async, use_state
import pyxpo_web_browser as WebBrowser

AUTH = "https://auth.example.com/authorize"
REDIRECT = "myapp://callback"


@component
def SignIn():
    taps, set_taps = use_state(0)
    status, set_status = use_state("Signed out")

    async def run():
        if taps == 0:
            return
        url = AUTH + "?" + urlencode({"client_id": "abc", "redirect_uri": REDIRECT,
                                       "response_type": "code", "scope": "openid"})
        res = await WebBrowser.open_auth_session_async(url, REDIRECT, prefer_ephemeral_session=True)
        if res["type"] == "success":
            code = parse_qs(urlparse(res["url"]).query).get("code", [""])[0]
            set_status("Got a code (%d chars). Exchange it on your server." % len(code))
        elif res["type"] == "cancel":
            set_status("Cancelled")
        else:
            set_status("Result: %s" % res["type"])

    use_async(run, [taps])
    return View(Text(status), Button("Sign in", lambda: set_taps(taps + 1)))

Warm up Custom Tabs

Start the browser early on Android when the user will probably open a page soon.

python
import pyxpo_web_browser as WebBrowser
from pyxpo import Platform


async def prepare_help_page():
    if Platform.OS == "android":
        await WebBrowser.warm_up_async()
        await WebBrowser.may_init_with_url_async("https://pyxpo.dev")

Testing

On the desktop host the system browser path runs, so patch webbrowser.open.

python
import asyncio

import pytest
import pyxpo_web_browser as wb


def test_rejects_non_http_urls():
    with pytest.raises(ValueError):
        asyncio.run(wb.open_browser_async("file:///etc/passwd"))


def test_desktop_opens_system_browser(monkeypatch):
    import webbrowser
    seen = []
    monkeypatch.setattr(webbrowser, "open", lambda url: seen.append(url) or True)
    res = asyncio.run(wb.open_browser_async("https://pyxpo.dev"))
    assert res == {"type": "opened"}
    assert seen == ["https://pyxpo.dev"]

Good to know

  • It is not a WebView. It is the system's browser UI, so the user keeps their cookies and autofill, and your app cannot read the page.
  • On Android 15 and later a tab on top puts your app in the background, so dismiss_browser() may not close it. Sign-in redirects are not affected.
  • If sign-in never returns on Android, the redirect does not start with the app scheme, or the provider rejects it.
  • On the desktop dismiss_browser() returns {"type": "dismiss"} without closing anything.