@pmate/bridge 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Native application opening
2
2
 
3
+ ## Background
4
+
3
5
  The application catalog and deployment URL lookup belong to the apps API. The bridge only opens the resolved URL in a separate native WebView, leaving its caller alive.
4
6
 
5
7
  ```tsx
@@ -8,3 +10,19 @@ await openApp({ url: "https://issues.pmate.chat/" })
8
10
  ```
9
11
 
10
12
  Requires a host implementing `container.openApp`. Resolves `{ opened: true }` after native presentation, not after page load. Rejects unsupported hosts and invalid URLs; callers must display the error. HTTPS and loopback HTTP are allowed; embedded URL credentials are rejected. No authentication token is added to the URL. Closing the child returns to its existing parent. Browser apps should use their explicit browser navigation path instead.
13
+
14
+ ## Pending application notifications
15
+
16
+ A designated native launcher can use `getPendingAppOpen()` and `onAppOpenRequested()` to receive a destination containing only `appId`, `notificationId`, and `origin`. Other WebViews cannot read this queue. Hosts without this capability reject the query; their existing notification routing continues unchanged.
17
+
18
+ The launcher must first query the apps API with its current identity and Team, verify the application is visible, and resolve its deployed URL. Compare the resolved origin to the pending origin, then call `openApp({ url, notificationId })`. Native verifies that the same pending notification still exists and that the URL matches its origin, opens a new WebView, and consumes that pending request. A failed open retains the request until retry or expiry. Cancel stale API results when identity or Team changes.
19
+
20
+ The target application receives the full notification through its own authenticated online session. The launcher does not receive notification bodies or another application's token.
21
+
22
+ ## Runtime environment
23
+
24
+ Desktop hosts expose a preload bridge separately from mobile WebViews. Applications must not treat a missing mobile bridge as proof they are running in a browser.
25
+
26
+ `getRuntimeInfo()` returns `environment` (`browser`, `electron`, or `native`), desktop `platform`, `appVersion`, `electronVersion` when supplied by the host, and `capabilities.openNotificationSettings`. Detection uses host injection, never the user agent; it is not an authorization boundary. Native device details remain available through `useDeviceInfo()`.
27
+
28
+ `openNotificationSettings()` routes to the desktop host or native settings protocol and rejects on unavailable/failed operations. Electron notification permissions are managed by the OS: browser `Notification.permission` does not describe the main-process notification permission. Do not display browser permission prompts or claim OS permission is granted based on notification API support.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/bridge",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "files": [
5
5
  "src",
6
6
  "global.d.ts"
package/src/index.ts CHANGED
@@ -3,6 +3,8 @@ export * from "./SafeAreaVarsProvider"
3
3
  export * from "./hooks"
4
4
  export {
5
5
  openApp,
6
+ getPendingAppOpen,
7
+ onAppOpenRequested,
6
8
  cancelAlarm,
7
9
  cancelAllAlarms,
8
10
  connectBluetoothDevice,
@@ -27,3 +29,6 @@ export {
27
29
  writeBluetooth,
28
30
  } from "./internal"
29
31
  export type * from "./types"
32
+
33
+ export { getRuntimeInfo, openNotificationSettings } from "./runtime"
34
+ export type { RuntimeInfo } from "./runtime"
package/src/internal.ts CHANGED
@@ -253,5 +253,26 @@ export async function openApp(options: import("./types").OpenAppOptions): Promis
253
253
  if ((url.protocol !== "https:" && !(local && url.protocol === "http:")) || url.username || url.password) {
254
254
  return Promise.reject(new Error("App URL must use HTTPS (or loopback HTTP) without credentials"))
255
255
  }
256
- return requireBridge<import("./types").OpenAppResult>("container.openApp", { url: url.href })
256
+ const notificationId = options.notificationId
257
+ if (notificationId !== undefined && (!notificationId || notificationId.length > 512)) throw new Error("Invalid notification ID")
258
+ const result = await requireBridge<import("./types").OpenAppResult>("container.openApp", { url: url.href, ...(notificationId === undefined ? {} : { notificationId }) })
259
+ if (result?.opened !== true) throw new Error("Native app opening was not confirmed")
260
+ return result
261
+ }
262
+
263
+ /** Available only to a native host's designated launcher. Older hosts reject it. */
264
+ export async function getPendingAppOpen(): Promise<import("./types").AppOpenIntent | null> {
265
+ const result = await requireBridge<import("./types").AppOpenIntent | null>("container.getPendingAppOpen")
266
+ if (result === null) return null
267
+ if (!result || typeof result.appId !== "string" || !result.appId || result.appId.length > 128
268
+ || typeof result.notificationId !== "string" || !result.notificationId || result.notificationId.length > 512
269
+ || typeof result.origin !== "string" || result.origin.length > 2048) throw new Error("Invalid application notification")
270
+ const origin = new URL(result.origin)
271
+ if (origin.protocol !== "https:" || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) throw new Error("Invalid application notification origin")
272
+ return { appId: result.appId, notificationId: result.notificationId, origin: origin.origin }
273
+ }
274
+
275
+ /** A hint to re-read the pending intent; the event itself never supplies a URL to open. */
276
+ export function onAppOpenRequested(handler: () => void): () => void {
277
+ return hasWindow() ? (window as PmateWindow).PmateBridge?.on?.("container.appOpenRequested", handler) ?? (() => {}) : () => {}
257
278
  }
package/src/runtime.ts ADDED
@@ -0,0 +1,33 @@
1
+ /** Host identity comes from the preload/native bridge, never the user agent. */
2
+ export interface RuntimeInfo {
3
+ environment: "browser" | "electron" | "native"
4
+ platform?: string
5
+ appVersion?: string
6
+ electronVersion?: string
7
+ capabilities: { openNotificationSettings: boolean }
8
+ }
9
+ type HostWindow = Window & {
10
+ PmateBridge?: { call: (name: string, params?: unknown) => Promise<unknown> }
11
+ pmateDesktop?: {
12
+ runtime?: { platform: string; appVersion: string; electronVersion: string }
13
+ notifications?: { openSettings?: () => Promise<boolean> }
14
+ }
15
+ }
16
+ export function getRuntimeInfo(): RuntimeInfo {
17
+ const host = typeof window === "undefined" ? undefined : window as HostWindow
18
+ if (host?.pmateDesktop) return {
19
+ environment: "electron", ...host.pmateDesktop.runtime,
20
+ capabilities: { openNotificationSettings: typeof host.pmateDesktop.notifications?.openSettings === "function" },
21
+ }
22
+ return { environment: host?.PmateBridge ? "native" : "browser", capabilities: { openNotificationSettings: !!host?.PmateBridge } }
23
+ }
24
+ /** Rejects when the host cannot confirm opening settings; never uses browser permission APIs. */
25
+ export async function openNotificationSettings(): Promise<void> {
26
+ const host = typeof window === "undefined" ? undefined : window as HostWindow
27
+ if (getRuntimeInfo().environment === "electron") {
28
+ if (await host?.pmateDesktop?.notifications?.openSettings?.() !== true) throw new Error("当前桌面版本无法打开通知设置,请在系统设置中选择 Going Merry")
29
+ return
30
+ }
31
+ if (!host?.PmateBridge) throw new Error("当前环境不支持打开系统通知设置")
32
+ await host.PmateBridge.call("settings.open", { destination: "notification" })
33
+ }
package/src/types.ts CHANGED
@@ -209,5 +209,7 @@ export interface BluetoothDataEvent {
209
209
  }
210
210
 
211
211
  /** Opens a separate native WebView. The caller resolves the app URL via the apps API. */
212
- export interface OpenAppOptions { url: string }
212
+ export interface OpenAppOptions { url: string; notificationId?: string }
213
213
  export interface OpenAppResult { opened: true }
214
+ /** Destination only; notification content is delivered to the target app after authentication. */
215
+ export interface AppOpenIntent { appId: string; notificationId: string; origin: string }