@softr/sdk 0.0.48-alpha.0 → 0.0.49-alpha.0
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/auth/isomorphic.d.ts +1 -1
- package/dist/auth/isomorphic.js +1 -2
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/default-error-overlay.js +73 -1
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/mcp.d.ts +2 -2
- package/dist/package.js +1 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +2 -6
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +1 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +2 -2
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +1 -1
- package/dist/pwa.d.ts +0 -49
- package/dist/pwa.js +0 -147
- package/skills/softr-sdk/pwa/SKILL.md +0 -163
|
@@ -17,7 +17,7 @@ import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
|
17
17
|
* requestMiddleware: [softrAuthMiddleware()],
|
|
18
18
|
* }))
|
|
19
19
|
*/
|
|
20
|
-
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined
|
|
20
|
+
declare function softrAuthMiddleware(): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, readonly [_tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>], {
|
|
21
21
|
user: User | null;
|
|
22
22
|
permissions: Permissions;
|
|
23
23
|
}>;
|
package/dist/auth/isomorphic.js
CHANGED
|
@@ -4,7 +4,6 @@ import { sessionConfig } from "./config.js";
|
|
|
4
4
|
import { clearSession, makeSessionDataCookie, readSessionData, readSessionToken } from "./session.js";
|
|
5
5
|
import { makePlatform } from "./platform.js";
|
|
6
6
|
import { bootErrorMiddleware } from "../boot-error.js";
|
|
7
|
-
import { pwaMiddleware } from "../pwa.js";
|
|
8
7
|
import { createMiddleware, createServerFn, getGlobalStartContext } from "@tanstack/react-start";
|
|
9
8
|
import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
10
9
|
|
|
@@ -22,7 +21,7 @@ import { rootRouteId, useRouteContext } from "@tanstack/react-router";
|
|
|
22
21
|
* }))
|
|
23
22
|
*/
|
|
24
23
|
function softrAuthMiddleware() {
|
|
25
|
-
return createMiddleware({ type: "request" }).middleware([bootErrorMiddleware()
|
|
24
|
+
return createMiddleware({ type: "request" }).middleware([bootErrorMiddleware()]).server(async ({ next }) => {
|
|
26
25
|
async function getSession() {
|
|
27
26
|
const token = readSessionToken();
|
|
28
27
|
const cached = readSessionData(token);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react0 from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/default-error-overlay.d.ts
|
|
4
4
|
type Props = {
|
|
@@ -8,6 +8,6 @@ type Props = {
|
|
|
8
8
|
declare function DefaultErrorOverlay({
|
|
9
9
|
error,
|
|
10
10
|
reset
|
|
11
|
-
}: Props):
|
|
11
|
+
}: Props): react0.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DefaultErrorOverlay };
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { useRouter } from "@tanstack/react-router";
|
|
2
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
4
|
|
|
4
5
|
//#region src/default-error-overlay.tsx
|
|
6
|
+
function subscribeToAgentTurn(listener) {
|
|
7
|
+
const observer = new MutationObserver(listener);
|
|
8
|
+
observer.observe(document.documentElement, {
|
|
9
|
+
attributes: true,
|
|
10
|
+
attributeFilter: ["data-agent-turn"]
|
|
11
|
+
});
|
|
12
|
+
return () => observer.disconnect();
|
|
13
|
+
}
|
|
14
|
+
function isAgentTurnRunning() {
|
|
15
|
+
return document.documentElement.dataset.agentTurn === "running";
|
|
16
|
+
}
|
|
5
17
|
function DefaultErrorOverlay({ error, reset }) {
|
|
6
18
|
if (process.env.NODE_ENV === "production") return /* @__PURE__ */ jsx("div", {
|
|
7
19
|
style: s.overlay,
|
|
@@ -26,6 +38,23 @@ function DefaultErrorOverlay({ error, reset }) {
|
|
|
26
38
|
});
|
|
27
39
|
}
|
|
28
40
|
function DevView({ error, reset }) {
|
|
41
|
+
const running = useSyncExternalStore(subscribeToAgentTurn, isAgentTurnRunning, () => false);
|
|
42
|
+
const caughtDuringTurn = useRef(running);
|
|
43
|
+
const router = useRouter();
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (running) {
|
|
46
|
+
caughtDuringTurn.current = true;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!caughtDuringTurn.current) return;
|
|
50
|
+
const timer = setTimeout(() => void router.invalidate().then(() => reset?.()), 1e3);
|
|
51
|
+
return () => clearTimeout(timer);
|
|
52
|
+
}, [
|
|
53
|
+
running,
|
|
54
|
+
reset,
|
|
55
|
+
router
|
|
56
|
+
]);
|
|
57
|
+
if (running) return /* @__PURE__ */ jsx(UpdatingView, {});
|
|
29
58
|
return /* @__PURE__ */ jsx("div", {
|
|
30
59
|
style: s.overlay,
|
|
31
60
|
children: /* @__PURE__ */ jsxs("div", {
|
|
@@ -56,6 +85,38 @@ function DevView({ error, reset }) {
|
|
|
56
85
|
})
|
|
57
86
|
});
|
|
58
87
|
}
|
|
88
|
+
function UpdatingView() {
|
|
89
|
+
return /* @__PURE__ */ jsx("div", {
|
|
90
|
+
style: s.overlay,
|
|
91
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
92
|
+
style: s.card,
|
|
93
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
94
|
+
style: s.pending,
|
|
95
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
96
|
+
style: icon.md,
|
|
97
|
+
viewBox: "0 0 24 24",
|
|
98
|
+
fill: "none",
|
|
99
|
+
stroke: "currentColor",
|
|
100
|
+
strokeWidth: "2",
|
|
101
|
+
strokeLinecap: "round",
|
|
102
|
+
strokeLinejoin: "round",
|
|
103
|
+
children: [
|
|
104
|
+
/* @__PURE__ */ jsx("path", { d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }),
|
|
105
|
+
/* @__PURE__ */ jsx("path", { d: "M21 3v5h-5" }),
|
|
106
|
+
/* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }),
|
|
107
|
+
/* @__PURE__ */ jsx("path", { d: "M8 16H3v5" })
|
|
108
|
+
]
|
|
109
|
+
})
|
|
110
|
+
}), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h1", {
|
|
111
|
+
style: s.heading,
|
|
112
|
+
children: "Updating the app…"
|
|
113
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
114
|
+
style: s.body,
|
|
115
|
+
children: "The agent is still working on this change. The preview will recover once it is complete."
|
|
116
|
+
})] })]
|
|
117
|
+
})
|
|
118
|
+
});
|
|
119
|
+
}
|
|
59
120
|
function cleanStack(stack) {
|
|
60
121
|
return stack.split("\n").slice(1).filter((line) => line.trim().startsWith("at ") && !line.includes("/node_modules/")).map((line) => line.replace(/https?:\/\/[^/]+\//, "").replace(/\?[^):]+/g, "")).join("\n");
|
|
61
122
|
}
|
|
@@ -228,6 +289,17 @@ const s = {
|
|
|
228
289
|
color: "var(--destructive, oklch(0.577 0.245 27.325))",
|
|
229
290
|
flexShrink: 0
|
|
230
291
|
},
|
|
292
|
+
pending: {
|
|
293
|
+
display: "flex",
|
|
294
|
+
alignItems: "center",
|
|
295
|
+
justifyContent: "center",
|
|
296
|
+
width: "2.5rem",
|
|
297
|
+
height: "2.5rem",
|
|
298
|
+
borderRadius: "var(--radius, 0.625rem)",
|
|
299
|
+
background: "var(--muted, oklch(0.97 0 0))",
|
|
300
|
+
color: "var(--muted-foreground, oklch(0.556 0 0))",
|
|
301
|
+
flexShrink: 0
|
|
302
|
+
},
|
|
231
303
|
heading: {
|
|
232
304
|
margin: 0,
|
|
233
305
|
fontSize: "1rem",
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedErr
|
|
|
8
8
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
|
|
9
9
|
import { softrMcpMiddleware } from "./mcp.js";
|
|
10
10
|
import { useUserTimezone } from "./timezone.js";
|
|
11
|
-
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
12
11
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
13
12
|
import { Audience } from "./visibility/audience.js";
|
|
14
|
-
export { type ActionPermissions, Audience, DefaultErrorOverlay, type FileAttachment, type FileItem, type
|
|
13
|
+
export { type ActionPermissions, Audience, DefaultErrorOverlay, type FileAttachment, type FileItem, type SoftrContext, type SoftrDatasources, type SoftrUserGroupId, type SoftrUserGroups, type User, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,10 @@ import { useFileUpload } from "./core/use-file-upload.js";
|
|
|
2
2
|
import { defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./visibility/visibility.js";
|
|
4
4
|
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./visibility/route.js";
|
|
5
|
-
import { SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
6
5
|
import { getSoftrContext, initialSoftrContext, softrAuthMiddleware, useCurrentUser } from "./auth/isomorphic.js";
|
|
7
6
|
import { softrMcpMiddleware } from "./mcp.js";
|
|
8
7
|
import { useUserTimezone } from "./timezone.js";
|
|
9
8
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
10
9
|
import { Audience } from "./visibility/audience.js";
|
|
11
10
|
|
|
12
|
-
export { Audience, DefaultErrorOverlay,
|
|
11
|
+
export { Audience, DefaultErrorOverlay, assertRouteAccess, checkAccess, defineIntegration, defineMutation, defineQuery, defineVisibility, defineWorkflow, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, softrAuthMiddleware, softrMcpMiddleware, useCurrentUser, useFileUpload, useUserTimezone };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DatasourceAction } from "./action.js";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _tanstack_react_start0 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
@@ -37,6 +37,6 @@ type SoftrMcpOptions = {
|
|
|
37
37
|
* ],
|
|
38
38
|
* }))
|
|
39
39
|
*/
|
|
40
|
-
declare function softrMcpMiddleware(options?: SoftrMcpOptions):
|
|
40
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start0.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
41
41
|
//#endregion
|
|
42
42
|
export { softrMcpMiddleware };
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: core
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.49-alpha.0
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
# Softr SDK core
|
|
@@ -29,7 +29,7 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
29
29
|
|
|
30
30
|
| Import | Runs | Holds |
|
|
31
31
|
| --- | --- | --- |
|
|
32
|
-
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `
|
|
32
|
+
| `@softr/sdk` | Both | `defineQuery`, `defineMutation`, `defineVisibility`, `guardRoute`, `assertRouteAccess`, `isAuthorized`, `checkAccess`, `Audience`, `useCurrentUser`, `useUserTimezone`, `getSoftrContext`, `initialSoftrContext`, `softrAuthMiddleware`, the error predicates, and every type |
|
|
33
33
|
| `@softr/sdk/server` | Server only | `ds`, `auth`, `workflow`, `integration` |
|
|
34
34
|
| `@softr/sdk/client` | Browser only | Nothing yet, reserved for browser-only utilities |
|
|
35
35
|
| `@softr/sdk/vite` | Build | `softr()`, already in `vite.config.ts` |
|
|
@@ -48,7 +48,6 @@ Each sub-skill stands alone for its task and repeats the boundary above, so most
|
|
|
48
48
|
| Sign a visitor in, out, or up | [auth/SKILL.md](./auth/SKILL.md) |
|
|
49
49
|
| Call a connected third-party API (Slack, Stripe, Gmail, …) | [integrations/SKILL.md](./integrations/SKILL.md) |
|
|
50
50
|
| Open the app to AI assistants over MCP | [mcp/SKILL.md](./mcp/SKILL.md) |
|
|
51
|
-
| Let visitors install the app on their home screen | [pwa/SKILL.md](./pwa/SKILL.md) |
|
|
52
51
|
|
|
53
52
|
## Quick Decision Tree
|
|
54
53
|
|
|
@@ -83,9 +82,6 @@ Calling a connected third-party API (Slack, Stripe, Gmail, Google Calendar, …)
|
|
|
83
82
|
Opening the app to an AI assistant over MCP?
|
|
84
83
|
load softr-sdk/mcp
|
|
85
84
|
|
|
86
|
-
Adding an install button, or making the app installable?
|
|
87
|
-
load softr-sdk/pwa
|
|
88
|
-
|
|
89
85
|
Writing a server function, loader, or middleware in general?
|
|
90
86
|
not a Softr concern. Use the TanStack Start skills.
|
|
91
87
|
```
|
|
@@ -16,7 +16,7 @@ description: >-
|
|
|
16
16
|
metadata:
|
|
17
17
|
type: sub-skill
|
|
18
18
|
library: softr-sdk
|
|
19
|
-
library_version: 0.0.
|
|
19
|
+
library_version: 0.0.49-alpha.0
|
|
20
20
|
---
|
|
21
21
|
|
|
22
22
|
**Boundary.** `auth` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build.
|
|
@@ -12,7 +12,7 @@ description: >-
|
|
|
12
12
|
metadata:
|
|
13
13
|
type: sub-skill
|
|
14
14
|
library: softr-sdk
|
|
15
|
-
library_version: 0.0.
|
|
15
|
+
library_version: 0.0.49-alpha.0
|
|
16
16
|
---
|
|
17
17
|
|
|
18
18
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.49-alpha.0
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `upload` lives on `@softr/sdk/server` and runs inside server functions. `useFileUpload` and `FileItem` live on `@softr/sdk` and run in components. `FileAttachment` comes from `@softr/sdk` and can be imported anywhere.
|
|
@@ -10,7 +10,7 @@ description: >-
|
|
|
10
10
|
metadata:
|
|
11
11
|
type: sub-skill
|
|
12
12
|
library: softr-sdk
|
|
13
|
-
library_version: 0.0.
|
|
13
|
+
library_version: 0.0.49-alpha.0
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
**Boundary.** `integration` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineIntegration` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.49-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `softrMcpMiddleware` lives on `@softr/sdk` and is registered once, in `src/start.ts`. The actions you hand it are the same `defineQuery` / `defineMutation` values the app's own pages use — declared wherever they already live, not duplicated for MCP.
|
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
metadata:
|
|
12
12
|
type: sub-skill
|
|
13
13
|
library: softr-sdk
|
|
14
|
-
library_version: 0.0.
|
|
14
|
+
library_version: 0.0.49-alpha.0
|
|
15
15
|
---
|
|
16
16
|
|
|
17
17
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineMutation` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.49-alpha.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `ds` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler or a server-side route hook — importing it into a component fails the build. `defineQuery` and every type come from `@softr/sdk` and can be imported anywhere.
|
|
@@ -183,7 +183,7 @@ A read the datasource turns down throws rather than coming back empty, and it th
|
|
|
183
183
|
|
|
184
184
|
## Typing
|
|
185
185
|
|
|
186
|
-
Queries you write are type safe and a type error is shown when there's a mistake. For an in depth reference on what fields every datasource has
|
|
186
|
+
Queries you write are type safe and a type error is shown when there's a mistake. For an in depth reference on what fields every datasource has, read `docs/datasources/README.md` first — it lists every registered datasource with a link to its file. Follow those links to get field ids, types, and constraints. Do not guess filenames from table names.
|
|
187
187
|
|
|
188
188
|
## References
|
|
189
189
|
|
|
@@ -13,7 +13,7 @@ description: >-
|
|
|
13
13
|
metadata:
|
|
14
14
|
type: sub-skill
|
|
15
15
|
library: softr-sdk
|
|
16
|
-
library_version: 0.0.
|
|
16
|
+
library_version: 0.0.49-alpha.0
|
|
17
17
|
---
|
|
18
18
|
|
|
19
19
|
**Boundary.** `defineVisibility`, the access functions and the error predicates come from `@softr/sdk` and work anywhere.
|
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
metadata:
|
|
10
10
|
type: sub-skill
|
|
11
11
|
library: softr-sdk
|
|
12
|
-
library_version: 0.0.
|
|
12
|
+
library_version: 0.0.49-alpha.0
|
|
13
13
|
---
|
|
14
14
|
|
|
15
15
|
**Boundary.** `workflow` lives on `@softr/sdk/server` and runs only inside a `createServerFn` handler — importing it into a component fails the build. `defineWorkflow` and every type come from `@softr/sdk` and can be imported anywhere.
|
package/dist/pwa.d.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import * as react0 from "react";
|
|
2
|
-
import "@tanstack/react-start";
|
|
3
|
-
|
|
4
|
-
//#region src/pwa.d.ts
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The links that make the app installable: its manifest and its home screen
|
|
8
|
-
* icon. Render it once, anywhere in the root route; React moves the links into
|
|
9
|
-
* `<head>`.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* function RootComponent() {
|
|
13
|
-
* return (
|
|
14
|
-
* <>
|
|
15
|
-
* <SoftrPwaHead />
|
|
16
|
-
* <Outlet />
|
|
17
|
-
* </>
|
|
18
|
-
* )
|
|
19
|
-
* }
|
|
20
|
-
*/
|
|
21
|
-
declare function SoftrPwaHead(): react0.FunctionComponentElement<react0.FragmentProps>;
|
|
22
|
-
type PwaPlatform = "ios" | "android" | "desktop";
|
|
23
|
-
type PwaInstall = {
|
|
24
|
-
available: false;
|
|
25
|
-
} | {
|
|
26
|
-
available: true;
|
|
27
|
-
/** Which steps install the app on this device. */
|
|
28
|
-
platform: PwaPlatform;
|
|
29
|
-
/** The name the home screen will show. */
|
|
30
|
-
name: string;
|
|
31
|
-
/** The icon the home screen will show. */
|
|
32
|
-
iconUrl: string;
|
|
33
|
-
};
|
|
34
|
-
/**
|
|
35
|
-
* Whether to offer installing the app, and on which platform. Render an
|
|
36
|
-
* install button only while this says `available`: it is unavailable while
|
|
37
|
-
* loading, when the builder has not turned the PWA on, when Softr has switched
|
|
38
|
-
* it off, and when the app already runs installed.
|
|
39
|
-
*
|
|
40
|
-
* @example
|
|
41
|
-
* function InstallButton() {
|
|
42
|
-
* const install = usePwaInstall()
|
|
43
|
-
* if (!install.available) return null
|
|
44
|
-
* return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
|
|
45
|
-
* }
|
|
46
|
-
*/
|
|
47
|
-
declare function usePwaInstall(): PwaInstall;
|
|
48
|
-
//#endregion
|
|
49
|
-
export { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall };
|
package/dist/pwa.js
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { logger } from "./logger.js";
|
|
2
|
-
import { fetch } from "./fetch.js";
|
|
3
|
-
import { sessionConfig } from "./auth/config.js";
|
|
4
|
-
import { platformApiUrl } from "./auth/platform.js";
|
|
5
|
-
import { Fragment, createElement, useEffect, useState } from "react";
|
|
6
|
-
import { createMiddleware } from "@tanstack/react-start";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
|
|
9
|
-
//#region src/pwa.ts
|
|
10
|
-
const MANIFEST_PATH = "/manifest.webmanifest";
|
|
11
|
-
const ICON_PATH = "/apple-touch-icon.png";
|
|
12
|
-
const INSTALL_PATH = "/_softr/pwa.json";
|
|
13
|
-
const TTL_MS = 6e4;
|
|
14
|
-
const CACHE_CONTROL = "public, max-age=60";
|
|
15
|
-
const pwaSchema = z.object({
|
|
16
|
-
manifest: z.record(z.string(), z.unknown()),
|
|
17
|
-
install: z.object({
|
|
18
|
-
name: z.string(),
|
|
19
|
-
iconUrl: z.string()
|
|
20
|
-
})
|
|
21
|
-
});
|
|
22
|
-
let cached;
|
|
23
|
-
/**
|
|
24
|
-
* The app's PWA, or `null` when it is not installable right now. Never
|
|
25
|
-
* rejects: a failure reads as not installable and is not remembered, so the
|
|
26
|
-
* next request tries again.
|
|
27
|
-
*/
|
|
28
|
-
function readPwa() {
|
|
29
|
-
const now = Date.now();
|
|
30
|
-
if (cached !== void 0 && cached.expires > now) return cached.pwa;
|
|
31
|
-
const entry = {
|
|
32
|
-
expires: now + TTL_MS,
|
|
33
|
-
pwa: fetchPwa().catch((error) => {
|
|
34
|
-
logger.warn("Could not read the PWA settings:", error);
|
|
35
|
-
if (cached === entry) cached = void 0;
|
|
36
|
-
return null;
|
|
37
|
-
})
|
|
38
|
-
};
|
|
39
|
-
cached = entry;
|
|
40
|
-
return entry.pwa;
|
|
41
|
-
}
|
|
42
|
-
async function fetchPwa() {
|
|
43
|
-
const { applicationId } = sessionConfig();
|
|
44
|
-
const response = await fetch(`${await platformApiUrl()}/v1/vibe/applications/${applicationId}/pwa`, { headers: { accept: "application/json" } });
|
|
45
|
-
if (response.status === 404) return null;
|
|
46
|
-
if (!response.ok) throw new Error(`The platform answered ${response.status}`);
|
|
47
|
-
return pwaSchema.parse(await response.json());
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Serves the app's web app manifest, its home screen icon, and what an install
|
|
51
|
-
* button needs to know, from the app's own origin. Every other request passes
|
|
52
|
-
* through untouched.
|
|
53
|
-
*
|
|
54
|
-
* Answers 404, never an error page, when the app is not installable: a browser
|
|
55
|
-
* silently drops a manifest it cannot parse, so a 500 there would be invisible.
|
|
56
|
-
*/
|
|
57
|
-
function pwaMiddleware() {
|
|
58
|
-
return createMiddleware({ type: "request" }).server(async ({ next, pathname }) => {
|
|
59
|
-
if (pathname !== MANIFEST_PATH && pathname !== ICON_PATH && pathname !== INSTALL_PATH) return next();
|
|
60
|
-
const pwa = await readPwa();
|
|
61
|
-
if (pwa === null) return new Response(null, { status: 404 });
|
|
62
|
-
if (pathname === MANIFEST_PATH) return new Response(JSON.stringify(pwa.manifest), { headers: {
|
|
63
|
-
"content-type": "application/manifest+json",
|
|
64
|
-
"cache-control": CACHE_CONTROL
|
|
65
|
-
} });
|
|
66
|
-
if (pathname === INSTALL_PATH) return Response.json(pwa.install, { headers: { "cache-control": CACHE_CONTROL } });
|
|
67
|
-
try {
|
|
68
|
-
const iconResponse = await fetch(pwa.install.iconUrl);
|
|
69
|
-
if (!iconResponse.ok) return new Response(null, { status: 404 });
|
|
70
|
-
return new Response(iconResponse.body, { headers: {
|
|
71
|
-
"content-type": "image/png",
|
|
72
|
-
"cache-control": CACHE_CONTROL
|
|
73
|
-
} });
|
|
74
|
-
} catch (error) {
|
|
75
|
-
logger.warn("Could not fetch the PWA icon:", error);
|
|
76
|
-
return new Response(null, { status: 404 });
|
|
77
|
-
}
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* The links that make the app installable: its manifest and its home screen
|
|
82
|
-
* icon. Render it once, anywhere in the root route; React moves the links into
|
|
83
|
-
* `<head>`.
|
|
84
|
-
*
|
|
85
|
-
* @example
|
|
86
|
-
* function RootComponent() {
|
|
87
|
-
* return (
|
|
88
|
-
* <>
|
|
89
|
-
* <SoftrPwaHead />
|
|
90
|
-
* <Outlet />
|
|
91
|
-
* </>
|
|
92
|
-
* )
|
|
93
|
-
* }
|
|
94
|
-
*/
|
|
95
|
-
function SoftrPwaHead() {
|
|
96
|
-
return createElement(Fragment, null, createElement("link", {
|
|
97
|
-
rel: "manifest",
|
|
98
|
-
href: MANIFEST_PATH
|
|
99
|
-
}), createElement("link", {
|
|
100
|
-
rel: "apple-touch-icon",
|
|
101
|
-
href: ICON_PATH
|
|
102
|
-
}));
|
|
103
|
-
}
|
|
104
|
-
const installSchema = pwaSchema.shape.install;
|
|
105
|
-
/**
|
|
106
|
-
* Whether to offer installing the app, and on which platform. Render an
|
|
107
|
-
* install button only while this says `available`: it is unavailable while
|
|
108
|
-
* loading, when the builder has not turned the PWA on, when Softr has switched
|
|
109
|
-
* it off, and when the app already runs installed.
|
|
110
|
-
*
|
|
111
|
-
* @example
|
|
112
|
-
* function InstallButton() {
|
|
113
|
-
* const install = usePwaInstall()
|
|
114
|
-
* if (!install.available) return null
|
|
115
|
-
* return <button onClick={() => openSteps(install.platform)}>Install {install.name}</button>
|
|
116
|
-
* }
|
|
117
|
-
*/
|
|
118
|
-
function usePwaInstall() {
|
|
119
|
-
const [install, setInstall] = useState({ available: false });
|
|
120
|
-
useEffect(() => {
|
|
121
|
-
if (window.matchMedia("(display-mode: standalone)").matches || "standalone" in navigator && navigator.standalone === true) return;
|
|
122
|
-
let cancelled = false;
|
|
123
|
-
fetch(INSTALL_PATH, { headers: { accept: "application/json" } }).then(async (response) => {
|
|
124
|
-
if (!response.ok) return;
|
|
125
|
-
const parsed = installSchema.safeParse(await response.json());
|
|
126
|
-
if (!parsed.success || cancelled) return;
|
|
127
|
-
setInstall({
|
|
128
|
-
available: true,
|
|
129
|
-
platform: detectPlatform(),
|
|
130
|
-
...parsed.data
|
|
131
|
-
});
|
|
132
|
-
}).catch(() => {});
|
|
133
|
-
return () => {
|
|
134
|
-
cancelled = true;
|
|
135
|
-
};
|
|
136
|
-
}, []);
|
|
137
|
-
return install;
|
|
138
|
-
}
|
|
139
|
-
function detectPlatform() {
|
|
140
|
-
const userAgent = navigator.userAgent;
|
|
141
|
-
if (/iPhone|iPad|iPod/.test(userAgent) || /Macintosh/.test(userAgent) && navigator.maxTouchPoints > 1) return "ios";
|
|
142
|
-
if (/Android/.test(userAgent)) return "android";
|
|
143
|
-
return "desktop";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//#endregion
|
|
147
|
-
export { SoftrPwaHead, pwaMiddleware, usePwaInstall };
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: pwa
|
|
3
|
-
description: >-
|
|
4
|
-
Let visitors install the app on their home screen: SoftrPwaHead for the
|
|
5
|
-
manifest and icon links, usePwaInstall to decide whether to show an install
|
|
6
|
-
button and which platform's steps to show, the PwaInstall and PwaPlatform
|
|
7
|
-
types, how to check that the app is installable, and the default install
|
|
8
|
-
button pattern. The manifest and icon are already served; never write them.
|
|
9
|
-
metadata:
|
|
10
|
-
type: sub-skill
|
|
11
|
-
library: softr-sdk
|
|
12
|
-
library_version: 0.0.48-alpha.0
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
# Installing the app
|
|
16
|
-
|
|
17
|
-
The builder turns installing on in Studio, under Settings → Mobile app, and sets the name, colour and icon there. The app already answers with its manifest and icon once they do. What the app adds is two things: the links that point the browser at them, and a button that tells visitors how to install.
|
|
18
|
-
|
|
19
|
-
> **CRITICAL**: Never write a web app manifest, icon files, a service worker, or `manifest` / `apple-touch-icon` links of your own, in `public/` or in `head()`. The app already serves them from Studio's settings, and a file of your own shadows those settings.
|
|
20
|
-
> **CRITICAL**: Render an install button only while `usePwaInstall()` returns `available: true`. Never show it unconditionally, never cache the answer, never replace the check with your own. Softr can switch installing off for an app, and this check is the only way that reaches the button.
|
|
21
|
-
|
|
22
|
-
## Setup
|
|
23
|
-
|
|
24
|
-
```ts
|
|
25
|
-
import { SoftrPwaHead, usePwaInstall } from '@softr/sdk'
|
|
26
|
-
import type { PwaInstall, PwaPlatform } from '@softr/sdk'
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
```ts
|
|
30
|
-
type PwaPlatform = 'ios' | 'android' | 'desktop'
|
|
31
|
-
|
|
32
|
-
type PwaInstall =
|
|
33
|
-
| { available: false }
|
|
34
|
-
| {
|
|
35
|
-
available: true
|
|
36
|
-
platform: PwaPlatform // which steps install the app on this device
|
|
37
|
-
name: string // what the home screen will show
|
|
38
|
-
iconUrl: string // the icon the home screen will show
|
|
39
|
-
}
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## Is the app installable?
|
|
43
|
-
|
|
44
|
-
Ask the running app for `/_softr/pwa.json`. `200` means installable, and the body is the name and icon. `404` means the builder has not turned installing on, or has not uploaded an icon. Do not build an install button against a `404`: tell the builder what to turn on instead. A change in Studio can take a few minutes to reach the app.
|
|
45
|
-
|
|
46
|
-
## Core Patterns
|
|
47
|
-
|
|
48
|
-
### The links: `<SoftrPwaHead />`
|
|
49
|
-
|
|
50
|
-
Render it once, in the root route's shell. React moves the links into `<head>`, so it can sit anywhere in the tree.
|
|
51
|
-
|
|
52
|
-
```tsx
|
|
53
|
-
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
54
|
-
return (
|
|
55
|
-
<html lang="en">
|
|
56
|
-
<head>
|
|
57
|
-
<HeadContent />
|
|
58
|
-
</head>
|
|
59
|
-
<body>
|
|
60
|
-
<SoftrPwaHead />
|
|
61
|
-
{children}
|
|
62
|
-
<Scripts />
|
|
63
|
-
</body>
|
|
64
|
-
</html>
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
### Whether to show a button: `usePwaInstall()`
|
|
70
|
-
|
|
71
|
-
It fetches after mount, so the first render, the server render included, always says `available: false`. It stays unavailable when the app is not installable, when the request fails, and when the app already runs installed from the home screen. Inside the Studio preview it says available, so the builder can see the button; installing itself only works on the published app.
|
|
72
|
-
|
|
73
|
-
There is no `install()` to call. No browser opens its install dialog for this app on request, so the button opens instructions instead.
|
|
74
|
-
|
|
75
|
-
### The default button pattern
|
|
76
|
-
|
|
77
|
-
Start from this unless the builder asked for something else. A round floating button with a download icon, placed where it covers nothing else on screen, that opens a sheet with the steps for the visitor's platform.
|
|
78
|
-
|
|
79
|
-
```tsx
|
|
80
|
-
function InstallButton() {
|
|
81
|
-
const install = usePwaInstall()
|
|
82
|
-
const [open, setOpen] = useState(false)
|
|
83
|
-
if (!install.available) return null
|
|
84
|
-
|
|
85
|
-
return (
|
|
86
|
-
<>
|
|
87
|
-
<button
|
|
88
|
-
type="button"
|
|
89
|
-
aria-label={`Install ${install.name}`}
|
|
90
|
-
onClick={() => setOpen(true)}
|
|
91
|
-
className="fixed right-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-40 size-12 rounded-full"
|
|
92
|
-
>
|
|
93
|
-
<DownloadIcon />
|
|
94
|
-
</button>
|
|
95
|
-
{open ? <InstallSteps install={install} onClose={() => setOpen(false)} /> : null}
|
|
96
|
-
</>
|
|
97
|
-
)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function InstallSteps({
|
|
101
|
-
install,
|
|
102
|
-
onClose,
|
|
103
|
-
}: {
|
|
104
|
-
install: Extract<PwaInstall, { available: true }>
|
|
105
|
-
onClose: () => void
|
|
106
|
-
}) {
|
|
107
|
-
return (
|
|
108
|
-
<dialog open onClose={onClose}>
|
|
109
|
-
<img src={install.iconUrl} alt="" width={64} height={64} />
|
|
110
|
-
<h2>Install {install.name}</h2>
|
|
111
|
-
{install.platform === 'ios' ? (
|
|
112
|
-
<ol>
|
|
113
|
-
<li>Tap the Share button in the browser's toolbar.</li>
|
|
114
|
-
<li>Choose Add to Home Screen.</li>
|
|
115
|
-
<li>Tap Add.</li>
|
|
116
|
-
</ol>
|
|
117
|
-
) : (
|
|
118
|
-
<ol>
|
|
119
|
-
<li>Open the browser's menu.</li>
|
|
120
|
-
<li>Choose Install app, or Add to Home screen.</li>
|
|
121
|
-
</ol>
|
|
122
|
-
)}
|
|
123
|
-
</dialog>
|
|
124
|
-
)
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
The markup is only the shape. Build it from the app's own components and design tokens so it looks like part of the app, and change its look, place or wording freely. What does not change is the `available` check that decides whether it renders at all.
|
|
129
|
-
|
|
130
|
-
## Common Mistakes
|
|
131
|
-
|
|
132
|
-
### Showing the button without the check
|
|
133
|
-
|
|
134
|
-
Wrong:
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
function InstallButton() {
|
|
138
|
-
const install = usePwaInstall()
|
|
139
|
-
return <button onClick={openSteps}>Install</button>
|
|
140
|
-
}
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
Correct:
|
|
144
|
-
|
|
145
|
-
```tsx
|
|
146
|
-
function InstallButton() {
|
|
147
|
-
const install = usePwaInstall()
|
|
148
|
-
if (!install.available) return null
|
|
149
|
-
return <button onClick={openSteps}>Install {install.name}</button>
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
A button that shows unconditionally stays up when installing is off, on a device that already has the app installed, and inside the installed app itself.
|
|
154
|
-
|
|
155
|
-
### Adding a manifest file
|
|
156
|
-
|
|
157
|
-
Wrong: creating `public/manifest.webmanifest`, `public/apple-touch-icon.png`, or a `head()` link to either.
|
|
158
|
-
|
|
159
|
-
Correct: `<SoftrPwaHead />` once in the root. The builder changes the name and icon in Studio, and a file in the app would keep the old ones.
|
|
160
|
-
|
|
161
|
-
## References
|
|
162
|
-
|
|
163
|
-
- [SKILL.md](../SKILL.md): the entrypoints and which one each export lives on
|