@protobase/cli 0.73.0 → 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.
- package/dist/build/service-worker-push-code.js +36 -8
- package/dist/cli/package.js +1 -1
- package/dist/mobile/build.js +19 -10
- package/dist/mobile/capacitor-config.js +5 -3
- package/dist/mobile/ios-release.js +99 -0
- package/dist/mobile/run.js +2 -1
- package/package.json +8 -8
- package/skills/protobase/references/guides/mobile-app.md +3 -1
- package/skills/protobase/references/reference/api.md +1 -1
- package/skills/protobase/references/reference/cli.md +1 -1
- package/skills/protobase/references/reference/client.md +1 -1
- package/skills/protobase/references/reference/notifications.md +1 -1
|
@@ -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
|
-
* -
|
|
12
|
-
* a
|
|
13
|
-
*
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
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
|
})
|
package/dist/cli/package.js
CHANGED
package/dist/mobile/build.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { androidSigning } from "./android-signing.js";
|
|
2
2
|
import { runChild } from "./child-process.js";
|
|
3
|
+
import { buildIosAutomatic, iosApiKey } from "./ios-release.js";
|
|
3
4
|
import { copyRelease, releaseFile } from "./release-output.js";
|
|
4
5
|
import { prepareReleaseProject } from "./release-project.js";
|
|
5
6
|
import { prepare, summary } from "./run.js";
|
|
@@ -11,25 +12,24 @@ const gradleArgs = [
|
|
|
11
12
|
"--no-configuration-cache",
|
|
12
13
|
"-Pkotlin.compiler.execution.strategy=in-process"
|
|
13
14
|
];
|
|
14
|
-
const
|
|
15
|
+
const capBuildManual = (team, { certificate, profile }) => [
|
|
15
16
|
"build",
|
|
16
17
|
"ios",
|
|
17
18
|
"--xcode-team-id",
|
|
18
19
|
team,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
manual.profile
|
|
26
|
-
] : []
|
|
20
|
+
"--xcode-signing-style",
|
|
21
|
+
"manual",
|
|
22
|
+
"--xcode-signing-certificate",
|
|
23
|
+
certificate,
|
|
24
|
+
"--xcode-provisioning-profile",
|
|
25
|
+
profile
|
|
27
26
|
];
|
|
28
27
|
/** `protobase mobile build`: generates the native project as a release, builds it signed and copies the binary out. */
|
|
29
28
|
const mobileBuild = async (input, out, deps = {}) => {
|
|
30
29
|
const { version, outDir, ios, ...mobile } = input;
|
|
31
30
|
if (mobile.platform === "ios" && !ios) throw new Error("An iOS release is signed by your Apple Developer team: pass --team");
|
|
32
31
|
const signing = mobile.platform === "android" ? androidSigning(process.env, mobile.projectDir) : void 0;
|
|
32
|
+
const apiKey = ios && !ios.manual ? iosApiKey(process.env, mobile.projectDir) : void 0;
|
|
33
33
|
const prepared = await prepare({
|
|
34
34
|
...mobile,
|
|
35
35
|
release: true
|
|
@@ -45,7 +45,16 @@ const mobileBuild = async (input, out, deps = {}) => {
|
|
|
45
45
|
...signing.env
|
|
46
46
|
}
|
|
47
47
|
});
|
|
48
|
-
} else
|
|
48
|
+
} else {
|
|
49
|
+
const { team, manual } = ios;
|
|
50
|
+
if (manual) await prepared.cap(capBuildManual(team, manual));
|
|
51
|
+
else await buildIosAutomatic({
|
|
52
|
+
ws: prepared.ws,
|
|
53
|
+
env: prepared.env,
|
|
54
|
+
team,
|
|
55
|
+
key: apiKey
|
|
56
|
+
}, deps.run ?? runChild);
|
|
57
|
+
}
|
|
49
58
|
const file = await copyRelease(releaseFile(prepared.ws, mobile.platform), outDir, `${prepared.appId}-${version.version}-${version.build}`);
|
|
50
59
|
out(`release ${path.relative(mobile.projectDir, file)}\n`);
|
|
51
60
|
};
|
|
@@ -8,7 +8,8 @@ const liveUpdatePlugin = "@capawesome/capacitor-live-update";
|
|
|
8
8
|
const readyTimeout = 3e4;
|
|
9
9
|
/**
|
|
10
10
|
* The app's `capacitor.config.json`, every key it sets named here. It never sets `server.url`, the hostname or the
|
|
11
|
-
* schemes (the API's CORS list names Capacitor's default origins)
|
|
11
|
+
* schemes (the API's CORS list names Capacitor's default origins). WebView debugging is off in an iOS release by name:
|
|
12
|
+
* Capacitor's prebuilt simulator framework is a Debug build, so its default lists a Release app in Safari's Develop menu.
|
|
12
13
|
* `plugins` configures ProtobaseAuth, the keyboard plugin and, with the live updater, what its guard needs: the key
|
|
13
14
|
* releases are signed with, where they come from, the binary's fingerprint, shell version and the order of its own
|
|
14
15
|
* files, background updates off, a rolled-back bundle blocked and `readyTimeout`, within which the shell must call
|
|
@@ -19,7 +20,7 @@ const readyTimeout = 3e4;
|
|
|
19
20
|
* guards it. The file ships outside the app's web part, so the fingerprint in the user agent and ProtobaseAuth's
|
|
20
21
|
* origins are beyond page script and a live update.
|
|
21
22
|
*/
|
|
22
|
-
const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, liveUpdate, allowances, apiOrigin, platform }) => ({
|
|
23
|
+
const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, liveUpdate, allowances, apiOrigin, platform, release }) => ({
|
|
23
24
|
appId,
|
|
24
25
|
appName,
|
|
25
26
|
webDir: "web",
|
|
@@ -50,7 +51,8 @@ const capacitorConfig = ({ appId, appName, fingerprint, shellVersion, plugins, l
|
|
|
50
51
|
android: {
|
|
51
52
|
resolveServiceWorkerRequests: false,
|
|
52
53
|
...allowances.mixedContent && { allowMixedContent: true }
|
|
53
|
-
}
|
|
54
|
+
},
|
|
55
|
+
...release && platform === "ios" && { ios: { webContentsDebuggingEnabled: false } }
|
|
54
56
|
});
|
|
55
57
|
/** Writes the config into the workspace `dir`, removing the `.ts` and `.js` configs Capacitor would read instead. */
|
|
56
58
|
const writeCapacitorConfig = async (dir, config) => {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { rm, writeFile } from "node:fs/promises";
|
|
4
|
+
//#region src/mobile/ios-release.ts
|
|
5
|
+
const apiKeyVariables = [
|
|
6
|
+
"PROTOBASE_IOS_API_KEY_PATH",
|
|
7
|
+
"PROTOBASE_IOS_API_KEY_ID",
|
|
8
|
+
"PROTOBASE_IOS_API_KEY_ISSUER"
|
|
9
|
+
];
|
|
10
|
+
/**
|
|
11
|
+
* The API key in `env`, checked before anything is installed: none of its variables or all of them, the key a file,
|
|
12
|
+
* made absolute against the project folder since xcodebuild runs in the native project. Without it Xcode signs with
|
|
13
|
+
* the account in its settings.
|
|
14
|
+
*/
|
|
15
|
+
const iosApiKey = (env, projectDir) => {
|
|
16
|
+
const missing = apiKeyVariables.filter((key) => !env[key]);
|
|
17
|
+
if (missing.length === apiKeyVariables.length) return;
|
|
18
|
+
if (missing.length > 0) throw new Error(`Signing with an App Store Connect API key takes all of ${apiKeyVariables.join(", ")}: set ${missing.join(", ")}`);
|
|
19
|
+
const file = path.resolve(projectDir, env.PROTOBASE_IOS_API_KEY_PATH);
|
|
20
|
+
if (!statSync(file, { throwIfNoEntry: false })?.isFile()) throw new Error(`PROTOBASE_IOS_API_KEY_PATH names ${file}, which is not a file`);
|
|
21
|
+
return {
|
|
22
|
+
path: file,
|
|
23
|
+
id: env.PROTOBASE_IOS_API_KEY_ID,
|
|
24
|
+
issuer: env.PROTOBASE_IOS_API_KEY_ISSUER
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
const archive = "App.xcarchive";
|
|
28
|
+
const exportOptions = "archive.plist";
|
|
29
|
+
const provisioningArgs = (key) => ["-allowProvisioningUpdates", ...key ? [
|
|
30
|
+
"-authenticationKeyPath",
|
|
31
|
+
key.path,
|
|
32
|
+
"-authenticationKeyID",
|
|
33
|
+
key.id,
|
|
34
|
+
"-authenticationKeyIssuerID",
|
|
35
|
+
key.issuer
|
|
36
|
+
] : []];
|
|
37
|
+
/** xcodebuild's arguments to archive the release for `team`. */
|
|
38
|
+
const iosArchiveArgs = (team, key) => [
|
|
39
|
+
"-project",
|
|
40
|
+
"App.xcodeproj",
|
|
41
|
+
"-scheme",
|
|
42
|
+
"App",
|
|
43
|
+
"-configuration",
|
|
44
|
+
"Release",
|
|
45
|
+
"-destination",
|
|
46
|
+
"generic/platform=iOS",
|
|
47
|
+
"-archivePath",
|
|
48
|
+
archive,
|
|
49
|
+
"archive",
|
|
50
|
+
`DEVELOPMENT_TEAM=${team}`,
|
|
51
|
+
...provisioningArgs(key)
|
|
52
|
+
];
|
|
53
|
+
/** xcodebuild's arguments to export the archive as an .ipa into `output`, where `releaseFile` finds it. */
|
|
54
|
+
const iosExportArgs = (key) => [
|
|
55
|
+
"-exportArchive",
|
|
56
|
+
"-archivePath",
|
|
57
|
+
archive,
|
|
58
|
+
"-exportOptionsPlist",
|
|
59
|
+
exportOptions,
|
|
60
|
+
"-exportPath",
|
|
61
|
+
"output",
|
|
62
|
+
...provisioningArgs(key)
|
|
63
|
+
];
|
|
64
|
+
/** The export options of an App Store build `team` signs automatically; `team` is an id `--team` checked. */
|
|
65
|
+
const iosExportOptions = (team) => `<?xml version="1.0" encoding="UTF-8"?>
|
|
66
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
67
|
+
<plist version="1.0">
|
|
68
|
+
<dict>
|
|
69
|
+
<key>method</key>
|
|
70
|
+
<string>app-store-connect</string>
|
|
71
|
+
<key>signingStyle</key>
|
|
72
|
+
<string>automatic</string>
|
|
73
|
+
<key>teamID</key>
|
|
74
|
+
<string>${team}</string>
|
|
75
|
+
</dict>
|
|
76
|
+
</plist>
|
|
77
|
+
`;
|
|
78
|
+
/**
|
|
79
|
+
* Archives and exports the iOS project in `ws` with automatic signing. xcodebuild runs here rather than through
|
|
80
|
+
* `cap build ios`, which lets Xcode create nothing while archiving and cannot pass an API key to either step.
|
|
81
|
+
*/
|
|
82
|
+
const buildIosAutomatic = async ({ ws, env, team, key }, run) => {
|
|
83
|
+
const cwd = path.join(ws, "ios", "App");
|
|
84
|
+
await run("xcodebuild", iosArchiveArgs(team, key), {
|
|
85
|
+
cwd,
|
|
86
|
+
env
|
|
87
|
+
});
|
|
88
|
+
await writeFile(path.join(cwd, exportOptions), iosExportOptions(team));
|
|
89
|
+
await run("xcodebuild", iosExportArgs(key), {
|
|
90
|
+
cwd,
|
|
91
|
+
env
|
|
92
|
+
});
|
|
93
|
+
await rm(path.join(cwd, archive), {
|
|
94
|
+
recursive: true,
|
|
95
|
+
force: true
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { buildIosAutomatic, iosApiKey, iosArchiveArgs, iosExportArgs, iosExportOptions };
|
package/dist/mobile/run.js
CHANGED
|
@@ -71,7 +71,8 @@ const prepare = async ({ projectDir, platform, api, extensions, live, release },
|
|
|
71
71
|
plugins,
|
|
72
72
|
allowances,
|
|
73
73
|
apiOrigin: url.origin,
|
|
74
|
-
platform
|
|
74
|
+
platform,
|
|
75
|
+
...release && { release }
|
|
75
76
|
};
|
|
76
77
|
await writeCapacitorConfig(ws, capacitorConfig({
|
|
77
78
|
...config,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/cli",
|
|
3
|
-
"version": "0.73.
|
|
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.
|
|
47
|
-
"@protobase/layout": "0.73.
|
|
48
|
-
"@protobase/query": "0.73.
|
|
49
|
-
"@protobase/schema": "0.73.
|
|
50
|
-
"@protobase/server": "0.73.
|
|
51
|
-
"@protobase/ui": "0.73.
|
|
52
|
-
"@protobase/workflows": "0.73.
|
|
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",
|
|
@@ -118,7 +118,9 @@ Use it as the upload key with Play App Signing. Set these variables in your shel
|
|
|
118
118
|
|
|
119
119
|
Only Gradle gets them, for the length of the build: npm, its install scripts and Capacitor never do. The build needs the Android SDK and JDK 21.
|
|
120
120
|
|
|
121
|
-
**iOS**, on a Mac with Xcode: `--team` is your Apple Developer team id, shown in your account's membership details. Xcode signs with the certificates and profiles of the account you added in its settings. For manual signing, pass `--certificate <name>` and `--profile <name>` too. <gap>Manual signing cannot use a certificate or profile name that holds `<`, `>` or `&` because writing the escaped export options is not implemented.</gap>
|
|
121
|
+
**iOS**, on a Mac with Xcode: `--team` is your Apple Developer team id, shown in your account's membership details. Xcode signs with the certificates and profiles of the account you added in its settings. For manual signing, pass `--certificate <name>` and `--profile <name>` too. <gap>Manual signing cannot use a certificate or profile name that holds `<`, `>` or `&` because writing the escaped export options is not implemented.</gap> Xcode creates the certificates and profiles a new app lacks.
|
|
122
|
+
|
|
123
|
+
To sign without an account in Xcode, as on a build machine, make an App Store Connect API key with the Admin role and set `PROTOBASE_IOS_API_KEY_PATH` to its `.p8` file, absolute or relative to the project folder, `PROTOBASE_IOS_API_KEY_ID` to its key id and `PROTOBASE_IOS_API_KEY_ISSUER` to its issuer id. Set all three or none; manual signing ignores them. The bundle id must exist on the team first.
|
|
122
124
|
|
|
123
125
|
## 7. Live updates
|
|
124
126
|
|
|
@@ -143,7 +143,7 @@ The queue is an `AuditQueue`, `{ publish(event): Promise<void> }`, the default e
|
|
|
143
143
|
|
|
144
144
|
## Filters
|
|
145
145
|
|
|
146
|
-
`filter` and `order_by` are AIP-160 and AIP-132 text, checked against the model (filterable and sortable fields only). Functions: `in(field, v, ...)`, `search("text")` and bare words (every word, ignoring case, anywhere inside one of the resource's search fields, so `port` finds "Portfolio"; `%`, `_` and `\` are literal; a [`digitsEnd`](data-config.md#resource) field matches only the end of its digits), `similar(field, "text")`, `regex(field, "pattern")`, `isNull(field)`, `now()` with offsets such as `now() - 7d`. A filter is capped at 10,000 characters in a query string and 200,000 in a `:search` body.
|
|
146
|
+
`filter` and `order_by` are AIP-160 and AIP-132 text, checked against the model (filterable and sortable fields only). Functions: `in(field, v, ...)`, `search("text")` and bare words (every word, ignoring case, anywhere inside one of the resource's search fields, so `port` finds "Portfolio"; `%`, `_` and `\` are literal; a [`digitsEnd`](data-config.md#resource) field matches only the end of its digits), `similar(field, "text")`, `regex(field, "pattern")`, `isNull(field)`, `now()` with offsets such as `now() - 7d`. A filter is capped at 10,000 characters in a query string and 200,000 in a `:search` body. A proxy in front of the API can refuse a long URL sooner with a `431`, counting the headers too: Protobase Cloud refused about 8,000 characters. So send a long filter to `:search`, as `client.list` does when its query string passes 4,000 characters.
|
|
147
147
|
|
|
148
148
|
## Writes
|
|
149
149
|
|
|
@@ -650,7 +650,7 @@ Builds the project in the current directory as an iOS or Android app with Capaci
|
|
|
650
650
|
- It installs `@protobase/mobile`, of the CLI's version, which pins Capacitor and its plugins, with the section's `plugins` into `.protobase/mobile/` with npm, when that changed, and on every run for a package from a local folder, given as `file:` or a path such as `../my-plugin`, which npm copies. When such a folder's dependencies are not the versions installed, it removes the workspace's `node_modules` and `package-lock.json` so npm resolves them again, and stops when a pin is still not met, as when two packages pin one dependency at different versions. It builds the app's files as [`build`](#build) does and keeps only `public/`, its `index.html` naming the API origin; writes `capacitor.config.json`, compiling in only the section's plugins; and generates the platform's native project again, replacing the last one.
|
|
651
651
|
- `--api` is the server the app calls, the app's `url` by default: an http or https origin, plain http only to `localhost`, `127.0.0.1` and, on iOS, `[::1]`, or on Android `10.0.2.2`. An Android app calling `localhost` gets the port forwarded.
|
|
652
652
|
- `run` runs the app on `--target`, or asks for one. On iOS, `--team` writes the Apple team id into the generated project and builds it once with xcodebuild allowed to create the development profile and register the device, `--target` or the one connected device, before Capacitor runs it there. With `--live` it opens `protobase dev` on `localhost:--port` instead of its own files. `open` opens the native project in Xcode or Android Studio.
|
|
653
|
-
- `build` makes a signed release, an `.aab` (Android) or `.ipa` (iOS), and copies it to `--out`, which ignores itself in git when the command creates it, as `<appId>-<version>-<build number>`. Android signs through Gradle with the keystore, alias and passwords in `PROTOBASE_ANDROID_KEYSTORE`, `PROTOBASE_ANDROID_KEY_ALIAS`, `PROTOBASE_ANDROID_KEYSTORE_PASSWORD` and `PROTOBASE_ANDROID_KEY_PASSWORD`; iOS
|
|
653
|
+
- `build` makes a signed release, an `.aab` (Android) or `.ipa` (iOS), and copies it to `--out`, which ignores itself in git when the command creates it, as `<appId>-<version>-<build number>`. Android signs through Gradle with the keystore, alias and passwords in `PROTOBASE_ANDROID_KEYSTORE`, `PROTOBASE_ANDROID_KEY_ALIAS`, `PROTOBASE_ANDROID_KEYSTORE_PASSWORD` and `PROTOBASE_ANDROID_KEY_PASSWORD`; iOS archives and exports with xcodebuild for `--team`, letting Xcode create certificates and profiles with the account in its settings, or with the App Store Connect API key in `PROTOBASE_IOS_API_KEY_PATH`, `PROTOBASE_IOS_API_KEY_ID` and `PROTOBASE_IOS_API_KEY_ISSUER`; `--certificate` with `--profile` signs manually through Capacitor's build.
|
|
654
654
|
- A release calls an https server that is not this machine, and `--app-version` (one to three numbers) and `--build-number` (1 to 2100000000) are required.
|
|
655
655
|
- npm and Capacitor get the CLI's environment without any variable named in the current directory's `.env*` files, nor the four `PROTOBASE_ANDROID_*` signing variables, which only Gradle gets, and with `CI=true`, which keeps Capacitor's telemetry off. The project's config, its extensions' configs and the web build run inside the CLI with no `PROTOBASE_ANDROID_*` variable left in its environment, even one the project's `.env` sets.
|
|
656
656
|
- It prints the workspace, the API origin and the native fingerprint, which the app also sends in its user agent as `ProtobaseNative/<fingerprint>`: a hash of the native packages `@protobase/mobile` pins, at their versions, its own native contract, and the section's `plugins`, at the versions they give. A [mobile release](#mobile-releases) runs only on a binary of its own fingerprint. A Protobase release that pins the same native packages and keeps the same native contract keeps it; raise a plugin's version when the app's files start using a newer native feature of it, which changes it.
|
|
@@ -17,7 +17,7 @@ const { record, etag } = await api.get(orders, id)
|
|
|
17
17
|
await api.update(orders, id, { status: 'shipped' }, etag) // 412 rejects with PreconditionFailedError
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/))
|
|
20
|
+
Pass a resource builder to get its record type ([`InferRecord`](https://docs.protobase.net/api/schema/type-aliases/inferrecord/)), or a name for loose records. Filters are AIP-160 text or [`where`](https://docs.protobase.net/api/schema/variables/where/) results (printed with [`printFilter`](https://docs.protobase.net/api/schema/functions/printfilter/)), and a list whose query string passes 4,000 characters goes by `:search`. Errors are `ApiError` (`status`, `slug`, `errors`, `filterErrors` with spans, `blockedBy` for a delete other records point at), which `isApiError(error)` tells apart. Also: `search`, `create`, `remove`, `facets`, `series`, `histogram`, `map(resource, { latitude, longitude, zoom, filter?, orderBy?, fields? })` (the records in a map's view, or buckets of them, see [`:map`](api.md#maps)), `seek`, `reveal(resource, key, field)` (a [sensitive field](data-config.md#sensitive-fields)'s value), `generate(resource, key, action)` (runs an [AI action](ai.md#ai-actions) and resolves to the record as written), `meta(etag?, { signal? })`, `invoke(name, { body?, method?, path?, query? })` (one of the app's [API functions](../guides/api-functions.md#calling-a-function); its answer as parsed JSON, text, or a `Blob` for any other type), `history(resource, key, { pageSize?, pageToken? })` (a page of a record's [changes](history.md), newest first), `comments(resource, key, { pageSize?, pageToken? })`, `comment(resource, key, { body, mentions? })` and `mentionable(resource, key, query)` (a record's [comments](comments.md#the-endpoints), adding one, and the people it may mention), `form(name)`, `formChallenge(name)` and `submitForm(name, values, proof)` (a [public form](forms.md#routes)), `upload(resource, field, file, { onProgress?, signal? })` (a file for a [file field](files.md#uploading); its `value` goes in the next write, and `onProgress` uses XMLHttpRequest, since fetch reports no upload progress), `live({ onChanges, onResync, onRetry? })` (hears which records of the [live](live.md) resources change until the function it returns is called, connecting again by itself; every caller of one client shares one stream), `channel(resource, key)` (a record's [channel](channels.md#from-a-page), with its presence and broadcasts, every channel of the client on one socket).
|
|
21
21
|
|
|
22
22
|
With an absolute `baseUrl`, such as another app's `https://desk.example.com/api/v1`, the `url` of each [file](files.md) in a record the client answers is absolute too, on that API's origin, where the API answers a path on its own origin. `routedClient(own, sourceOf)` is one client over several APIs: each call about a resource goes to the client `sourceOf(resource)` gives, else to `own`, as the admin app does for [remote resources](remote-resources.md), and `sourceMeta(meta, source)` keeps the part of another API's `/meta` an app draws.
|
|
23
23
|
|
|
@@ -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
|
|
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
|
|