@acms-subir/core 0.3.3-next.20260929113209 → 0.3.3
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/CHANGELOG.md +69 -0
- package/cli/common.mjs +2 -2
- package/cli/legacy.mjs +1 -1
- package/cli/main.mjs +82 -8
- package/cli/mobile-doctor.mjs +430 -0
- package/cli/mobile.mjs +315 -0
- package/cli/native-sections.mjs +168 -0
- package/cli/sections.mjs +186 -0
- package/cli/themes.mjs +52 -8
- package/docs/mobile.md +527 -0
- package/docs/sections.md +203 -0
- package/docs/theme-authoring.md +1 -1
- package/integration.mjs +55 -1
- package/mobile-template/client/AGENTS.md +97 -0
- package/mobile-template/client/README.md +65 -0
- package/mobile-template/client/app/_layout.tsx +56 -0
- package/mobile-template/client/app/index.tsx +21 -0
- package/mobile-template/client/app.json +21 -0
- package/mobile-template/client/components/LanguageSwitch.tsx +31 -0
- package/mobile-template/client/eas.json +21 -0
- package/mobile-template/client/env.example +3 -0
- package/mobile-template/client/gitignore +42 -0
- package/mobile-template/client/package.json +38 -0
- package/mobile-template/client/sections/index.ts +11 -0
- package/mobile-template/client/tsconfig.json +7 -0
- package/mobile-template/editor/AGENTS.md +66 -0
- package/mobile-template/editor/README.md +73 -0
- package/mobile-template/editor/app/_layout.tsx +76 -0
- package/mobile-template/editor/app/auth.tsx +13 -0
- package/mobile-template/editor/app/index.tsx +55 -0
- package/mobile-template/editor/app.json +21 -0
- package/mobile-template/editor/components/Screen.tsx +51 -0
- package/mobile-template/editor/components/SignIn.tsx +36 -0
- package/mobile-template/editor/components/TextTabs.tsx +27 -0
- package/mobile-template/editor/eas.json +21 -0
- package/mobile-template/editor/env.example +5 -0
- package/mobile-template/editor/gitignore +42 -0
- package/mobile-template/editor/lib/appearance.tsx +12 -0
- package/mobile-template/editor/lib/config.ts +17 -0
- package/mobile-template/editor/lib/format.ts +24 -0
- package/mobile-template/editor/lib/tools.ts +16 -0
- package/mobile-template/editor/package.json +40 -0
- package/mobile-template/editor/tools/README.md +2 -0
- package/mobile-template/editor/tsconfig.json +7 -0
- package/mobile-template/recipes/booking/files/app/book/[resource].tsx +57 -0
- package/mobile-template/recipes/booking/files/app/book/details.tsx +105 -0
- package/mobile-template/recipes/booking/files/app/book/index.tsx +38 -0
- package/mobile-template/recipes/booking/recipe.json +17 -0
- package/mobile-template/recipes/editor-bookings/files/app/bookings/index.tsx +113 -0
- package/mobile-template/recipes/editor-bookings/files/tools/bookings.ts +1 -0
- package/mobile-template/recipes/editor-bookings/recipe.json +8 -0
- package/mobile-template/recipes/editor-content/files/app/content/[kind]/[id].tsx +13 -0
- package/mobile-template/recipes/editor-content/files/app/content/index.tsx +76 -0
- package/mobile-template/recipes/editor-content/files/components/DocumentDetail.tsx +55 -0
- package/mobile-template/recipes/editor-content/files/tools/content.ts +1 -0
- package/mobile-template/recipes/editor-content/recipe.json +8 -0
- package/mobile-template/recipes/editor-media/files/app/media/index.tsx +73 -0
- package/mobile-template/recipes/editor-media/files/tools/media.ts +1 -0
- package/mobile-template/recipes/editor-media/recipe.json +8 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/[pageId]/[sectionId].tsx +12 -0
- package/mobile-template/recipes/editor-sections/files/app/sections/index.tsx +48 -0
- package/mobile-template/recipes/editor-sections/files/components/SectionEditorPane.tsx +77 -0
- package/mobile-template/recipes/editor-sections/files/tools/sections.ts +1 -0
- package/mobile-template/recipes/editor-sections/recipe.json +8 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/index.tsx +63 -0
- package/mobile-template/recipes/editor-translations/files/app/translations/review.tsx +14 -0
- package/mobile-template/recipes/editor-translations/files/components/ReviewPane.tsx +72 -0
- package/mobile-template/recipes/editor-translations/files/tools/translations.ts +1 -0
- package/mobile-template/recipes/editor-translations/recipe.json +8 -0
- package/mobile-template/recipes/editor-writer/files/app/write/[kind]/[id].tsx +132 -0
- package/mobile-template/recipes/editor-writer/files/app/write/new.tsx +61 -0
- package/mobile-template/recipes/editor-writer/files/tools/writer.ts +1 -0
- package/mobile-template/recipes/editor-writer/recipe.json +8 -0
- package/mobile-template/recipes/events/files/app/events/[slug].tsx +41 -0
- package/mobile-template/recipes/events/files/app/events/index.tsx +50 -0
- package/mobile-template/recipes/events/files/lib/addToCalendar.ts +27 -0
- package/mobile-template/recipes/events/recipe.json +15 -0
- package/mobile-template/recipes/news/files/app/news/[slug].tsx +25 -0
- package/mobile-template/recipes/news/files/app/news/index.tsx +50 -0
- package/mobile-template/recipes/news/recipe.json +13 -0
- package/mobile-template/recipes/push/files/app/notifications.tsx +64 -0
- package/mobile-template/recipes/push/files/components/PushSetup.tsx +15 -0
- package/mobile-template/recipes/push/files/lib/push.ts +15 -0
- package/mobile-template/recipes/push/recipe.json +20 -0
- package/package.json +4 -2
- package/section-library/README.md +16 -0
- package/section-library/contact/Contact.astro +49 -0
- package/section-library/contact/section.json +106 -0
- package/section-library/cta-band/CtaBand.astro +28 -0
- package/section-library/cta-band/section.json +51 -0
- package/section-library/feature-grid/FeatureGrid.astro +54 -0
- package/section-library/feature-grid/section.json +107 -0
- package/section-library/gallery/Gallery.astro +29 -0
- package/section-library/gallery/section.json +51 -0
- package/section-library/hero-banner/HeroBanner.astro +42 -0
- package/section-library/hero-banner/section.json +81 -0
- package/section-library/latest-posts/LatestPosts.astro +22 -0
- package/section-library/latest-posts/section.json +37 -0
- package/section-library/pricing/Pricing.astro +40 -0
- package/section-library/pricing/section.json +117 -0
- package/section-library/questions/Questions.astro +38 -0
- package/section-library/questions/section.json +69 -0
- package/section-library/service-times/ServiceTimes.astro +42 -0
- package/section-library/service-times/section.json +127 -0
- package/section-library/team/Team.astro +45 -0
- package/section-library/team/section.json +88 -0
- package/section-library/upcoming-events/UpcomingEvents.astro +54 -0
- package/section-library/upcoming-events/section.json +51 -0
- package/src/actions/index.ts +19 -171
- package/src/components/AiSettings.tsx +45 -5
- package/src/components/DesignManager.tsx +112 -16
- package/src/components/MobileApp.tsx +392 -0
- package/src/components/ThemeSections.astro +40 -21
- package/src/components/ThemeSectionsEditor.tsx +757 -229
- package/src/components/editor/PostEditor.tsx +174 -10
- package/src/components/import/ImportExport.tsx +4 -1
- package/src/components/import/runner.ts +1 -0
- package/src/components/sections/SectionFields.tsx +560 -0
- package/src/components/sections/SectionPicker.tsx +120 -0
- package/src/components/sections/TranslationPanel.tsx +143 -0
- package/src/components/sections/checks.ts +64 -0
- package/src/layouts/Admin.astro +6 -1
- package/src/layouts/Site.astro +107 -19
- package/src/lib/agent-discovery.ts +11 -7
- package/src/lib/agent.ts +35 -2
- package/src/lib/ai-schema.ts +5 -0
- package/src/lib/ai.ts +5 -0
- package/src/lib/api.ts +12 -0
- package/src/lib/content-service.ts +198 -0
- package/src/lib/design-service.ts +153 -5
- package/src/lib/drafts.ts +3 -3
- package/src/lib/editor/auth.ts +264 -0
- package/src/lib/editor/bookings.ts +60 -0
- package/src/lib/editor/config.ts +25 -0
- package/src/lib/editor/documents.ts +227 -0
- package/src/lib/editor/http.ts +48 -0
- package/src/lib/editor/me.ts +30 -0
- package/src/lib/editor/migration.ts +57 -0
- package/src/lib/editor/next.ts +11 -0
- package/src/lib/editor/openapi.ts +124 -0
- package/src/lib/editor/schemas.ts +91 -0
- package/src/lib/editor/sections.ts +96 -0
- package/src/lib/editor/store.ts +184 -0
- package/src/lib/editor/token-route.ts +74 -0
- package/src/lib/editor/translations.ts +163 -0
- package/src/lib/email/accounts.ts +2 -1
- package/src/lib/email/types.ts +2 -1
- package/src/lib/home-source.ts +34 -0
- package/src/lib/i18n.ts +34 -0
- package/src/lib/import/apply.ts +31 -2
- package/src/lib/import/bundle.ts +4 -0
- package/src/lib/import/export.ts +1 -0
- package/src/lib/import/ghost.ts +1 -0
- package/src/lib/media/upload.ts +83 -0
- package/src/lib/mobile/associations.ts +25 -0
- package/src/lib/mobile/calendar.ts +19 -11
- package/src/lib/mobile/config.mjs +59 -0
- package/src/lib/mobile/content.ts +18 -13
- package/src/lib/mobile/embed.ts +83 -0
- package/src/lib/mobile/http.ts +8 -6
- package/src/lib/mobile/locale.ts +18 -0
- package/src/lib/mobile/openapi.ts +111 -0
- package/src/lib/mobile/pages.ts +71 -0
- package/src/lib/mobile/route.ts +3 -3
- package/src/lib/mobile/schemas.ts +60 -0
- package/src/lib/mobile/site.ts +41 -0
- package/src/lib/push/api.ts +158 -0
- package/src/lib/push/config.ts +20 -0
- package/src/lib/push/expo.ts +76 -0
- package/src/lib/push/migration.ts +60 -0
- package/src/lib/push/service.ts +302 -0
- package/src/lib/push/store.ts +144 -0
- package/src/lib/push/types.ts +43 -0
- package/src/lib/repo/migrations.ts +11 -0
- package/src/lib/repo/sql.ts +40 -7
- package/src/lib/repo/types.ts +9 -3
- package/src/lib/roles.ts +5 -0
- package/src/lib/scheduler.ts +3 -0
- package/src/lib/schema.ts +27 -1
- package/src/lib/section-ai.ts +55 -0
- package/src/lib/section-targets.ts +62 -0
- package/src/lib/sections-agent.ts +146 -0
- package/src/lib/sections.ts +217 -0
- package/src/lib/theme.ts +16 -1
- package/src/lib/translate.ts +97 -0
- package/src/lib/translations.ts +39 -0
- package/src/middleware.ts +32 -0
- package/src/pages/.well-known/apple-app-site-association.ts +6 -0
- package/src/pages/.well-known/assetlinks.json.ts +6 -0
- package/src/pages/[...slug].astro +8 -3
- package/src/pages/admin/app-auth.astro +159 -0
- package/src/pages/admin/c/[collection]/[id].astro +9 -0
- package/src/pages/admin/calendar/events/[id].astro +9 -0
- package/src/pages/admin/login.astro +6 -2
- package/src/pages/admin/logout.ts +5 -2
- package/src/pages/admin/mobile.astro +18 -0
- package/src/pages/admin/pages/[id].astro +62 -0
- package/src/pages/admin/section-preview.astro +61 -0
- package/src/pages/admin/users.astro +107 -11
- package/src/pages/api/agent/docs.ts +6 -2
- package/src/pages/api/agent/index.ts +1 -1
- package/src/pages/api/ai/settings.ts +7 -4
- package/src/pages/api/design/ai.ts +15 -0
- package/src/pages/api/design/index.ts +12 -42
- package/src/pages/api/design/translate.ts +15 -0
- package/src/pages/api/live/thumb.ts +1 -1
- package/src/pages/api/mcp.ts +2 -2
- package/src/pages/api/media/upload.ts +10 -78
- package/src/pages/api/push/admin.ts +27 -0
- package/src/pages/api/v1/admin/bookings/[id]/[action].ts +14 -0
- package/src/pages/api/v1/admin/bookings/index.ts +6 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/[action].ts +17 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/index.ts +20 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/[action].ts +26 -0
- package/src/pages/api/v1/admin/documents/[kind]/[id]/translations/[locale]/index.ts +18 -0
- package/src/pages/api/v1/admin/documents/index.ts +13 -0
- package/src/pages/api/v1/admin/me.ts +6 -0
- package/src/pages/api/v1/admin/media/index.ts +32 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/publish.ts +14 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/[action].ts +22 -0
- package/src/pages/api/v1/admin/sections/[pageId]/[sectionId]/translations/[locale]/index.ts +14 -0
- package/src/pages/api/v1/admin/sections/index.ts +6 -0
- package/src/pages/api/v1/auth/revoke.ts +5 -0
- package/src/pages/api/v1/auth/token.ts +5 -0
- package/src/pages/api/v1/devices/[token].ts +20 -0
- package/src/pages/api/v1/devices/index.ts +17 -0
- package/src/pages/api/v1/openapi.json.ts +7 -0
- package/src/pages/api/v1/pages/[...slug].ts +7 -0
- package/src/pages/api/v1/pages/home.ts +7 -0
- package/src/pages/api/v1/push/topics.ts +7 -0
- package/src/pages/api/v1/site.ts +7 -0
- package/src/pages/api/v1/theme.ts +7 -0
- package/src/pages/blog/[slug].astro +8 -2
- package/src/pages/blog/index.astro +11 -4
- package/src/pages/embed/section/[...path].astro +42 -0
- package/src/pages/events/[slug].astro +7 -2
- package/src/pages/index.astro +11 -5
- package/src/pages/sitemap.xml.ts +8 -0
- package/src/styles/admin.css +69 -0
- package/src/styles/editor.css +21 -0
- package/src/styles/mobile-app.css +151 -0
- package/src/styles/sections.css +7 -1
- package/src/styles/theme-editor.css +255 -0
- package/src/theme-sdk/contract.d.mts +2193 -89
- package/src/theme-sdk/contract.mjs +77 -20
- package/src/theme-sdk/fields.d.mts +88 -0
- package/src/theme-sdk/fields.mjs +326 -0
- package/src/theme-sdk/index.ts +10 -2
- package/src/theme-sdk/tokens.d.mts +11 -0
- package/src/virtual.d.ts +1 -0
package/docs/mobile.md
ADDED
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
# Mobile apps: build the owner's own Expo app on their ACMS site
|
|
2
|
+
|
|
3
|
+
This is the guide for agents (and people) working on an ACMS mobile app. The
|
|
4
|
+
site owner edits content in the site admin; the app is **their own** Expo
|
|
5
|
+
(React Native) app that shows it on iOS, Android and the web. ACMS ships the
|
|
6
|
+
foundation (API, client, hooks, native sections, starter, recipes); you build
|
|
7
|
+
the app.
|
|
8
|
+
|
|
9
|
+
> **The site owns the content. The app owns the look and the navigation.**
|
|
10
|
+
|
|
11
|
+
Run every `acms` command from the **site folder** (the one that contains the
|
|
12
|
+
app folder, normally with `astro.config.mjs`), not from the app folder:
|
|
13
|
+
`npx acms …`. The app is a
|
|
14
|
+
separate project beside the site (default `mobile/`, its own `package.json`
|
|
15
|
+
and `node_modules`), found by the `"acms": { "app": "client" }` marker in its
|
|
16
|
+
`package.json`.
|
|
17
|
+
|
|
18
|
+
## Start and finish
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npx acms inspect --json # site, themes, mobile apps (dir, kind, bundle ids, versions)
|
|
22
|
+
npx acms docs mobile # this guide
|
|
23
|
+
npx acms add mobile # only when there is no app yet (acms mobile list)
|
|
24
|
+
cd mobile && npm install && cd ..
|
|
25
|
+
npx acms mobile dev # site + Expo on an address a phone can reach
|
|
26
|
+
# … work …
|
|
27
|
+
cd mobile && npx tsc --noEmit && cd ..
|
|
28
|
+
npx acms mobile doctor # must end with 0 failed; read every warning
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Read the app's `AGENTS.md` too: it holds the same rules, next to the code.
|
|
32
|
+
|
|
33
|
+
## Rules
|
|
34
|
+
|
|
35
|
+
1. **Public API only, through `@acms-subir/client`.** Use the hooks from
|
|
36
|
+
`@acms-subir/native` (they wrap the client). For anything without a hook,
|
|
37
|
+
call the typed client from `useAcms()` inside a TanStack Query hook. Never
|
|
38
|
+
raw `fetch`, never `/api/agent`, `/api/design`, `/api/media` or any other
|
|
39
|
+
admin route, never the database.
|
|
40
|
+
2. **No secrets in the app.** Everything in the bundle is public, including
|
|
41
|
+
every `EXPO_PUBLIC_*` value. No `ACMS_TOKEN`, API keys (`acms_key_…`,
|
|
42
|
+
`acms_agent_…`), admin passwords or `Authorization: Bearer …` literals. The
|
|
43
|
+
only setting is `EXPO_PUBLIC_ACMS_URL`: `.env` (committed) is the address
|
|
44
|
+
baked into builds, set by `acms add mobile --url`, and must be the https
|
|
45
|
+
production site before a store build (it may be a dev address until then);
|
|
46
|
+
`.env.local` (git-ignored) is the dev address `acms mobile dev` writes, and
|
|
47
|
+
wins while developing.
|
|
48
|
+
3. **Content comes from the site, never hardcoded.** Names, times, text,
|
|
49
|
+
events, prices, people, images, links, giving details, addresses: all read
|
|
50
|
+
through hooks. If the owner should be able to change it, it lives in the
|
|
51
|
+
admin (a page, its sections, events, posts, resources). Only UI chrome
|
|
52
|
+
belongs in code: "Try again", "Book", tab titles, empty-state wording.
|
|
53
|
+
If the content the task needs doesn't exist on the site yet, build the
|
|
54
|
+
screen to read it anyway (by page slug, calendar slug, …), show a clear
|
|
55
|
+
empty state, and tell the user exactly what the owner must create in the
|
|
56
|
+
admin. Don't invent the content in code.
|
|
57
|
+
4. **Sections render through `<Sections>` and the registry.** A page's
|
|
58
|
+
sections come from `usePage(slug)` / `<Sections page="slug" registry={sections}>`.
|
|
59
|
+
Each section uses the first match of: `sections/index.ts` (yours) →
|
|
60
|
+
the packaged library (`@acms-subir/native/sections`: hero-banner,
|
|
61
|
+
service-times, upcoming-events, feature-grid, team, pricing, gallery,
|
|
62
|
+
questions, cta-band, contact, latest-posts) → the website's rendering in a
|
|
63
|
+
web view → a generic view. Keys are a type or `type:variant`.
|
|
64
|
+
5. **Native twins mirror the web.** Same fields, same variants, same
|
|
65
|
+
empty-field behaviour as `src/themes/<theme>/sections/<Type>.astro`.
|
|
66
|
+
- Change a library section: `npx acms section add team --theme <slug> --native`
|
|
67
|
+
→ `mobile/sections/Team.tsx`, registered. Edit that copy.
|
|
68
|
+
- New coded section: `npx acms section new <type> --theme <slug>` writes the
|
|
69
|
+
web component **and** a typed native stub (`sections/<Type>.tsx`, types in
|
|
70
|
+
`sections/types.ts`). Finish the stub.
|
|
71
|
+
- Build from the exported pieces (`Grid`, `FocalImage`, `SectionHeader`,
|
|
72
|
+
`ActionButton`, `TextLink`, `useSectionMetrics`) so it stays themed and
|
|
73
|
+
responsive. `npx acms check` refreshes `sections/types.ts` and lists
|
|
74
|
+
sections that only show as a web view.
|
|
75
|
+
6. **Web-view fallback** covers the site's own coded sections until a native
|
|
76
|
+
version exists (needs `react-native-webview`; an iframe on web). Leave it on
|
|
77
|
+
unless every section has a native component. `webFallback={false}` /
|
|
78
|
+
`<AcmsProvider webSectionFallback={false}>` switches to the generic view.
|
|
79
|
+
7. **Internal links go through `onNavigate`.** Section and rich-text links may
|
|
80
|
+
be root-relative. Open them with `useOpenLink()`; site pages go to
|
|
81
|
+
`<AcmsProvider onNavigate={(path, url) => …}>` in `app/_layout.tsx`, where
|
|
82
|
+
you `router.push` to your own screen (`/events/<slug>` → `app/events/[slug].tsx`)
|
|
83
|
+
and fall back to `Linking.openURL(url)`. External, `mailto:` and `tel:`
|
|
84
|
+
links always leave the app.
|
|
85
|
+
8. **Languages via `useLocale()`.** `{ locale, defaultLocale, languages, setLocale }`.
|
|
86
|
+
Every hook re-fetches in the chosen language; the starter's
|
|
87
|
+
`LanguageSwitch` shows when the site has more than one. Translate words in
|
|
88
|
+
the admin, not in the app. Date/number formatting: `<EventDate>`,
|
|
89
|
+
`formatEventDate`, `Intl` with `locale`.
|
|
90
|
+
9. **Theme via `useTheme()`, light and dark.** `t.colors` (`bg`, `ink`,
|
|
91
|
+
`muted`, `line`, `accent`, `onAccent`, …), `t.styles` (`text.*`, `card`,
|
|
92
|
+
`button`, `space(n)`, `fonts`), `t.radius` (≤12), `t.scheme`. No literal
|
|
93
|
+
colors. Check both schemes. No pill shapes (10–12px radius rectangles or
|
|
94
|
+
plain text), no colored left-edge stripes, no gradient-squircle logos.
|
|
95
|
+
10. **Phones and tablets.** Lay out from `useLayout()` (`columns`, `gutter`,
|
|
96
|
+
`contentWidth`, `wide`) or `useWindowDimensions()`; never fixed widths.
|
|
97
|
+
On a tablet use the width (grids, split panes, `maxWidth` content column),
|
|
98
|
+
not a stretched phone column. `supportsTablet` is on.
|
|
99
|
+
11. **Touch targets ≥ 44pt** (`minHeight: 44`, or `hitSlop` on small icons);
|
|
100
|
+
`accessibilityRole` and labels on custom buttons; text scales with the
|
|
101
|
+
system font size.
|
|
102
|
+
12. **Offline.** `offlinePersister()` from `@acms-subir/native/persist` keeps
|
|
103
|
+
content reads for 7 days (install `@react-native-async-storage/async-storage`
|
|
104
|
+
and `@tanstack/query-async-storage-persister` with `npx expo install`).
|
|
105
|
+
Every screen needs loading, error (`<ErrorView error retry>`) and empty states,
|
|
106
|
+
and pull to refresh (`useRefresh()`).
|
|
107
|
+
13. **Bookings are idempotent.** Use `useBooking({ createKey: randomUUID })`
|
|
108
|
+
(expo-crypto): the same details reuse one Idempotency-Key until success,
|
|
109
|
+
so retries never double-book. Never put the manage token, email or key in
|
|
110
|
+
a URL, log or analytics; keep the manage token in `expo-secure-store`.
|
|
111
|
+
|
|
112
|
+
## Hooks at a glance
|
|
113
|
+
|
|
114
|
+
| Hook | For |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| `useSite()` | name, menu, languages, features (`calendar`, `bookings`, `blog`) |
|
|
117
|
+
| `usePage(slug)` | a published page: `mode` (`sections`/`content`), `sections`, `body.html`; 404 when missing |
|
|
118
|
+
| `usePosts({ collection, limit })` / `usePost(slug)` | posts (infinite: `items`, `fetchNextPage`) |
|
|
119
|
+
| `useEvents({ calendar, from, to, limit, q })` | `data.occurrences[]` (`start`, `end`, `allDay`, `event`) in a window (default 30 days, max 90) |
|
|
120
|
+
| `useEvent(slug)` | one event |
|
|
121
|
+
| `useResources()` / `useAvailability({ resource, link })` / `useBooking()` | bookings |
|
|
122
|
+
| `useLocale()`, `useTheme()`, `useLayout()`, `useRefresh()`, `useOpenLink()` | language, tokens, layout, refresh, links |
|
|
123
|
+
| `usePushRegistration({ topics })` / `usePushTopics()` / `useNotificationTaps()` | push notifications (see Push notifications) |
|
|
124
|
+
| `useAcms()` | the typed client (e.g. `calendars()`) for your own `useQuery` |
|
|
125
|
+
|
|
126
|
+
Components: `<Sections>`, `<RichText html>`, `<EventDate start end allDay timezone>`,
|
|
127
|
+
`<Button>`, `<ErrorView error retry>`, `<GenericSection>`. Full reference:
|
|
128
|
+
`@acms-subir/native` README; API contract: the site's `docs/mobile-api-v1.md`
|
|
129
|
+
and `<site>/api/v1/openapi.json`.
|
|
130
|
+
|
|
131
|
+
## Recipes
|
|
132
|
+
|
|
133
|
+
Ready-made screens; add them instead of writing from scratch:
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
npx acms mobile recipe list
|
|
137
|
+
npx acms mobile recipe add events # list, detail, add to calendar (expo-calendar)
|
|
138
|
+
npx acms mobile recipe add news # posts list, article
|
|
139
|
+
npx acms mobile recipe add booking # resources → slots → form → confirmation (expo-crypto)
|
|
140
|
+
npx acms mobile recipe add push # notifications: turn on, topic toggles, taps open the page (expo-notifications, expo-device)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Each prints the `npx expo install …` line for its packages: run it (doctor
|
|
144
|
+
fails when a recipe's packages are missing). Recipes never overwrite files.
|
|
145
|
+
|
|
146
|
+
## Common tasks
|
|
147
|
+
|
|
148
|
+
**Add a tab.** The starter is a Stack. Move the home screen into a tab group.
|
|
149
|
+
On Expo SDK 57+ import `Tabs` from `expo-router/js-tabs` (the `expo-router`
|
|
150
|
+
export is deprecated):
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
app/_layout.tsx Stack: <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
|
|
154
|
+
app/(tabs)/_layout.tsx <Tabs screenOptions={{ tabBarActiveTintColor: t.colors.accent,
|
|
155
|
+
tabBarInactiveTintColor: t.colors.muted,
|
|
156
|
+
tabBarStyle: { backgroundColor: t.colors.bg, borderTopColor: t.colors.line },
|
|
157
|
+
headerStyle: { backgroundColor: t.colors.bg }, headerTintColor: t.colors.ink,
|
|
158
|
+
headerRight: () => <LanguageSwitch /> }}>
|
|
159
|
+
app/(tabs)/index.tsx the old app/index.tsx (delete the original; one route per path)
|
|
160
|
+
app/(tabs)/giving.tsx the new tab
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Keep detail screens (e.g. `app/events/[slug].tsx`) outside `(tabs)` so they
|
|
164
|
+
push over the tabs. The header title moves to the tab screen
|
|
165
|
+
(`options={{ headerTitle: site?.name }}`). Tab icons: `react-native-svg` is
|
|
166
|
+
already installed (draw small `Path` icons), or run
|
|
167
|
+
`npx expo install @expo/vector-icons` first and use
|
|
168
|
+
`tabBarIcon: ({ color, size }) => <Ionicons name="heart-outline" color={color} size={size} />`.
|
|
169
|
+
To open the tab from site links, map its path in `onNavigate`
|
|
170
|
+
(`router.navigate('/giving')`).
|
|
171
|
+
|
|
172
|
+
**A tab that shows an owner-editable page** (giving, visit, about). Ask the
|
|
173
|
+
owner for (or check for) a page with that slug; render it whichever way it is
|
|
174
|
+
built, and explain what's missing when it isn't there:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
export default function Giving() {
|
|
178
|
+
const t = useTheme();
|
|
179
|
+
const page = usePage('giving');
|
|
180
|
+
const { refreshing, refresh } = useRefresh();
|
|
181
|
+
if (page.isPending) return <ActivityIndicator style={{ marginTop: 48 }} color={t.colors.muted} />;
|
|
182
|
+
if (page.error instanceof AcmsError && page.error.status === 404) return <MissingPage slug="giving" />; // your empty state
|
|
183
|
+
if (page.error) return <ErrorView error={page.error} retry={() => page.refetch()} />;
|
|
184
|
+
return (
|
|
185
|
+
<ScrollView style={{ backgroundColor: t.colors.bg }} refreshControl={<RefreshControl refreshing={refreshing} onRefresh={refresh} />}>
|
|
186
|
+
{page.data.mode === 'sections'
|
|
187
|
+
? <Sections page="giving" registry={sections} />
|
|
188
|
+
: <View style={{ padding: t.styles.space(5) }}><RichText html={page.data.body.html} /></View>}
|
|
189
|
+
</ScrollView>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`AcmsError` (from `@acms-subir/client`) has `status` and `code`; show a
|
|
195
|
+
friendly note for `404` rather than a raw error (`isAcmsError(e)` narrows it).
|
|
196
|
+
Fixed chrome (the tab title "Giving") is fine in code; amounts, fund names,
|
|
197
|
+
bank details and links are not.
|
|
198
|
+
|
|
199
|
+
**Show the next 3 events of one calendar on home.** Calendars are addressed
|
|
200
|
+
by slug (`useAcms().calendars()` lists public ones; the site's
|
|
201
|
+
`/api/v1/calendars`). Keep the slug in one named constant, and render the
|
|
202
|
+
page's own sections below it. The events window starts at **midnight UTC
|
|
203
|
+
today** and `from` takes only a date, so the response can include occurrences
|
|
204
|
+
that already ended: ask for a few extra and filter by `end`:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
const YOUTH_CALENDAR = 'youth'; // a calendar slug from the site, not content
|
|
208
|
+
const day = (offset: number) => new Date(Date.now() + offset * 86_400_000).toISOString().slice(0, 10);
|
|
209
|
+
|
|
210
|
+
function NextEvents() {
|
|
211
|
+
const t = useTheme();
|
|
212
|
+
const open = useOpenLink();
|
|
213
|
+
const to = useMemo(() => day(90), []); // the API's maximum window
|
|
214
|
+
const events = useEvents({ calendar: YOUTH_CALENDAR, to, limit: 8 });
|
|
215
|
+
const next = (events.data?.occurrences ?? []).filter((o) => Date.parse(o.end || o.start) > Date.now()).slice(0, 3);
|
|
216
|
+
// loading / error (404 = calendar renamed or private) / empty states …
|
|
217
|
+
return next.map((o) => (
|
|
218
|
+
<Pressable key={o.id} onPress={() => open(o.event.url)} style={{ minHeight: 44 }} accessibilityRole="link">
|
|
219
|
+
<Text style={t.styles.text.heading}>{o.event.title}</Text>
|
|
220
|
+
<EventDate start={o.start} end={o.end} allDay={o.allDay} timezone={o.event.timezone} />
|
|
221
|
+
</Pressable>
|
|
222
|
+
));
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
A weekly event may fill all three slots; that is correct. If the owner should
|
|
227
|
+
choose the calendar, better still: they add an **Upcoming events** section
|
|
228
|
+
(its events source has a calendar and a limit) to the home page in the admin,
|
|
229
|
+
and `<Sections>` renders it with no app code at all. Say so in your summary.
|
|
230
|
+
|
|
231
|
+
**A native version of a coded section.** From the site folder:
|
|
232
|
+
`npx acms section new visit-banner --theme <slug>` (new) or, for an existing
|
|
233
|
+
coded section, write `mobile/sections/VisitBanner.tsx` typed with
|
|
234
|
+
`SectionComponentProps<VisitBannerData>` from `./types` and register it in
|
|
235
|
+
`sections/index.ts` (`'visit-banner': VisitBanner`). Run `npx acms check` to
|
|
236
|
+
refresh `sections/types.ts`.
|
|
237
|
+
|
|
238
|
+
**Translate.** Nothing to translate in the app beyond chrome: content
|
|
239
|
+
languages come from the site (`useLocale`, the `LanguageSwitch`). The owner
|
|
240
|
+
translates in the admin. For chrome strings, keep a tiny `{ en, es }` map
|
|
241
|
+
keyed by `locale`.
|
|
242
|
+
|
|
243
|
+
**Add a booking flow.** `npx acms mobile recipe add booking` then
|
|
244
|
+
`npx expo install expo-crypto`; bookings must be enabled on the site
|
|
245
|
+
(`useSite().data.features.bookings`) with a public booking link. Link to it:
|
|
246
|
+
`<Link href="/book">`.
|
|
247
|
+
|
|
248
|
+
## Push notifications
|
|
249
|
+
|
|
250
|
+
Optional; the owner sends them from **Admin → Mobile app** (manual messages
|
|
251
|
+
plus automatic ones: new post, new event, "starting soon"). The app only
|
|
252
|
+
registers and chooses topics; it never sends.
|
|
253
|
+
|
|
254
|
+
1. Site: `acms({ mobile: { push: true } })` in `astro.config.mjs` (off by
|
|
255
|
+
default; `/api/v1/devices` and `/api/v1/push/topics` 404 until then).
|
|
256
|
+
Optional secret `EXPO_ACCESS_TOKEN` when "enhanced push security" is on for
|
|
257
|
+
the Expo project.
|
|
258
|
+
2. App: `npx acms mobile recipe add push`, `npx expo install expo-notifications expo-device`,
|
|
259
|
+
add `"expo-notifications"` to `plugins` in app.json, `npx eas-cli init`
|
|
260
|
+
(push tokens need `extra.eas.projectId`), and mount `<PushSetup />` inside
|
|
261
|
+
`<AcmsProvider>` in `app/_layout.tsx`. Link to `/notifications`.
|
|
262
|
+
3. Taps carry `data.url` / `data.path` and go through the provider's
|
|
263
|
+
`onNavigate`, so route `/events/<slug>` and post paths to your screens there.
|
|
264
|
+
|
|
265
|
+
Rules: ask for permission from a button after saying why (the recipe does),
|
|
266
|
+
never on first launch. Topic ids and names come from the site
|
|
267
|
+
(`usePushTopics()`); don't hardcode labels. `usePushRegistration` re-registers
|
|
268
|
+
quietly each start and sends the app's language, so notices arrive in the
|
|
269
|
+
reader's language when a translation exists. Test on a real device (simulators
|
|
270
|
+
get no push); doctor checks the plugin, the project id and that the site has
|
|
271
|
+
push on.
|
|
272
|
+
|
|
273
|
+
## Editor apps (staff sign-in)
|
|
274
|
+
|
|
275
|
+
A second, separate Expo app for the people who run the site: owners and
|
|
276
|
+
other staff write posts, edit section text, review translations, upload media and
|
|
277
|
+
answer bookings from their phone. It signs in through the site (OAuth 2.0 +
|
|
278
|
+
PKCE in the system browser, never a password in the app) and uses the
|
|
279
|
+
bearer-only `/api/v1/admin/*` API (docs/mobile-api-v1.md → Editor apps). We
|
|
280
|
+
ship building blocks, a blank signed-in shell and optional recipes; the app
|
|
281
|
+
itself is the owner's to shape.
|
|
282
|
+
|
|
283
|
+
### Start
|
|
284
|
+
|
|
285
|
+
```sh
|
|
286
|
+
npx acms add mobile editor --url https://example.com # → mobile-editor/
|
|
287
|
+
cd mobile-editor && npm install && cd ..
|
|
288
|
+
npx acms mobile recipe add editor-content # and more, below
|
|
289
|
+
npx acms mobile dev --dir mobile-editor
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`add mobile editor` picks a reverse-DNS bundle id (`org.example.staff`), uses
|
|
293
|
+
it lowercased as the app's scheme, and its sign-in return address is
|
|
294
|
+
`<scheme>://auth`. It adds that address to
|
|
295
|
+
`acms({ mobile: { editor: { name, redirectUris } } })` in `astro.config` when
|
|
296
|
+
the file has a shape it can edit safely (`acms()`, `acms({ … })` without a
|
|
297
|
+
`mobile` key, a `mobile: {…}` without `editor`, or an existing
|
|
298
|
+
`redirectUris: [...]`); otherwise it prints the exact snippet (`--no-config`
|
|
299
|
+
never touches the file). Restart or deploy the site afterwards. Other
|
|
300
|
+
addresses to list:
|
|
301
|
+
|
|
302
|
+
- the web build: `https://<where it is hosted>/auth`; `http://localhost/auth`
|
|
303
|
+
covers local development on any port (loopback rule);
|
|
304
|
+
- Expo Go while testing: `exp://<lan ip>:8081/--/auth` (remove before release).
|
|
305
|
+
|
|
306
|
+
`create-acms-subir --mobile editor` (or `both`, `client,editor`) does the same
|
|
307
|
+
while scaffolding.
|
|
308
|
+
|
|
309
|
+
### The starter
|
|
310
|
+
|
|
311
|
+
- `app/_layout.tsx`: `<EditorProvider baseUrl clientId redirectUri>` and the
|
|
312
|
+
signed-out / signed-in switch (`useAuth().status`).
|
|
313
|
+
- `components/SignIn.tsx`: site name and logo from `/api/v1/site`, "Sign in
|
|
314
|
+
with ‹site›", errors and "you were signed out" notices.
|
|
315
|
+
- `app/index.tsx`: Home — "Signed in as … · role", the app's tools (one file
|
|
316
|
+
per entry in `tools/`), Light / Dark / Auto, sign out.
|
|
317
|
+
- `app/auth.tsx`: the return-address route; `completeAuthRedirect()` lets the
|
|
318
|
+
web sign-in popup hand back its result.
|
|
319
|
+
- `components/Screen.tsx`: `Screen`, `SplitView` (list + detail on ≥ 768),
|
|
320
|
+
`useWide`, `HeaderActions`; `components/TextTabs.tsx`; `lib/format.ts`.
|
|
321
|
+
|
|
322
|
+
The look is the admin's Inset Paper (light and dark): `useEditorTheme()`
|
|
323
|
+
(`colors.panel/raised/sel/line/text/muted/accent/green/blue/amber/red`,
|
|
324
|
+
`text.screenTitle/title/row/body/meta/label/section`, `radius`). Change tokens
|
|
325
|
+
with `<EditorProvider theme={{ colors: { light: { accent: '#0a7' } } }}>`.
|
|
326
|
+
|
|
327
|
+
### `@acms-subir/native/editor`
|
|
328
|
+
|
|
329
|
+
No hard dependencies. Optional peers, loaded only if installed (a missing one
|
|
330
|
+
throws a message naming the package): `expo-secure-store` (tokens),
|
|
331
|
+
`expo-web-browser` (sign-in), `expo-crypto` (PKCE; Web Crypto otherwise),
|
|
332
|
+
`expo-image-picker` (uploads), `expo-device` (device name).
|
|
333
|
+
|
|
334
|
+
`<EditorProvider>` props: `baseUrl`, `clientId`, `redirectUri` (use
|
|
335
|
+
`redirectUriFor(scheme)`: `<scheme>://auth` on devices, `<origin>/auth` on the
|
|
336
|
+
web), `storage` (default: expo-secure-store with
|
|
337
|
+
`WHEN_UNLOCKED_THIS_DEVICE_ONLY`; sessionStorage on the web; or
|
|
338
|
+
`memoryTokenStorage()` / your own `{ get, set }`), `storageKey`,
|
|
339
|
+
`openAuthSession`, `crypto`, `deviceName`, `queryClient` (default: the
|
|
340
|
+
surrounding one or a new one), `colorScheme`, `theme`, `fetch`. It refreshes
|
|
341
|
+
tokens before they expire (single flight), signs out locally when a refresh is
|
|
342
|
+
rejected or a request is still 401 after a refresh, and drops all cached admin
|
|
343
|
+
data on sign-out.
|
|
344
|
+
|
|
345
|
+
| Hook | Gives you |
|
|
346
|
+
| --- | --- |
|
|
347
|
+
| `useAuth()` | `status` (`loading`/`signedOut`/`signingIn`/`signedIn`), `user`, `role`, `device`, `error`, `signedOutReason`, `signIn()`, `signOut()` |
|
|
348
|
+
| `useSiteInfo()` / `useMe()` | public site name + logo / the person, site features and `permissions` |
|
|
349
|
+
| `useDocuments(query)` | infinite list (`kind`, `status`, `collection`, `q`); flat `items`, `total` |
|
|
350
|
+
| `useCreateDocument()` | mutation: `{ kind, title, collection? \| start? }` |
|
|
351
|
+
| `useDocument(kind, id)` | `document`, `save(fields)` (sends the loaded revision), `saving`, `conflict` (`server`, `mine`), `keepMine()`, `keepTheirs()` |
|
|
352
|
+
| `usePublish(kind, id)` | `publish()` (Update for live docs with staged edits), `unpublish()`, `schedule(at)`, `discard()`, `action` |
|
|
353
|
+
| `useSectionLayouts({ stage, pageId })` | pages with sections, definitions, revision, role |
|
|
354
|
+
| `useSectionEditor(pageId, sectionId)` | `data`, `setField`, `dirty`, `issues`, `permissions`, `canEdit(key, sub?)`, `save()` (only changed fields), `publish()` (this section), `conflict`/`keepMine`/`keepTheirs`, `reset()` |
|
|
355
|
+
| `useTranslations(target, locale)` | `units` (source, value, `missing`/`ai`/`reviewed`/`outdated`), `summary`, `set(values)`, `markReviewed(paths?)`, `aiDraft(paths?)` |
|
|
356
|
+
| `useTranslationQueue(locale)` | sections + newest documents that still need a person |
|
|
357
|
+
| `useMedia(query)` | infinite library, `upload(file)`, `pickAndUpload({ source, quality, aspect })` |
|
|
358
|
+
| `useBookingsInbox({ status })` | infinite inbox, `pending`, `approve/decline/cancel(id, message?)` |
|
|
359
|
+
| `useEditorClient()` | the typed bearer client for anything else |
|
|
360
|
+
|
|
361
|
+
`target` is `{ kind, id }` for a post/page/event or `{ pageId, sectionId }`.
|
|
362
|
+
Errors: `errorMessage(e)`, `isConflict(e)` (409), `isForbidden(e)` (403).
|
|
363
|
+
|
|
364
|
+
Section saves are guarded by the design revision, which changes whenever
|
|
365
|
+
anyone saves anything. `useSectionEditor` re-applies your changed fields on
|
|
366
|
+
the latest copy automatically; only when someone changed the **same** fields
|
|
367
|
+
does `conflict` ask. Documents never auto-merge.
|
|
368
|
+
|
|
369
|
+
**Forms.** `<FieldInput field value onChange siblings role readOnly>` renders
|
|
370
|
+
any section field type — `text`, `textarea`, `richtext`, `url`, `number`,
|
|
371
|
+
`toggle`, `select` (option buttons, a sheet over 8 options), `date`, `time`,
|
|
372
|
+
`link` (label, address, "Pick" a page/post/event → `ref`), `image` (media
|
|
373
|
+
library or upload, tap to set the focal point `<key>__focus`), `list` (add,
|
|
374
|
+
remove, reorder rows; `_id`s kept so translations follow rows), `source`
|
|
375
|
+
(calendar / collection / resource from the site, tag, how many) — with the
|
|
376
|
+
label, `required` marker, `maxLength` counter, help text, validation after
|
|
377
|
+
blur, `visibleIf`, and read-only for editors when a field is `locked`.
|
|
378
|
+
`<SectionFields definition data onChange permissions role>` renders a whole
|
|
379
|
+
section. The rules are the server's (a dependency-free copy of
|
|
380
|
+
`theme-sdk/fields.mjs` with a drift test): `normalizeData`, `valueIssues`,
|
|
381
|
+
`fieldVisible`, `formIssues`, `sectionPermissions`, `translationUnits`, …
|
|
382
|
+
|
|
383
|
+
**Rich text** edits "markdown-lite" and stores a safe HTML subset
|
|
384
|
+
(`<p> <br> <strong> <em> <a href> <ul> <ol> <li> <h2> <h3> <blockquote>`):
|
|
385
|
+
`**bold**`, `*italic*`, `[label](https://…)`, `- ` bullets, `1. ` numbers,
|
|
386
|
+
`## ` headings, `> ` quotes, with a B / I / Link / List / H toolbar. HTML with
|
|
387
|
+
anything richer (images, embeds, cards, tables, classes, nested lists) opens
|
|
388
|
+
read-only with "Edit this on the website" (`richTextSupport(html)`), so the
|
|
389
|
+
phone never silently drops content.
|
|
390
|
+
|
|
391
|
+
Building blocks: `EditorButton`, `EditorInput`, `Segmented`, `Notice`,
|
|
392
|
+
`StatusText`, `SectionLabel`, `ListRow`, `Card`, `Sheet`, `OptionGrid`,
|
|
393
|
+
`MediaPicker`, `DocumentPicker`.
|
|
394
|
+
|
|
395
|
+
### Cookbook (editor apps)
|
|
396
|
+
|
|
397
|
+
**A screen of your own** (add `tools/notices.ts` so Home lists it):
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
// app/notices.tsx: edit one section's "notice" text from Home
|
|
401
|
+
import { EditorButton, FieldInput, Notice, errorMessage, useAuth, useSectionEditor } from '@acms-subir/native/editor';
|
|
402
|
+
import { Screen } from '../components/Screen';
|
|
403
|
+
|
|
404
|
+
export default function Notices() {
|
|
405
|
+
const { role } = useAuth();
|
|
406
|
+
const ed = useSectionEditor(PAGE_ID, SECTION_ID); // from useSectionLayouts(), never hardcoded content
|
|
407
|
+
if (!ed.definition) return null;
|
|
408
|
+
return (
|
|
409
|
+
<Screen>
|
|
410
|
+
<FieldInput field={ed.definition.fields.text} value={ed.data.text} onChange={(v) => ed.setField('text', v)}
|
|
411
|
+
role={role ?? undefined} readOnly={!ed.canEdit('text')} />
|
|
412
|
+
{!!ed.saveError && <Notice tone="error">{errorMessage(ed.saveError)}</Notice>}
|
|
413
|
+
<EditorButton variant="primary" label="Save and publish" busy={ed.saving || ed.publishing} onPress={() => ed.publish()} />
|
|
414
|
+
</Screen>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Only owners see a button:** `const { data: me } = useMe(); me?.permissions.sections.editLocked && <EditorButton … />`.
|
|
420
|
+
|
|
421
|
+
**A data call the hooks don't cover:** `const api = useEditorClient();`
|
|
422
|
+
then `useQuery({ queryKey: editorKey(baseUrl, user?.id, 'bookings-week'), queryFn: ({ signal }) => api.listBookings({ from }, { signal }) })`;
|
|
423
|
+
keep keys under `editorKey(...)` so sign-out clears them.
|
|
424
|
+
|
|
425
|
+
**Test sign-in on the web:** list `http://localhost/auth` in `redirectUris`,
|
|
426
|
+
`npx expo start --web` in the app, sign in in the popup.
|
|
427
|
+
|
|
428
|
+
### Recipes (editor apps)
|
|
429
|
+
|
|
430
|
+
```sh
|
|
431
|
+
npx acms mobile recipe add editor-content # list, kind/status filters, search, publish/unpublish/discard
|
|
432
|
+
npx acms mobile recipe add editor-writer # new post/page/event, title/summary/body/tags/address, save, publish, schedule, conflicts
|
|
433
|
+
npx acms mobile recipe add editor-sections # page → section → form → save → publish this section (expo-image-picker)
|
|
434
|
+
npx acms mobile recipe add editor-translations # "Review Español": missing / AI drafts / outdated; edit beside the original, approve, AI draft
|
|
435
|
+
npx acms mobile recipe add editor-media # grid + upload from library or camera (expo-image-picker)
|
|
436
|
+
npx acms mobile recipe add editor-bookings # inbox: approve / decline / cancel with a message
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
Recipes know their app kind: client recipes refuse editor apps and the other
|
|
440
|
+
way round; with both apps in the site, `recipe add` picks the matching one.
|
|
441
|
+
Each adds a `tools/<name>.ts` entry, so it shows on Home. Tablets get list +
|
|
442
|
+
detail side by side. Push for editors ("new booking", "draft to review") is
|
|
443
|
+
not supported yet: the push API registers anonymous visitor devices only.
|
|
444
|
+
|
|
445
|
+
### Rules for editor apps
|
|
446
|
+
|
|
447
|
+
- Never ask for or store a password; never put the owner password, API keys
|
|
448
|
+
or agent keys in an app. Sign-in is `useAuth().signIn()`.
|
|
449
|
+
- Tokens live only in secure storage through `<EditorProvider>` (never
|
|
450
|
+
AsyncStorage, files, logs, analytics or crash reports; only the
|
|
451
|
+
`Authorization` header). Don't read them into React state.
|
|
452
|
+
- Hide what the role can't do (`useMe().data.permissions`, `canEdit`,
|
|
453
|
+
`permissions.canEditRows`), but always let the server decide; show 403 as
|
|
454
|
+
"your role can't do that".
|
|
455
|
+
- Every write sends the revision you loaded; on 409 show the conflict and let
|
|
456
|
+
the person choose — never retry blindly over someone's work.
|
|
457
|
+
- Same design rules as the client app: no pill shapes (8–12px rectangles,
|
|
458
|
+
text tabs, marker + text), no colored left-edge stripes, no gradient-squircle
|
|
459
|
+
logos; phones and tablets (≥ 768 uses the width).
|
|
460
|
+
|
|
461
|
+
### Without the starter
|
|
462
|
+
|
|
463
|
+
`@acms-subir/client` alone: `createEditorAuth` (authorize URL, `parseRedirect`
|
|
464
|
+
with the state check, code exchange, refresh, revoke), `createEditorSession`
|
|
465
|
+
(storage adapter, single-flight refresh) and `createEditorClient`. Pass
|
|
466
|
+
expo-crypto as `crypto` when Web Crypto isn't available.
|
|
467
|
+
|
|
468
|
+
`acms mobile doctor` checks editor apps too: expo-secure-store,
|
|
469
|
+
expo-web-browser and expo-crypto installed; app.json's scheme gives a return
|
|
470
|
+
address the site lists (from `.astro/acms-project.json`) and the running site
|
|
471
|
+
has editor apps on; no tokens in AsyncStorage, no logged tokens, no password
|
|
472
|
+
fields.
|
|
473
|
+
|
|
474
|
+
## Deep links (universal / app links)
|
|
475
|
+
|
|
476
|
+
1. Site: `acms({ mobile: { ios: { teamId, bundleId, paths }, android: { package, sha256CertFingerprints } } })`
|
|
477
|
+
in `astro.config.mjs`. The site then serves
|
|
478
|
+
`/.well-known/apple-app-site-association` and `/.well-known/assetlinks.json`.
|
|
479
|
+
`bundleId` / `package` must equal `app.json`. The Android fingerprint comes
|
|
480
|
+
from `npx eas-cli credentials` (the release signing key).
|
|
481
|
+
2. App (`app.json`): `ios.associatedDomains: ["applinks:example.com"]`;
|
|
482
|
+
`android.intentFilters: [{ "action": "VIEW", "autoVerify": true, "data": [{ "scheme": "https", "host": "example.com" }], "category": ["BROWSABLE", "DEFAULT"] }]`;
|
|
483
|
+
`scheme` for `myapp://` links (the starter sets it).
|
|
484
|
+
3. Expo Router opens `https://example.com/events/x` at the route
|
|
485
|
+
`app/events/[slug].tsx` when the file path matches. Map other site paths in
|
|
486
|
+
`app/+native-intent.tsx` (`export function redirectSystemPath({ path })`).
|
|
487
|
+
|
|
488
|
+
Doctor checks that ids match and the files serve.
|
|
489
|
+
|
|
490
|
+
## Test
|
|
491
|
+
|
|
492
|
+
- iOS simulator, Android emulator and `npm run web` (web preview), light and
|
|
493
|
+
dark, phone and tablet sizes. Compare with the website: same sections,
|
|
494
|
+
same order, same words.
|
|
495
|
+
- A real phone: `npx acms mobile dev` (same Wi-Fi or Tailscale).
|
|
496
|
+
- `npx tsc --noEmit` in the app; `npx expo export --platform web` bundles.
|
|
497
|
+
- `npx acms mobile doctor` (add `--offline` without network).
|
|
498
|
+
|
|
499
|
+
## Release (EAS)
|
|
500
|
+
|
|
501
|
+
```sh
|
|
502
|
+
cd mobile
|
|
503
|
+
npx eas-cli init # links an EAS project
|
|
504
|
+
npx eas-cli build --profile preview --platform all # installable test builds
|
|
505
|
+
npx eas-cli build --profile production --platform all
|
|
506
|
+
npx eas-cli submit --platform ios # and android
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Store checklist: real `ios.bundleIdentifier` / `android.package` (can't change
|
|
510
|
+
after release; doctor warns on `com.example.*`); app name, `icon` (1024×1024)
|
|
511
|
+
and splash in `app.json`; `version` and build numbers bumped; `.env` points at
|
|
512
|
+
the **https** production site; privacy policy URL; screenshots for phone and
|
|
513
|
+
tablet; deep-link config deployed on the site; a production build tested on
|
|
514
|
+
real devices; `acms mobile doctor` clean.
|
|
515
|
+
|
|
516
|
+
## Doctor
|
|
517
|
+
|
|
518
|
+
`npx acms mobile doctor [--dir mobile] [--url https://site] [--offline] [--json]`
|
|
519
|
+
checks every app (or `--dir`): packages and versions compatible with the
|
|
520
|
+
site's core, Expo SDK; the site URL is set, reachable from a phone and on the
|
|
521
|
+
same API version; app ids and scheme; deep-link files; sections on the site's
|
|
522
|
+
pages the app can't render; pages and calendars the app reads by slug that
|
|
523
|
+
the site doesn't publish; secrets in source or env files; recipe packages
|
|
524
|
+
(and for push: the `expo-notifications` plugin, `extra.eas.projectId`, and
|
|
525
|
+
push turned on for the site).
|
|
526
|
+
Exit 1 when anything fails. Network checks become warnings when the site is
|
|
527
|
+
unreachable; `--offline` skips them.
|