Show web pages inside your app and run sign-in flows that redirect back to it
One.Browser opens web pages without leaving your app, and runs sign-in
pages that end by redirecting to a URL your app owns. On iOS pages open in
SFSafariViewController and sign-in runs in ASWebAuthenticationSession.
On Android both use the system browser through Custom Tabs and Auth Tab.
Use it for terms, help articles, and checkout pages, and for OAuth sign-in. The page runs in the real browser engine, with the person’s saved logins and autofill, and your app never sees what they type.
import { Button } from 'react-native'import { One } from 'one'
function HelpButton() { return ( <Button title="Help" onPress={() => One.Browser.open('https://example.com/help', { toolbarColor: '#111827' })} /> )}To use sign-in, register a URL scheme for the redirect in your app config before prebuild:
import { one } from 'one/vite'
export default { plugins: [ one({ native: { app: { name: 'MyApp', scheme: 'myapp', }, }, }), ],}Then open the provider’s page with that redirect:
import { One } from 'one'
async function signIn() { const result = await One.Browser.openAuthSession( 'https://auth.example.com/authorize?redirect_uri=myapp://callback', 'myapp://callback' ) if (result.type !== 'success') return null return new URL(result.url).searchParams.get('code')}Pages on iOS. open presents a Safari view controller over the top-most
screen, full screen unless you pass presentationStyle. The promise stays
pending while it is up and resolves cancel when people tap Done, or
dismiss when you call dismiss(). Opening a second page while one is up
resolves locked at once.
Pages on Android. open launches a Custom Tab in the default browser, or
in browserPackage. Custom Tabs report nothing when they close, so open
resolves opened as soon as the tab starts, and dismiss() cannot close it.
The first One.Browser call binds to the default browser’s Custom Tabs
service, which is what warmup and mayLaunchUrl talk to. A page passed to mayLaunchUrl loads in
the background and opens faster in the next open.
Sign-in on iOS. openAuthSession starts an authentication session.
iOS asks people to allow the app to use the site for signing in, then shows
the page. When the page navigates to the redirect, the session closes and
resolves success with the full URL, query included. On iOS 17.4 and later
an http or https redirect is handed to Apple’s HTTPS callback, which
matches host and path; a custom scheme, and every redirect before 17.4,
matches by scheme alone. Any ending without a redirect,
including people tapping Cancel, resolves cancel.
Sign-in on Android. openAuthSession opens an Auth Tab, which hands the
redirect straight back to One. A browser without Auth Tab support opens a
Custom Tab instead, and the redirect reaches your app through the scheme you
registered; One takes the first incoming link with the redirect’s scheme.
If people return to the app any other way, the session resolves cancel.
Web. open calls window.open with a new tab, clears the new tab’s
opener, and resolves opened. A blocked popup rejects. There is no sign-in
session on web; redirect the page itself instead.
| Method | Resolves |
|---|---|
| open(url, options?) | How the page ended |
| dismiss() | dismiss, after closing on iOS |
| openAuthSession(url, redirect, options?) | The redirect, or how it ended |
| dismissAuthSession() | Nothing; returns at once |
| warmup(browserPackage?) | Whether Android started the browser |
| mayLaunchUrl(url, browserPackage?) | Whether Android took the hint |
| BrowserOpenOptions | Type | Description |
|---|---|---|
presentationStyleBrowserPresentationStyle = "overFullScreen"iOS. How the Safari view is presented. | BrowserPresentationStyle = "overFullScreen" | iOS. How the Safari view is presented. |
browserPackagestringAndroid. The browser app that opens the page. | string | Android. The browser app that opens the page. |
toolbarColorstringThe toolbar background, as #RRGGBB. | string | The toolbar background, as #RRGGBB. |
secondaryToolbarColorstringAndroid. The bottom toolbar background. | string | Android. The bottom toolbar background. |
controlsColorstringiOS. The color of the buttons. | string | iOS. The color of the buttons. |
showTitleboolean = falseAndroid. Show the page title above the URL. | boolean = false | Android. Show the page title above the URL. |
colorSchemeBrowserColorScheme = "system"Force light or dark browser chrome. | BrowserColorScheme = "system" | Force light or dark browser chrome. |
| BrowserAuthSessionOptions | Type | Description |
|---|---|---|
preferEphemeralSessionboolean = falseUse a private session that shares no cookies with the browser. | boolean = false | Use a private session that shares no cookies with the browser. |
openAuthSession also takes every open option, though only
browserPackage, toolbarColor, and colorScheme reach Android’s Auth Tab,
and none reach the iOS session.
BrowserResultType is cancel, dismiss, opened, or locked. A sign-in
that redirects resolves { type: 'success', url } instead. Failures reject
with an error code: E_BROWSER_URL on iOS for a URL with no scheme,
E_BROWSER_START when iOS cannot start the session, E_BROWSER_ACTIVITY
and E_BROWSER_OPEN on Android, and E_BROWSER_BLOCKED on web.
| iOS | Android | Web | |
|---|---|---|---|
| Pages | Safari view | Custom Tab | New tab |
| Page result | When it closes | Opened at once | Opened at once |
| Close from code | Yes | No | No |
| Sign-in | Auth session | Auth Tab | Rejects |
| Warmup | Resolves false | Yes | Resolves false |
opened as the end
of the call and refresh what the page might have changed when your screen
regains focus.openAuthSession while one is pending
resolves locked. On iOS the same is true of pages.#RRGGBBAA,
while Android reads them as #AARRGGBB, and Android also accepts color
names. A color neither side can parse is ignored.cancel only when people switch back to the app.dismissAuthSession resolves the pending call with dismiss on both
platforms, but only iOS closes the page.window.open outside a user
gesture, so call open straight from a press handler, without awaiting
anything first.import { useEffect } from 'react'import { Button } from 'react-native'import { One } from 'one'
const termsUrl = 'https://example.com/terms'
function TermsLink() { useEffect(() => { One.Browser.mayLaunchUrl(termsUrl) }, []) return <Button title="Terms of service" onPress={() => One.Browser.open(termsUrl, { showTitle: true })} />}When the screen mounts, Android starts loading the terms page in the
background, so the tap opens it ready. iOS and web resolve false and open
normally.
import { One } from 'one'
const redirect = 'myapp://callback'
function base64Url(bytes: Uint8Array) { return btoa(String.fromCharCode(...bytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')}
async function signInWithProvider() { const verifier = base64Url(crypto.getRandomValues(new Uint8Array(32))) const challenge = base64Url(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier)))) const state = crypto.randomUUID() const authorize = new URL('https://auth.example.com/authorize') authorize.search = new URLSearchParams({ client_id: 'my-client', response_type: 'code', redirect_uri: redirect, code_challenge: challenge, code_challenge_method: 'S256', state, }).toString()
const result = await One.Browser.openAuthSession(authorize.href, redirect, { preferEphemeralSession: true }) if (result.type !== 'success') return null const params = new URL(result.url).searchParams if (params.get('state') !== state) throw new Error('Sign-in state did not match') return { code: params.get('code'), verifier }}The standard crypto global works on native because One installs it; see
Crypto. Send the code and verifier to your token endpoint
to finish. The state check
proves the redirect answers this request, and the ephemeral session lets
people pick a different account each time.
import { One } from 'one'
async function checkout(url: string, onPaid: (handler: () => void) => () => void) { const stop = onPaid(() => { One.Browser.dismiss() }) const result = await One.Browser.open(url) stop() return result.type}Here onPaid is your own subscription, such as a server push or poll. On iOS
the sheet closes and open resolves dismiss; on Android the call already
resolved opened, so drive the next screen from onPaid itself.
Edit this page on GitHub.