@protobase/cli 0.73.1 → 0.73.2

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.
@@ -8,9 +8,13 @@ import { pushRecord } from "@protobase/schema/internal";
8
8
  * names the subscription's endpoint and the push names the record's user. A push without a title shows the app's
9
9
  * name alone. A push with no record, or for another endpoint or user, shows nothing: the worker closes what it showed,
10
10
  * clears the badge, drops the record and unsubscribes.
11
- * - `notificationclick` opens the notification's url only when it is an absolute `http` or `https` URL, parsed without
12
- * a base: on the worker's own origin in a window of the app, focused and navigated there, or in a new window when none
13
- * is open; on any other origin always in a new window. Anything else opens nothing, whoever showed the notification.
11
+ * - A push's tap opens its `notification.navigate`, or its `url` when it has none, and each of its `notification.actions`
12
+ * shows as a button that opens the action's own `navigate`, or the tap's url when it has none. Nothing else in the
13
+ * push, such as its `data`, is kept for a tap.
14
+ * - `notificationclick` opens the tapped action's url, or the notification's for a tap on no action it knows, only when
15
+ * it is an absolute `http` or `https` URL, parsed without a base: on the worker's own origin in a window of the app,
16
+ * focused and navigated there, or in a new window when none is open; on any other origin always in a new window.
17
+ * Anything else opens nothing, whoever showed the notification, and a failing action never falls back to another url.
14
18
  * - `pushsubscriptionchange` marks an existing record for renewal, which the helper registers once its user is signed
15
19
  * in; the worker has no token to do so itself.
16
20
  */
@@ -59,6 +63,22 @@ const pushData = (data) => {
59
63
  }
60
64
 
61
65
  const text = (value) => (typeof value === 'string' ? value : undefined)
66
+ const plainObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
67
+
68
+ // The checked urls a tap may open: the notification's, and per action [action, url], with null for a navigate that
69
+ // fails, so its button opens nothing. Pairs rather than an object keyed by the push's action names.
70
+ const tapTargets = (push) => {
71
+ const notification = plainObject(push.notification) ? push.notification : {}
72
+ const url = notification.navigate !== undefined ? tapUrl(notification.navigate) : tapUrl(push.url)
73
+ const actions = (Array.isArray(notification.actions) ? notification.actions : []).filter(
74
+ (action) => plainObject(action) && text(action.action) !== undefined && text(action.title) !== undefined,
75
+ )
76
+ return {
77
+ url,
78
+ buttons: actions.map((action) => ({ action: action.action, title: action.title })),
79
+ actions: actions.map((action) => [action.action, action.navigate === undefined ? url ?? null : tapUrl(action.navigate) ?? null]),
80
+ }
81
+ }
62
82
 
63
83
  // This browser is no longer registered for anyone: nothing it showed stays, and it gets no more pushes.
64
84
  const forgetPush = async () => {
@@ -75,11 +95,12 @@ const showPush = async (data) => {
75
95
  const push = pushData(data)
76
96
  if (!push || text(push.title) === undefined) return self.registration.showNotification(text(record.appName) || self.location.host, {})
77
97
  if (push.user !== record.userId) return forgetPush()
78
- const url = tapUrl(push.url)
98
+ const { url, buttons, actions } = tapTargets(push)
79
99
  const options = {}
80
100
  if (text(push.body) !== undefined) options.body = push.body
81
101
  if (text(push.tag) !== undefined) options.tag = push.tag
82
- if (url !== undefined) options.data = { url }
102
+ if (buttons.length > 0) options.actions = buttons
103
+ if (url !== undefined || actions.length > 0) options.data = { ...(url !== undefined && { url }), ...(actions.length > 0 && { actions }) }
83
104
  if (typeof push.badge === 'number' && self.navigator.setAppBadge) await self.navigator.setAppBadge(push.badge)
84
105
  return self.registration.showNotification(push.title, options)
85
106
  }
@@ -98,15 +119,22 @@ const openInApp = async (url) => {
98
119
  }
99
120
  }
100
121
 
101
- const openTap = (notification) => {
122
+ // The url a tap opens, checked again: page code may show a notification with any data.
123
+ const tappedUrl = (data, action) => {
124
+ if (!plainObject(data)) return undefined
125
+ const pair = action && Array.isArray(data.actions) ? data.actions.find((entry) => Array.isArray(entry) && entry[0] === action) : undefined
126
+ return tapUrl(pair ? pair[1] : data.url)
127
+ }
128
+
129
+ const openTap = (notification, action) => {
102
130
  notification.close()
103
- const url = tapUrl(notification.data && notification.data.url)
131
+ const url = tappedUrl(notification.data, action)
104
132
  if (url === undefined) return Promise.resolve()
105
133
  return new URL(url).origin === self.location.origin ? openInApp(url) : self.clients.openWindow(url)
106
134
  }
107
135
 
108
136
  self.addEventListener('push', (event) => event.waitUntil(showPush(event.data)))
109
- self.addEventListener('notificationclick', (event) => event.waitUntil(openTap(event.notification)))
137
+ self.addEventListener('notificationclick', (event) => event.waitUntil(openTap(event.notification, event.action)))
110
138
  self.addEventListener('pushsubscriptionchange', (event) => {
111
139
  event.waitUntil(readPushRecord().then((record) => record && writePushRecord({ ...record, renew: true })))
112
140
  })
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.73.1";
2
+ var version = "0.73.2";
3
3
  //#endregion
4
4
  export { version };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@protobase/cli",
3
- "version": "0.73.1",
3
+ "version": "0.73.2",
4
4
  "description": "The protobase command: scaffold, doctor, dev, build and serve a Protobase project.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -43,13 +43,13 @@
43
43
  "dependencies": {
44
44
  "@better-auth/core": "^1.7.12",
45
45
  "@hono/node-server": "^2.1.3",
46
- "@protobase/config": "0.73.1",
47
- "@protobase/layout": "0.73.1",
48
- "@protobase/query": "0.73.1",
49
- "@protobase/schema": "0.73.1",
50
- "@protobase/server": "0.73.1",
51
- "@protobase/ui": "0.73.1",
52
- "@protobase/workflows": "0.73.1",
46
+ "@protobase/config": "0.73.2",
47
+ "@protobase/layout": "0.73.2",
48
+ "@protobase/query": "0.73.2",
49
+ "@protobase/schema": "0.73.2",
50
+ "@protobase/server": "0.73.2",
51
+ "@protobase/ui": "0.73.2",
52
+ "@protobase/workflows": "0.73.2",
53
53
  "@react-email/render": "^2.1.0",
54
54
  "@scalar/hono-api-reference": "^0.12.10",
55
55
  "@sentry/vite-plugin": "^11.10.0",
@@ -207,7 +207,7 @@ export const PushButton = () => {
207
207
  - Only someone signed in as themselves can turn push on: a visitor's click and staff signed in as the user do nothing.
208
208
  - On iPhone and iPad, push works in the app added to the Home Screen; a browser tab answers `unsupported`.
209
209
  - Signing out turns push off in this browser, and so does someone else signing in on it: what the browser showed is closed, the app's badge cleared, and the device removed. Signing in again as the same user keeps it on. Each push names the user its device is registered for, and the browser shows nothing of a push for another user than the one it holds push for.
210
- - A tap on a notification whose url is on your app's origin opens that page in the app, in an open window of it or a new one; any other url opens in a new tab.
210
+ - A tap on a notification opens its `platform.web.payload` `notification.navigate`, or its `url` when it has none, and each of the payload's `notification.actions` shows as a button that opens its own `navigate`, or the notification's url when it has none. The browser checks each url again as the server does and opens nothing for one that fails; no other field, such as `data`, is ever opened. A url on your app's origin opens that page in the app, in an open window of it or a new one; any other url opens in a new tab.
211
211
 
212
212
  `protobase serve` and `protobase dev` both serve the service worker that shows notifications, `protobase dev` only when the app has `notifications`.
213
213