@pylonsync/create-pylon 0.9.1 → 0.10.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/package.json +1 -1
- package/templates/_root/AGENTS.md +11 -0
- package/templates/agency/AGENTS.md +11 -0
- package/templates/ai-chat/AGENTS.md +11 -0
- package/templates/ai-studio/AGENTS.md +11 -0
- package/templates/backend/mobile/apps/api/README.md +20 -1
- package/templates/backend/mobile/apps/api/app/(site)/layout.tsx +103 -0
- package/templates/backend/mobile/apps/api/app/(site)/not-found.tsx +23 -0
- package/templates/backend/mobile/apps/api/app/(site)/page.tsx +221 -0
- package/templates/backend/mobile/apps/api/app/(site)/privacy/page.tsx +131 -0
- package/templates/backend/mobile/apps/api/app/(site)/support/page.tsx +78 -0
- package/templates/backend/mobile/apps/api/app/(site)/terms/page.tsx +144 -0
- package/templates/backend/mobile/apps/api/app/error.tsx +27 -0
- package/templates/backend/mobile/apps/api/app/globals.css +87 -0
- package/templates/backend/mobile/apps/api/app/layout.tsx +27 -0
- package/templates/backend/mobile/apps/api/app/robots.ts +13 -0
- package/templates/backend/mobile/apps/api/app/sitemap.ts +16 -0
- package/templates/backend/mobile/apps/api/app.ts +6 -2
- package/templates/backend/mobile/apps/api/components/legal.tsx +64 -0
- package/templates/backend/mobile/apps/api/lib/site.ts +94 -0
- package/templates/backend/mobile/apps/api/package.json +8 -1
- package/templates/backend/mobile/apps/api/tsconfig.json +6 -2
- package/templates/barebones/AGENTS.md +11 -0
- package/templates/chat/AGENTS.md +11 -0
- package/templates/consumer/AGENTS.md +11 -0
- package/templates/creator/AGENTS.md +11 -0
- package/templates/crm/AGENTS.md +11 -0
- package/templates/directory/AGENTS.md +11 -0
- package/templates/expo/mobile/apps/expo/.env.example +13 -4
- package/templates/expo/mobile/apps/expo/README.md +9 -0
- package/templates/expo/mobile/apps/expo/STORE.md +9 -5
- package/templates/expo/mobile/apps/expo/app/(tabs)/settings.tsx +9 -6
- package/templates/expo/mobile/apps/expo/app/paywall.tsx +9 -13
- package/templates/expo/mobile/apps/expo/src/links.ts +29 -0
- package/templates/helpdesk/AGENTS.md +11 -0
- package/templates/inventory/AGENTS.md +11 -0
- package/templates/invoices/AGENTS.md +11 -0
- package/templates/local-service/AGENTS.md +11 -0
- package/templates/marketplace/AGENTS.md +11 -0
- package/templates/projects/AGENTS.md +11 -0
- package/templates/restaurant/AGENTS.md +11 -0
- package/templates/saas/AGENTS.md +11 -0
- package/templates/saas/app/dashboard/layout.tsx +9 -2
- package/templates/saas/app/dashboard/page.tsx +37 -14
- package/templates/saas/app/dashboard/settings/page.tsx +11 -4
- package/templates/shop/AGENTS.md +11 -0
- package/templates/todo/AGENTS.md +11 -0
- package/templates/waitlist/AGENTS.md +11 -0
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/crm/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -13,7 +13,16 @@ EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=
|
|
|
13
13
|
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=
|
|
14
14
|
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
# The privacy policy, terms, and help pages are served by the backend in
|
|
17
|
+
# apps/api, so they default to EXPO_PUBLIC_PYLON_BASE_URL + /privacy, /terms,
|
|
18
|
+
# and /support. Both stores require a reachable privacy policy URL.
|
|
19
|
+
#
|
|
20
|
+
# Set these only if the marketing site lives somewhere else, e.g. your own
|
|
21
|
+
# domain. EXPO_PUBLIC_SITE_URL moves all three at once.
|
|
22
|
+
# EXPO_PUBLIC_SITE_URL=https://yourapp.com
|
|
23
|
+
# EXPO_PUBLIC_PRIVACY_URL=
|
|
24
|
+
# EXPO_PUBLIC_TERMS_URL=
|
|
25
|
+
# EXPO_PUBLIC_SUPPORT_URL=
|
|
26
|
+
|
|
27
|
+
# Optional. Adds a "Contact support" row in Settings.
|
|
28
|
+
EXPO_PUBLIC_SUPPORT_EMAIL=
|
|
@@ -15,9 +15,16 @@ src/session.tsx boot + state machine
|
|
|
15
15
|
src/purchases.ts RevenueCat wrapper, safe in Expo Go
|
|
16
16
|
src/entitlements.ts usePro() from the synced RcEntitlement rows
|
|
17
17
|
src/analytics.ts funnel events; wire to your SDK in one place
|
|
18
|
+
src/links.ts privacy / terms / support URLs, served by apps/api
|
|
18
19
|
STORE.md the submission checklist
|
|
19
20
|
```
|
|
20
21
|
|
|
22
|
+
The public website (landing page, `/privacy`, `/terms`, `/support`) is served
|
|
23
|
+
by the backend in `apps/api`, on the same host as the API. The links in
|
|
24
|
+
Settings and under the paywall point there with no configuration, which is
|
|
25
|
+
what makes the store requirement satisfiable from a fresh scaffold. Edit its
|
|
26
|
+
copy in `apps/api/lib/site.ts`.
|
|
27
|
+
|
|
21
28
|
## Run
|
|
22
29
|
|
|
23
30
|
```bash
|
|
@@ -49,6 +56,8 @@ also carry Notes-specific copy or logic:
|
|
|
49
56
|
- `app/(tabs)/settings.tsx`: the delete-account confirmation.
|
|
50
57
|
- `app/(tabs)/index.tsx`: the list, `FREE_LIMIT`, and the sign-in nudge.
|
|
51
58
|
- `app.config.ts`: `name`, `slug`, `scheme`, and the icons in `assets/`.
|
|
59
|
+
- `apps/api/lib/site.ts`: the website copy, store links, and the company
|
|
60
|
+
details the privacy policy and terms need.
|
|
52
61
|
- `apps/api/functions/deleteMyData.ts`: delete every entity that stores
|
|
53
62
|
user data, or account deletion leaves rows behind.
|
|
54
63
|
- `apps/api/functions/createNote.ts`: the free-tier cap.
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The order below is the shortest path that App Review and Google Play accept
|
|
4
4
|
on the first try. Each step is one command or one dashboard page.
|
|
5
5
|
|
|
6
|
-
## 1. Backend live
|
|
6
|
+
## 1. Backend and website live
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
9
|
cd apps/api && pylon deploy
|
|
@@ -52,8 +52,12 @@ Test the purchase on TestFlight with a sandbox Apple ID before submitting.
|
|
|
52
52
|
|
|
53
53
|
Both stores require, and reviewers check:
|
|
54
54
|
|
|
55
|
-
- Privacy policy URL and terms URL
|
|
56
|
-
|
|
55
|
+
- Privacy policy URL and terms URL. The backend serves them at
|
|
56
|
+
`<your-backend>/privacy` and `<your-backend>/terms`, and the app links to
|
|
57
|
+
them from Settings and under the paywall with no configuration. Fill in
|
|
58
|
+
`apps/api/lib/site.ts` first: the text ships as a draft and a banner stays
|
|
59
|
+
on the page until you do.
|
|
60
|
+
- A support URL for App Store Connect: `<your-backend>/support`.
|
|
57
61
|
- Account deletion inside the app (Settings → Delete account). Present.
|
|
58
62
|
- Sign in with Apple when any other third-party sign-in is offered. Present.
|
|
59
63
|
- Subscription terms next to the purchase button (price, period, renewal). Present.
|
|
@@ -87,8 +91,8 @@ Without it `eas submit` asks for the app interactively.
|
|
|
87
91
|
RevenueCat, and the iOS and Android public keys are in `apps/expo/.env`.
|
|
88
92
|
- Sign-in: `PYLON_APPLE_NATIVE_CLIENT_IDS` (the bundle id) and
|
|
89
93
|
`PYLON_GOOGLE_NATIVE_CLIENT_IDS` are set on the backend.
|
|
90
|
-
- Legal: `
|
|
91
|
-
`
|
|
94
|
+
- Legal: `apps/api/lib/site.ts` is filled in, the draft banner is gone from
|
|
95
|
+
`/privacy` and `/terms`, and a lawyer has read both.
|
|
92
96
|
- Deletion: `apps/api/functions/deleteMyData.ts` removes every entity that
|
|
93
97
|
stores user data. Test it with a throwaway account.
|
|
94
98
|
- Copy: the "Replace the demo" list in `README.md` is done.
|
|
@@ -4,6 +4,7 @@ import { useRouter } from "expo-router";
|
|
|
4
4
|
import Constants from "expo-constants";
|
|
5
5
|
import { deleteAccount } from "@pylonsync/react-native";
|
|
6
6
|
import { track } from "@/analytics";
|
|
7
|
+
import { PRIVACY_URL, SUPPORT_EMAIL, SUPPORT_URL, TERMS_URL } from "@/links";
|
|
7
8
|
import { usePro } from "@/entitlements";
|
|
8
9
|
import { resetFlags } from "@/flags";
|
|
9
10
|
import { manageSubscriptionUrl, restore } from "@/purchases";
|
|
@@ -27,9 +28,8 @@ export default function Settings() {
|
|
|
27
28
|
const { pro } = usePro();
|
|
28
29
|
const [busy, setBusy] = useState<string | null>(null);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const support = process.env.EXPO_PUBLIC_SUPPORT_EMAIL;
|
|
31
|
+
// These resolve to the site the backend serves (apps/api), so the rows
|
|
32
|
+
// always render and always lead somewhere real. See src/links.ts.
|
|
33
33
|
const version = Constants.expoConfig?.version ?? "dev";
|
|
34
34
|
|
|
35
35
|
async function restorePurchases() {
|
|
@@ -99,9 +99,12 @@ export default function Settings() {
|
|
|
99
99
|
<Spacer />
|
|
100
100
|
|
|
101
101
|
<Caption>About</Caption>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
<Row label="Help" onPress={() => void Linking.openURL(SUPPORT_URL)} />
|
|
103
|
+
<Row label="Privacy policy" onPress={() => void Linking.openURL(PRIVACY_URL)} />
|
|
104
|
+
<Row label="Terms of service" onPress={() => void Linking.openURL(TERMS_URL)} />
|
|
105
|
+
{SUPPORT_EMAIL ? (
|
|
106
|
+
<Row label="Contact support" onPress={() => void Linking.openURL(`mailto:${SUPPORT_EMAIL}`)} />
|
|
107
|
+
) : null}
|
|
105
108
|
<Row label="Version" value={version} />
|
|
106
109
|
<Spacer />
|
|
107
110
|
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from "react";
|
|
|
2
2
|
import { Alert, Linking, Pressable, Text, View } from "react-native";
|
|
3
3
|
import { useLocalSearchParams, useRouter } from "expo-router";
|
|
4
4
|
import { track } from "@/analytics";
|
|
5
|
+
import { PRIVACY_URL, TERMS_URL } from "@/links";
|
|
5
6
|
import { usePro } from "@/entitlements";
|
|
6
7
|
import { available, offers, purchase, restore, type Offer } from "@/purchases";
|
|
7
8
|
import { radius, space, useTheme } from "@/theme";
|
|
@@ -45,8 +46,7 @@ export default function Paywall() {
|
|
|
45
46
|
if (pro) router.back();
|
|
46
47
|
}, [pro, router]);
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
const terms = process.env.EXPO_PUBLIC_TERMS_URL;
|
|
49
|
+
|
|
50
50
|
|
|
51
51
|
async function buy() {
|
|
52
52
|
const offer = list?.find((o) => o.id === selected);
|
|
@@ -160,17 +160,13 @@ export default function Paywall() {
|
|
|
160
160
|
<Button title="Restore purchases" variant="ghost" loading={busy === "restore"} onPress={() => void restorePurchases()} />
|
|
161
161
|
<Text style={{ color: t.muted, fontSize: 11, textAlign: "center", lineHeight: 16, marginTop: space.sm }}>
|
|
162
162
|
Renews automatically until cancelled. Manage in your store account settings.{" "}
|
|
163
|
-
{
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
<Text style={{ textDecorationLine: "underline" }} onPress={() => void Linking.openURL(privacy)}>
|
|
171
|
-
Privacy
|
|
172
|
-
</Text>
|
|
173
|
-
) : null}
|
|
163
|
+
<Text style={{ textDecorationLine: "underline" }} onPress={() => void Linking.openURL(TERMS_URL)}>
|
|
164
|
+
Terms
|
|
165
|
+
</Text>
|
|
166
|
+
{" · "}
|
|
167
|
+
<Text style={{ textDecorationLine: "underline" }} onPress={() => void Linking.openURL(PRIVACY_URL)}>
|
|
168
|
+
Privacy
|
|
169
|
+
</Text>
|
|
174
170
|
</Text>
|
|
175
171
|
</Screen>
|
|
176
172
|
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the app sends people for legal text and help.
|
|
3
|
+
*
|
|
4
|
+
* The backend in `apps/api` serves a website as well as the API, so these
|
|
5
|
+
* pages already exist on whatever host `EXPO_PUBLIC_PYLON_BASE_URL` points
|
|
6
|
+
* at. That is what makes the App Store and Play Store requirements
|
|
7
|
+
* satisfiable straight from a scaffold: both stores refuse a submission
|
|
8
|
+
* without a reachable privacy policy URL.
|
|
9
|
+
*
|
|
10
|
+
* Set the EXPO_PUBLIC_* values only if you host the marketing site somewhere
|
|
11
|
+
* else, on your own domain for example.
|
|
12
|
+
*/
|
|
13
|
+
import { PYLON_BASE_URL } from "./pylon";
|
|
14
|
+
|
|
15
|
+
const siteOrigin = (
|
|
16
|
+
process.env.EXPO_PUBLIC_SITE_URL || PYLON_BASE_URL
|
|
17
|
+
).replace(/\/+$/, "");
|
|
18
|
+
|
|
19
|
+
export const PRIVACY_URL =
|
|
20
|
+
process.env.EXPO_PUBLIC_PRIVACY_URL || `${siteOrigin}/privacy`;
|
|
21
|
+
|
|
22
|
+
export const TERMS_URL =
|
|
23
|
+
process.env.EXPO_PUBLIC_TERMS_URL || `${siteOrigin}/terms`;
|
|
24
|
+
|
|
25
|
+
export const SUPPORT_URL =
|
|
26
|
+
process.env.EXPO_PUBLIC_SUPPORT_URL || `${siteOrigin}/support`;
|
|
27
|
+
|
|
28
|
+
/** Optional. Settings shows a "Contact support" row when it is set. */
|
|
29
|
+
export const SUPPORT_EMAIL = process.env.EXPO_PUBLIC_SUPPORT_EMAIL;
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
package/templates/saas/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|
|
@@ -36,8 +36,15 @@ export default function DashboardLayout({
|
|
|
36
36
|
if (!auth.tenant_id) {
|
|
37
37
|
return <>{children}</>;
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// Every read is started before the first use(): calling them one at a time
|
|
40
|
+
// makes each wait for the last, since use() suspends on the first pending
|
|
41
|
+
// thenable. Never Promise.all them — a new pending promise each render means
|
|
42
|
+
// the page never returns (React error #482).
|
|
43
|
+
const mePromise = serverData.get<{ email?: string }>("User", auth.user_id);
|
|
44
|
+
const orgPromise = serverData.get<{ name?: string }>("Org", auth.tenant_id);
|
|
45
|
+
|
|
46
|
+
const me = use(mePromise);
|
|
47
|
+
const org = use(orgPromise);
|
|
41
48
|
// Longest matching NAV href wins so /dashboard/projects highlights
|
|
42
49
|
// Projects, not Overview.
|
|
43
50
|
const path = (url ?? "").split("?")[0];
|
|
@@ -27,13 +27,35 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
27
27
|
if (!auth.tenant_id) {
|
|
28
28
|
return <ProvisionWorkspace />;
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
// Every read is STARTED here, before the first use(). Each serverData call
|
|
31
|
+
// returns a thenable the handle caches by key, and use() suspends on the
|
|
32
|
+
// first one still pending — so reading them one at a time makes each wait
|
|
33
|
+
// for the last. Six reads that way is six round trips; issued together it
|
|
34
|
+
// is one, and the replayed render finds each already cached.
|
|
35
|
+
//
|
|
36
|
+
// Do NOT reach for Promise.all. It builds a new, pending, uncached promise
|
|
37
|
+
// on every render, so use() suspends, re-renders, builds another, and the
|
|
38
|
+
// page never returns — React reports it as an async Client Component
|
|
39
|
+
// (minified error #482).
|
|
40
|
+
//
|
|
41
|
+
// The onboarding redirect below only needs `org`, so on that path the other
|
|
42
|
+
// reads are issued and thrown away. A redirect is the rare case; paying one
|
|
43
|
+
// round trip on every normal load to save four on a rare one is the wrong
|
|
44
|
+
// way round.
|
|
45
|
+
const mePromise = serverData.get<{ email?: string }>("User", auth.user_id!);
|
|
46
|
+
const orgPromise = serverData.get<{
|
|
47
|
+
name?: string;
|
|
48
|
+
onboardedAt?: string | null;
|
|
49
|
+
setupDismissedAt?: string | null;
|
|
50
|
+
}>("Org", auth.tenant_id);
|
|
51
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
52
|
+
const membersPromise = serverData.list<OrgMemberRow>("OrgMember");
|
|
53
|
+
const subsPromise = serverData.list<Subscription>("StripeSubscription");
|
|
54
|
+
const invitesPromise =
|
|
55
|
+
serverData.list<{ orgId: string; acceptedAt?: string | null }>("OrgInvite");
|
|
56
|
+
|
|
57
|
+
const me = use(mePromise);
|
|
58
|
+
const org = use(orgPromise);
|
|
37
59
|
// A workspace created outside the wizard (or one that abandoned it) gets
|
|
38
60
|
// sent through it once. Owners/admins only; a member who lands here just
|
|
39
61
|
// sees the dashboard.
|
|
@@ -43,21 +65,22 @@ export default function DashboardPage({ auth, response, serverData }: PageProps)
|
|
|
43
65
|
response.redirect("/onboarding");
|
|
44
66
|
return null;
|
|
45
67
|
}
|
|
46
|
-
const projects = use(
|
|
47
|
-
const members = use(serverData.list<OrgMemberRow>("OrgMember"));
|
|
68
|
+
const projects = use(projectsPromise);
|
|
48
69
|
// The OrgMember read policy returns this user's memberships across every org,
|
|
49
70
|
// so scope the count to the active workspace.
|
|
50
|
-
const memberCount =
|
|
71
|
+
const memberCount = use(membersPromise).filter(
|
|
72
|
+
(m) => m.orgId === auth.tenant_id,
|
|
73
|
+
).length;
|
|
51
74
|
// Active-plan badge from the workspace's Stripe subscription (Free until one
|
|
52
75
|
// exists). Scoped to the active tenant by the plugin's read policy.
|
|
53
|
-
const
|
|
54
|
-
const active = subs.find((s) =>
|
|
76
|
+
const active = use(subsPromise).find((s) =>
|
|
55
77
|
["active", "trialing", "past_due"].includes(s.status),
|
|
56
78
|
);
|
|
57
79
|
const plan = active ? active.plan : "free";
|
|
58
80
|
// Getting-started checklist, derived from real rows so it ticks itself off.
|
|
59
|
-
const
|
|
60
|
-
|
|
81
|
+
const pendingInvites = use(invitesPromise).filter(
|
|
82
|
+
(i) => i.orgId === auth.tenant_id && !i.acceptedAt,
|
|
83
|
+
).length;
|
|
61
84
|
const setup: SetupState | null = org?.setupDismissedAt
|
|
62
85
|
? null
|
|
63
86
|
: {
|
|
@@ -21,10 +21,17 @@ export default function SettingsPage({ auth, response, serverData }: PageProps)
|
|
|
21
21
|
response.redirect("/dashboard");
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Every read is started before the first use(): calling them one at a time
|
|
25
|
+
// makes each wait for the last, since use() suspends on the first pending
|
|
26
|
+
// thenable. Never Promise.all them — a new pending promise each render means
|
|
27
|
+
// the page never returns (React error #482).
|
|
28
|
+
const orgPromise = serverData.get<OrgInfo>("Org", auth.tenant_id);
|
|
29
|
+
const mePromise = serverData.get<AccountInfo>("User", auth.user_id!);
|
|
30
|
+
const membersPromise = serverData.list<OrgMemberRow>("OrgMember");
|
|
31
|
+
|
|
32
|
+
const org = use(orgPromise);
|
|
33
|
+
const me = use(mePromise);
|
|
34
|
+
const memberCount = use(membersPromise).filter(
|
|
28
35
|
(m) => m.orgId === auth.tenant_id,
|
|
29
36
|
).length;
|
|
30
37
|
return (
|
package/templates/shop/AGENTS.md
CHANGED
|
@@ -48,6 +48,17 @@ this pass before the user has to ask.
|
|
|
48
48
|
- **`error.tsx` / `not-found.tsx` boundaries are HYDRATED (interactive).** `app/.../error.tsx` catches a throw below it (HTTP 500) and receives `{ error: { message, digest }, reset }` (`import type { ErrorBoundaryProps }`); `reset()` re-attempts the route; the stack NEVER reaches the client (dev overlay + logs only). `app/.../not-found.tsx` renders at 404 (also for `response.notFound()`) and gets the page props (`NotFoundProps`), no `reset`. Both run useState/onClick/hooks.
|
|
49
49
|
- **Client navigation hooks live in @pylonsync/react.** `useRouter()` → `{ push, replace, back, forward, refresh, prefetch }`; `useSearchParams()` → reactive `URLSearchParams`; `usePathname()` → reactive pathname. The hooks are CLIENT-reactive; during SSR they return defaults (empty params / "/"); for server-side URL values read the `pathname` / `searchParams` page props (`pathname` is the PATH only — the query is already parsed into `searchParams`, so never try to read a query parameter back out of it; the older `url` prop is the same value and is deprecated).
|
|
50
50
|
- **Dynamic + catch-all routes follow Next conventions.** `app/blog/[slug]/page.tsx` → `params.slug`. `app/docs/[...path]/page.tsx` is a catch-all (matches `/docs/a/b/c`; `params.path === "a/b/c"`; `.split("/")` for segments). `app/shop/[[...filters]]/page.tsx` is an optional catch-all (also matches the bare `/shop`, with `params.filters === ""`). A catch-all must be the last segment; static beats dynamic beats catch-all on overlap.
|
|
51
|
+
- **Never wrap `serverData` calls in `Promise.all`.** Each method returns a thenable the handle CACHES by key — on the client it is already fulfilled, so `use()` returns synchronously. `Promise.all` builds a new, pending, uncached promise on every render, so `use()` suspends, re-renders, builds another, and the page never returns; React reports it as an async Client Component (minified error #482) and the error boundary shows a broken page. To read several things in parallel, START every call before the first `use()`, then `use()` each handle — the reads overlap and the replayed render finds each one cached:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
const orgPromise = serverData.get<Org>("Org", auth.tenant_id);
|
|
55
|
+
const projectsPromise = serverData.list<Project>("Project");
|
|
56
|
+
const org = use(orgPromise);
|
|
57
|
+
const projects = use(projectsPromise);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Reading them one at a time (`use(serverData.a()); use(serverData.b());`) is correct but serial: each read waits for the one above it.
|
|
61
|
+
|
|
51
62
|
- **`serverData` (SSR) is READ-ONLY.** No write methods; the runtime rejects write frames (`SSR_WRITE_FORBIDDEN`). Mutations belong in actions/functions, never in a page render.
|
|
52
63
|
- **`response.*` / `response.redirect()` / `response.notFound()` must fire in the synchronous shell render**, before any `await` / `<Suspense>`. The HTTP head commits when the shell is ready; status/headers/cookies set from a suspended subtree are lost, and `redirect`/`notFound` thrown below a Suspense boundary are swallowed.
|
|
53
64
|
- **`ctx.llm`, `ctx.rooms`, and `ctx.connections` are on mutation + action only, NOT query** (reactive purity). `action` has no direct `ctx.db`; use `ctx.runQuery` / `ctx.runMutation`.
|