lecodes-sdk 2.2.6 → 2.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -45,7 +45,7 @@
45
45
  "marcidb-embedded": "^0.15.0",
46
46
  "sharp": "^0.35.2"
47
47
  },
48
- "version": "2.2.6",
48
+ "version": "2.2.8",
49
49
  "files": [
50
50
  "src",
51
51
  "dist",
package/src/chisel.ts CHANGED
@@ -118,7 +118,7 @@ export const chiselVocab: ChiselVocab = {
118
118
  },
119
119
  leaves: [
120
120
  "UIText", "UIImage", "UIVideo", "UIInput", "UITextArea", "UISpacer",
121
- "UITabs", "UIVirtualizedList", "NativeView", "CameraView", "MapView", "QRScanner",
121
+ "UITabs", "UIVirtualizedList", "NativeView", "CameraView", "MapView", "QRScanner", "WebView", "RTCVideoView",
122
122
  ],
123
123
  containers: [ "UIColumn", "UIRow", "UIBox", "UIButton", "UIScrollable", "UIScreen", "UIWidget" ],
124
124
  text: "UIText",
@@ -133,7 +133,7 @@ export const chiselVocab: ChiselVocab = {
133
133
  "append", "insert", "remove", "setContent", "widgets", "keepAlive", "url",
134
134
  "detents", "dismissible", "transition", "setSourceRect",
135
135
  "push", "pop", "popToRoot", "replace", "select",
136
- // UITabs / UIVirtualizedList's hand feed / the native views (NativeView, CameraView, MapView, QRScanner)
136
+ // UITabs / UIVirtualizedList's hand feed / the native views (NativeView, CameraView, MapView, QRScanner, WebView, RTCVideoView)
137
137
  "badge", "setData", "update", "prepend", "removeByKey",
138
138
  "on", "off", "onReady", "onError", "onTap", "onMove", "onScan",
139
139
  "flyTo", "jumpTo", "fitPoints", "setPadding", "setUserLocation",
package/src/inject.ts CHANGED
@@ -151,6 +151,14 @@ export {
151
151
  type LineLayer, type LineLayerOptions, type UserLocationOptions,
152
152
  } from "./plugins/gen/map/sdk/map"
153
153
  export { Push, type PushPayload, type PushStatus, type PushRegisterOptions, type PushEvent } from "./plugins/gen/push/sdk/push"
154
+ export { Browser, WebView, type BrowserOpenOptions, type WebViewParams, type WebViewEvents } from "./plugins/gen/browser/sdk/browser"
155
+ export {
156
+ RTCPeerConnection, MediaStream, MediaStreamTrack, RTCDataChannel, RTCRtpSender, RTCRtpReceiver, RTCRtpTransceiver,
157
+ RTCSessionDescription, RTCIceCandidate, RTCEventTarget, mediaDevices, RTCVideoView,
158
+ type RTCConfiguration, type RTCVideoViewOptions, type MediaStreamConstraints, type RTCIceCandidateInit,
159
+ type RTCPeerConnectionIceEvent, type RTCTrackEvent, type RTCDataChannelEvent, type RTCMessageEvent, type RTCErrorEvent, type RTCEvent,
160
+ type RTCEventListener,
161
+ } from "./plugins/gen/webrtc/sdk/webrtc"
154
162
  export { Service } from "./plugins/service"
155
163
 
156
164
  // ---- scenes as data (.scene.ts files — see src/scene/defineScene.ts) ----
package/src/plugin.ts CHANGED
@@ -13,8 +13,13 @@ import { ServiceClient } from "./runtime/service"
13
13
  import { NativeView, NativeViewElement } from "./ui/NativeView"
14
14
  import { _navEpoch } from "./ui/presentable"
15
15
  import { _requestCameraPermission } from "./plugins/permission"
16
+ import { appEventsOff, appEventsOn } from "./runtime/appEvents"
16
17
 
17
18
  export type { PresentOptions } from "./ui/presentable"
19
+ // The SDK UI a wrapper builds its own chrome with (the browser plugin's bar over an embedded page) and
20
+ // the navigation it presents through. Re-exported, never copied: chisel keys on these names.
21
+ export { Presentable, Router, UIBox, UIButton, UIColumn, UIRow, UIScreen, UIText } from "./ui/UI"
22
+ export { theme } from "./ui/theme"
18
23
  export type { NativeView } from "./ui/NativeView"
19
24
  export type { FetchResponse } from "./runtime/fetch"
20
25
 
@@ -172,6 +177,14 @@ export const _fromFile = (file: File): { systemId: number, name: string, size: n
172
177
 
173
178
  // ---- for the hand-written part ---------------------------------------------------------------------
174
179
 
180
+ /** Hear the app's "url" events (a deep link while the app runs — an OAuth redirect, a checkout's
181
+ * return) until the returned function is called. The listener the `app` global fans out from. */
182
+ export const _onAppUrl = (callback: (url: string) => void): (() => void) => {
183
+ const listener = (data?: string): void => { if (data) callback(data) }
184
+ appEventsOn("url", listener)
185
+ return () => appEventsOff("url", listener)
186
+ }
187
+
175
188
  /** The text of a host buffer — what a bundled `asset()` is in a shell compile (`"id:N"`): the host
176
189
  * already holds the bytes. Empty when there is no such buffer. */
177
190
  export const bufferText = (systemId: number): string => _creatorFetch.toText(systemId)
@@ -0,0 +1,108 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/browser/contract.d.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // browser — web content inside the app, on the two surfaces the platforms give:
4
+ // - the SYSTEM SHEET (the "browser" service): SFSafariViewController / ASWebAuthenticationSession on
5
+ // iOS, Chrome Custom Tabs on Android, a popup on the web. It shares the system browser's logins,
6
+ // passkeys and Apple Pay, and OAuth providers accept it. The app sees nothing of the page.
7
+ // - the EMBEDDED PAGE (the "webView" view): WKWebView / android.webkit.WebView / an iframe. A tree
8
+ // node the app embeds or presents fullscreen, with navigation events the app acts on.
9
+ //
10
+ // The hosts are dumb on purpose: they intercept a URL prefix and report it, forward the platform's
11
+ // callback URL, hand a non-http link to the system. Which URL ends a flow and what it means is the
12
+ // app's side (`Browser.open` in sdk/).
13
+
14
+ import type { Json, Service, View } from "../../../plugin"
15
+
16
+ // ---- the sheet ------------------------------------------------------------------------------------
17
+
18
+ export interface BrowserParams {}
19
+
20
+ export interface BrowserOpenOptions {
21
+ /** An auth flow: the scheme the provider redirects back to (`myshop` of `myshop://oauth`). iOS
22
+ * runs an ASWebAuthenticationSession and resolves `open` with the callback URL itself. Android
23
+ * and the web have nothing to do here: the redirect reaches the app as its "url" event. */
24
+ callbackScheme?: string
25
+ /** A private session: no cookies shared with the system browser. Honored on iOS (with a
26
+ * callbackScheme), ignored elsewhere. Default false. */
27
+ ephemeral?: boolean
28
+ /** The sheet's tint, "#RRGGBB". */
29
+ color?: string
30
+ }
31
+
32
+ export interface BrowserResult {
33
+ /** The callback URL the platform intercepted (iOS, with a callbackScheme); absent when the sheet
34
+ * was dismissed. */
35
+ url?: string
36
+ }
37
+
38
+ export interface BrowserEvents {
39
+ /** The sheet went away: the user dismissed it, the platform handed back the callback, or
40
+ * close(). */
41
+ closed: void
42
+ }
43
+
44
+ export interface Browser extends Service<"browser", BrowserParams, BrowserEvents> {
45
+ /** Show `url` in the system's in-app browser sheet. Resolves when the sheet is dismissed — with
46
+ * the callback URL when the platform intercepted one. One sheet at a time.
47
+ * @rejects busy invalid */
48
+ show(url: string, options?: BrowserOpenOptions): Promise<BrowserResult>
49
+ /** Dismiss the sheet; a no-op without one. */
50
+ dismiss(): Promise<void>
51
+ }
52
+
53
+ // ---- the embedded page ----------------------------------------------------------------------------
54
+
55
+ export interface WebViewParams {
56
+ /** What to load first. */
57
+ url?: string
58
+ /** Main-frame URLs that END a flow: a navigation whose URL starts with one is NOT loaded, the
59
+ * view emits `stop` instead. */
60
+ stops?: string[]
61
+ /** Install `window.lecodes.postMessage(data)` for the page (the `message` event) and let
62
+ * `postMessage` reach it (`window.lecodes.onMessage`). Default false — a user script disables
63
+ * Apple Pay in that WKWebView, so a checkout page wants it off. */
64
+ bridge?: boolean
65
+ /** A navigation to a non-http(s) URL — tel:, mailto:, intent:, a bank's app in a checkout:
66
+ * "open" hands it to the system, "block" drops it. Default "open". */
67
+ externalLinks?: "open" | "block"
68
+ userAgent?: string
69
+ /** A private session: no cookies or storage kept after the view goes. Default false. */
70
+ ephemeral?: boolean
71
+ }
72
+
73
+ export interface WebViewEvents {
74
+ /** A main-frame navigation started. */
75
+ navigate: { url: string }
76
+ /** The page finished loading. */
77
+ load: { url: string, title: string }
78
+ /** The page failed to load. */
79
+ error: { url: string, message: string }
80
+ /** A stop URL was reached: the navigation was cancelled, the page did not load. */
81
+ stop: { url: string }
82
+ /** The page called `window.lecodes.postMessage(data)` (with `bridge`). */
83
+ message: { data: Json }
84
+ /** The page called `window.close()`. */
85
+ exit: void
86
+ }
87
+
88
+ export interface WebView extends View<"webView", WebViewParams, WebViewEvents> {
89
+ /** Load `url`, with extra request headers for the first request. @rejects invalid */
90
+ load(url: string, headers?: { [name: string]: string }): Promise<void>
91
+ /** Show `html` as the page; relative URLs in it resolve against `baseUrl`. */
92
+ loadHtml(html: string, baseUrl?: string): Promise<void>
93
+ /** Replace the stop list (see WebViewParams.stops). */
94
+ setStops(stops: string[]): Promise<void>
95
+ reload(): Promise<void>
96
+ /** Stop the current load. */
97
+ stopLoading(): Promise<void>
98
+ goBack(): Promise<void>
99
+ goForward(): Promise<void>
100
+ canGoBack(): Promise<boolean>
101
+ /** Run `js` in the page and return what it evaluates to, as JSON. On iOS this disables Apple Pay
102
+ * in the view until its next navigation. @rejects failed */
103
+ evaluate(js: string): Promise<Json>
104
+ /** Deliver `data` to the page's `window.lecodes.onMessage(data)` (with `bridge`). */
105
+ postMessage(data: Json): Promise<void>
106
+ /** Clear the cookies and the storage of this view's session. */
107
+ clearData(): Promise<void>
108
+ }
@@ -0,0 +1,48 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/browser/sdk/browser.Browser.gen.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // GENERATED by `lecodes plugin gen` from contract.d.ts — do not edit.
4
+ // browser: the service "browser", contract version 1.
5
+
6
+ import { ServiceChannel } from "../../../../plugin"
7
+ import type { BrowserEvents, BrowserOpenOptions, BrowserParams, BrowserResult } from "../contract"
8
+
9
+ /** What the halves register under. */
10
+ export const NAME = "browser"
11
+ /** The contract's version: the highest `@since` of its members. */
12
+ export const VERSION = 1
13
+
14
+ // ---- the wire, call by call ----------------------------------------------------------------------
15
+
16
+ /** Show `url` in the system's in-app browser sheet. Resolves when the sheet is dismissed — with
17
+ * the callback URL when the platform intercepted one. One sheet at a time.
18
+ * Rejects with "busy" / "invalid". */
19
+ export const show = (service: ServiceChannel<BrowserEvents>, url: string, options?: BrowserOpenOptions): Promise<BrowserResult> =>
20
+ service._call("show", [url, options])
21
+
22
+ /** Dismiss the sheet; a no-op without one. */
23
+ export const dismiss = (service: ServiceChannel<BrowserEvents>): Promise<void> =>
24
+ service._call("dismiss", [])
25
+
26
+ // ---- what the host's half has --------------------------------------------------------------------
27
+
28
+ /** A method of the contract. */
29
+ export type Method = "show" | "dismiss"
30
+
31
+ /** Whether this host has the service "browser" — and, given a method, a half new enough for it
32
+ * (a method added after the first version of the contract is tagged `@since`). */
33
+ export const supports = (method?: Method): boolean =>
34
+ ServiceChannel.supports(NAME) // every method is of version 1
35
+
36
+ // ---- the client -----------------------------------------------------------------------------------
37
+
38
+ /** The service "browser" with the wire's methods as its own. */
39
+ export class BrowserBase extends ServiceChannel<BrowserEvents> {
40
+ constructor(params: BrowserParams = {}) { super("Browser", NAME, params) }
41
+
42
+ /** Show `url` in the system's in-app browser sheet. Resolves when the sheet is dismissed — with
43
+ * the callback URL when the platform intercepted one. One sheet at a time.
44
+ * Rejects with "busy" / "invalid". */
45
+ show(url: string, options?: BrowserOpenOptions): Promise<BrowserResult> { return show(this, url, options) }
46
+ /** Dismiss the sheet; a no-op without one. */
47
+ dismiss(): Promise<void> { return dismiss(this) }
48
+ }
@@ -0,0 +1,96 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/browser/sdk/browser.WebView.gen.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // GENERATED by `lecodes plugin gen` from contract.d.ts — do not edit.
4
+ // browser: the view "webView", contract version 1.
5
+
6
+ import { ViewChannel, type Json } from "../../../../plugin"
7
+ import type { WebViewEvents, WebViewParams } from "../contract"
8
+
9
+ /** What the halves register under. */
10
+ export const NAME = "webView"
11
+ /** The contract's version: the highest `@since` of its members. */
12
+ export const VERSION = 1
13
+
14
+ // ---- the wire, call by call ----------------------------------------------------------------------
15
+
16
+ /** Load `url`, with extra request headers for the first request.
17
+ * Rejects with "invalid". */
18
+ export const load = (view: ViewChannel<WebViewEvents>, url: string, headers?: { [key: string]: string }): Promise<void> =>
19
+ view._call("load", [url, headers])
20
+
21
+ /** Show `html` as the page; relative URLs in it resolve against `baseUrl`. */
22
+ export const loadHtml = (view: ViewChannel<WebViewEvents>, html: string, baseUrl?: string): Promise<void> =>
23
+ view._call("loadHtml", [html, baseUrl])
24
+
25
+ /** Replace the stop list (see WebViewParams.stops). */
26
+ export const setStops = (view: ViewChannel<WebViewEvents>, stops: string[]): Promise<void> =>
27
+ view._call("setStops", [stops])
28
+
29
+ export const reload = (view: ViewChannel<WebViewEvents>): Promise<void> =>
30
+ view._call("reload", [])
31
+
32
+ /** Stop the current load. */
33
+ export const stopLoading = (view: ViewChannel<WebViewEvents>): Promise<void> =>
34
+ view._call("stopLoading", [])
35
+
36
+ export const goBack = (view: ViewChannel<WebViewEvents>): Promise<void> =>
37
+ view._call("goBack", [])
38
+
39
+ export const goForward = (view: ViewChannel<WebViewEvents>): Promise<void> =>
40
+ view._call("goForward", [])
41
+
42
+ export const canGoBack = (view: ViewChannel<WebViewEvents>): Promise<boolean> =>
43
+ view._call("canGoBack", [])
44
+
45
+ /** Run `js` in the page and return what it evaluates to, as JSON. On iOS this disables Apple Pay
46
+ * in the view until its next navigation.
47
+ * Rejects with "failed". */
48
+ export const evaluate = (view: ViewChannel<WebViewEvents>, js: string): Promise<Json> =>
49
+ view._call("evaluate", [js])
50
+
51
+ /** Deliver `data` to the page's `window.lecodes.onMessage(data)` (with `bridge`). */
52
+ export const postMessage = (view: ViewChannel<WebViewEvents>, data: Json): Promise<void> =>
53
+ view._call("postMessage", [data])
54
+
55
+ /** Clear the cookies and the storage of this view's session. */
56
+ export const clearData = (view: ViewChannel<WebViewEvents>): Promise<void> =>
57
+ view._call("clearData", [])
58
+
59
+ // ---- what the host's half has --------------------------------------------------------------------
60
+
61
+ /** A method of the contract. */
62
+ export type Method = "load" | "loadHtml" | "setStops" | "reload" | "stopLoading" | "goBack" | "goForward" | "canGoBack" | "evaluate" | "postMessage" | "clearData"
63
+
64
+ /** Whether this host has the view "webView" — and, given a method, a half new enough for it
65
+ * (a method added after the first version of the contract is tagged `@since`). */
66
+ export const supports = (method?: Method): boolean =>
67
+ ViewChannel.supports(NAME) // every method is of version 1
68
+
69
+ // ---- the element ----------------------------------------------------------------------------------
70
+
71
+ /** The view "webView" with the wire's methods as its own. */
72
+ export class WebViewBase extends ViewChannel<WebViewEvents> {
73
+ constructor(params: WebViewParams = {}) { super("WebView", NAME, params) }
74
+
75
+ /** Load `url`, with extra request headers for the first request.
76
+ * Rejects with "invalid". */
77
+ load(url: string, headers?: { [key: string]: string }): Promise<void> { return load(this, url, headers) }
78
+ /** Show `html` as the page; relative URLs in it resolve against `baseUrl`. */
79
+ loadHtml(html: string, baseUrl?: string): Promise<void> { return loadHtml(this, html, baseUrl) }
80
+ /** Replace the stop list (see WebViewParams.stops). */
81
+ setStops(stops: string[]): Promise<void> { return setStops(this, stops) }
82
+ reload(): Promise<void> { return reload(this) }
83
+ /** Stop the current load. */
84
+ stopLoading(): Promise<void> { return stopLoading(this) }
85
+ goBack(): Promise<void> { return goBack(this) }
86
+ goForward(): Promise<void> { return goForward(this) }
87
+ canGoBack(): Promise<boolean> { return canGoBack(this) }
88
+ /** Run `js` in the page and return what it evaluates to, as JSON. On iOS this disables Apple Pay
89
+ * in the view until its next navigation.
90
+ * Rejects with "failed". */
91
+ evaluate(js: string): Promise<Json> { return evaluate(this, js) }
92
+ /** Deliver `data` to the page's `window.lecodes.onMessage(data)` (with `bridge`). */
93
+ postMessage(data: Json): Promise<void> { return postMessage(this, data) }
94
+ /** Clear the cookies and the storage of this view's session. */
95
+ clearData(): Promise<void> { return clearData(this) }
96
+ }
@@ -0,0 +1,218 @@
1
+ // GENERATED by scripts/vendor-plugins.ts from lecodes-plugins/plugins/browser/sdk/browser.ts — do not edit here.
2
+ // Change it in the plugin repo and run `bun run vendor:plugins`; `bun run check:plugins` fails when this copy is stale.
3
+ // Browser.open — a URL on the surface the scenario needs, ONE verb for the three of them:
4
+ //
5
+ // await Browser.open(url) a page, in the system sheet
6
+ // const back = await Browser.open(authUrl, { returnUrl: "myshop://oauth" }) OAuth: the sheet,
7
+ // resolved with the redirect URL
8
+ // const back = await Browser.open(checkoutUrl, { returnUrl: [paid, failed], embedded: true })
9
+ // a checkout in an embedded page,
10
+ // resolved with the URL it reached
11
+ //
12
+ // It resolves with the return URL reached, or null when the user left. `returnUrl` says what ENDS
13
+ // the page; `embedded` says WHERE it runs — the system sheet (shared logins, passkeys, Apple Pay;
14
+ // the only surface OAuth providers accept) or a WebView presented fullscreen under a bar the SDK
15
+ // draws (an https return without verified app links, a 3DS flow, no system chrome).
16
+ //
17
+ // Hand-written over the generated wire because the flows are the SDK's, not the hosts':
18
+ // - the sheet's return is ONE of two paths — iOS intercepts the scheme itself and resolves `show`
19
+ // with the URL, Android and the web deliver the redirect as the app's "url" event. The wrapper
20
+ // races both and the host does nothing it would not do for a plain deep link;
21
+ // - the embedded flow is a WebView with the return URLs as its `stops`, inside a screen with a
22
+ // close bar, pushed through the Router when one is showing and opened directly otherwise.
23
+ //
24
+ // WebView() is the element itself: embed it among a screen's children, push it with your own
25
+ // chrome, drive it (`load`, `evaluate`, `postMessage`) and listen (`navigate`, `load`, `stop`,
26
+ // `message`).
27
+
28
+ import {
29
+ Presentable, Router, UIBox, UIButton, UIRow, UIScreen, UIText, theme, _onAppUrl, type Json, type Listener, type NativeView,
30
+ } from "../../../../plugin"
31
+ import { BrowserBase, NAME as SHEET_NAME } from "./browser.Browser.gen"
32
+ import { WebViewBase, NAME as VIEW_NAME, supports as viewSupports, type Method as WebViewMethod } from "./browser.WebView.gen"
33
+ import type { WebViewEvents, WebViewParams } from "../contract"
34
+
35
+ export type { WebViewEvents, WebViewParams } from "../contract"
36
+
37
+ export interface BrowserOpenOptions {
38
+ /** What ends the page: a URL prefix, or several (a checkout's success and failure URLs). The
39
+ * promise resolves with the URL reached. In the sheet every prefix must share one scheme — the
40
+ * platform intercepts one; a custom scheme needs app.json `urlScheme`, an https one the app's
41
+ * verified links. */
42
+ returnUrl?: string | string[]
43
+ /** Run the page in an embedded WebView under a close bar instead of the system sheet. The
44
+ * WebView intercepts any return URL itself, https included. */
45
+ embedded?: boolean
46
+ /** A private session: no cookies shared with the system browser, nothing kept after. */
47
+ ephemeral?: boolean
48
+ /** The sheet's tint, "#RRGGBB". */
49
+ color?: string
50
+ /** Embedded: the bar's title until the page reports its own. */
51
+ title?: string
52
+ }
53
+
54
+ /** The embedded page — the element of the "webView" view: embeddable among a screen's children,
55
+ * presentable (`open()`, `Router.push`), with the page's controls and events. */
56
+ export interface WebView extends NativeView {
57
+ /** Load `url`, with extra request headers for the first request. Rejects "invalid" for a URL
58
+ * that is not a web one. */
59
+ load(url: string, headers?: { [name: string]: string }): Promise<void>
60
+ /** Show `html` as the page; relative URLs in it resolve against `baseUrl`. */
61
+ loadHtml(html: string, baseUrl?: string): Promise<void>
62
+ /** Replace the URL prefixes that end a navigation with the `stop` event. */
63
+ setStops(stops: string[]): Promise<void>
64
+ reload(): Promise<void>
65
+ /** Stop the current load. */
66
+ stopLoading(): Promise<void>
67
+ goBack(): Promise<void>
68
+ goForward(): Promise<void>
69
+ canGoBack(): Promise<boolean>
70
+ /** Run `js` in the page and get what it evaluates to, as JSON. On iOS this disables Apple Pay
71
+ * in the view until its next navigation. */
72
+ evaluate(js: string): Promise<Json>
73
+ /** Deliver `data` to the page's `window.lecodes.onMessage(data)` (a view created with `bridge`). */
74
+ postMessage(data: Json): Promise<void>
75
+ /** Clear the cookies and the storage of this view's session. */
76
+ clearData(): Promise<void>
77
+ /** The page's events: `navigate`, `load`, `error`, `stop`, `message`, `exit`. */
78
+ on<K extends keyof WebViewEvents & string>(event: K, callback: Listener<WebViewEvents, K>): this
79
+ off<K extends keyof WebViewEvents & string>(event: K, callback: Listener<WebViewEvents, K>): this
80
+ }
81
+
82
+ const returnUrls = (options: BrowserOpenOptions): string[] =>
83
+ options.returnUrl === undefined ? [] : Array.isArray(options.returnUrl) ? options.returnUrl : [options.returnUrl]
84
+
85
+ const matching = (url: string, prefixes: string[]): string | undefined =>
86
+ prefixes.some(p => url.startsWith(p)) ? url : undefined
87
+
88
+ const schemeOf = (url: string): string => {
89
+ const m = /^([a-z][a-z0-9+.-]*):/i.exec(url)
90
+ return m ? m[1]!.toLowerCase() : ""
91
+ }
92
+
93
+ const unsupported = (what: string, channel: string): Error =>
94
+ new Error(`${what} isn't available on this host — it needs the "browser" plugin (app.json "plugins"), a registered "${channel}" ${channel === SHEET_NAME ? "service" : "view"}.`)
95
+
96
+ // ---- the sheet -------------------------------------------------------------------------------------
97
+
98
+ let sheet: BrowserBase | null = null
99
+ const service = (): BrowserBase => sheet ??= new BrowserBase()
100
+
101
+ const openSheet = (url: string, options: BrowserOpenOptions): Promise<string | null> => new Promise((resolve, reject) => {
102
+ if (!BrowserBase.supports(SHEET_NAME)) return reject(unsupported("Browser.open", SHEET_NAME))
103
+ const stops = returnUrls(options)
104
+ const schemes = new Set(stops.map(schemeOf))
105
+ if (schemes.size > 1) return reject(new Error("Browser.open: every returnUrl must share one scheme — the platform intercepts one."))
106
+ const scheme = stops.length ? [...schemes][0]! : ""
107
+ const wire: { callbackScheme?: string, ephemeral?: boolean, color?: string } = {}
108
+ // A custom scheme is what a platform can intercept; an https return is the app's verified link,
109
+ // which reaches it as a "url" event like any deep link.
110
+ if (scheme && scheme !== "http" && scheme !== "https") wire.callbackScheme = scheme
111
+ if (options.ephemeral) wire.ephemeral = true
112
+ if (options.color) wire.color = options.color
113
+
114
+ let done = false
115
+ const off = stops.length ? _onAppUrl(back => {
116
+ if (!matching(back, stops)) return
117
+ finish(back)
118
+ service().dismiss().catch(() => {})
119
+ }) : () => {}
120
+ const finish = (back: string | null): void => {
121
+ if (done) return
122
+ done = true
123
+ off()
124
+ resolve(back)
125
+ }
126
+ service().show(url, wire).then(
127
+ result => finish(result?.url ?? null),
128
+ error => { if (done) return; done = true; off(); reject(error) },
129
+ )
130
+ })
131
+
132
+ // ---- the embedded page ----------------------------------------------------------------------------
133
+
134
+ const openEmbedded = (url: string, options: BrowserOpenOptions): Promise<string | null> => new Promise((resolve, reject) => {
135
+ if (!WebViewBase.supports(VIEW_NAME)) return reject(unsupported("Browser.open({ embedded: true })", VIEW_NAME))
136
+ const stops = returnUrls(options)
137
+ const params: WebViewParams = { url, stops }
138
+ if (options.ephemeral) params.ephemeral = true
139
+ const view = new WebViewBase(params)
140
+
141
+ const title = UIText(options.title ?? "")
142
+ .style({ flex: 1, fontSize: 16, fontWeight: "600", color: theme.text, textAlign: "center", lineClamp: 1 })
143
+ const close = UIButton(UIText("✕").style({ fontSize: 20, color: theme.text }))
144
+ .style({ width: 44, height: 44, alignItems: "center", justifyContent: "center" })
145
+ const bar = UIRow(close, title, UIBox().style({ width: 44 }))
146
+ .style({ pt: "safe-top", alignItems: "center", bgColor: theme.surface, borderBottomWidth: 1, borderColor: theme.border })
147
+ const screen = UIScreen(bar, view.style({ flex: 1 })).style({ bgColor: theme.bg })
148
+
149
+ // Where it goes: onto the Router when its page is what is on screen, else in place of what is —
150
+ // and back to it after.
151
+ const previous = Presentable.current
152
+ const routed = previous !== null && previous === Router.current
153
+ let left = false
154
+ const leave = (): void => {
155
+ if (left) return
156
+ left = true
157
+ if (routed) Router.pop()
158
+ else { screen.close(); previous?.open() }
159
+ }
160
+ let done = false
161
+ const finish = (back: string | null): void => {
162
+ if (done) return
163
+ done = true
164
+ leave()
165
+ resolve(back)
166
+ }
167
+ view.on("stop", ({ url: back }) => finish(back))
168
+ view.on("exit", () => finish(null))
169
+ view.on("load", ({ title: loaded }) => { if (loaded) title.text = loaded })
170
+ close.onClick(() => finish(null))
171
+ screen.onBack(() => finish(null))
172
+ // Gone by another hand (the back swipe popped it, another destination replaced it): the flow is
173
+ // over and there is nothing left to leave.
174
+ screen.onClose(() => { left = true; finish(null) })
175
+
176
+ if (routed) Router.push(screen)
177
+ else screen.open()
178
+ })
179
+
180
+ // ---- the app's side -------------------------------------------------------------------------------
181
+
182
+ /**
183
+ * Web content inside the app. `Browser.isSupported` reports whether this host registered the
184
+ * system sheet; `WebView.isSupported` the embedded page.
185
+ */
186
+ // PURE IIFE so an app that never opens a page tree-shakes the whole plugin away.
187
+ export const Browser: {
188
+ /** Whether this host registered the "browser" service — the system sheet. */
189
+ readonly isSupported: boolean
190
+ /** Open `url`: in the system sheet, or (`embedded`) in a WebView under a close bar. Resolves
191
+ * with the return URL reached (`returnUrl`), or null when the user left. */
192
+ open(url: string, options?: BrowserOpenOptions): Promise<string | null>
193
+ } = /*#__PURE__*/ (() => {
194
+ const browser = {
195
+ open: (url: string, options: BrowserOpenOptions = {}): Promise<string | null> =>
196
+ options.embedded ? openEmbedded(url, options) : openSheet(url, options),
197
+ }
198
+ Object.defineProperty(browser, "isSupported", { get: (): boolean => BrowserBase.supports(SHEET_NAME) })
199
+ return browser as any
200
+ })()
201
+
202
+ /**
203
+ * Create an embedded page. `WebView.isSupported` reports whether this host has the capability —
204
+ * check it before offering the feature.
205
+ */
206
+ // PURE IIFE so an app that never embeds a page tree-shakes the view away.
207
+ export const WebView: {
208
+ (params?: WebViewParams): WebView
209
+ /** Whether this host registered a "webView" view. */
210
+ readonly isSupported: boolean
211
+ /** Whether the host's half has `method`: false on a host whose half is older than it. */
212
+ supports(method: WebViewMethod): boolean
213
+ } = /*#__PURE__*/ (() => {
214
+ const factory = (params?: WebViewParams): WebView => new WebViewBase(params) as unknown as WebView
215
+ Object.defineProperty(factory, "isSupported", { get: (): boolean => viewSupports() })
216
+ factory.supports = viewSupports
217
+ return factory as any
218
+ })()