diff --git a/CHANGELOG.json b/CHANGELOG.json index 2471975..fb88222 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -6,6 +6,53 @@ "commitConvention": "conventional", "maintainers": ["grokify", "johncwang@gmail.com"], "releases": [ + { + "version": "v0.5.0", + "date": "2026-10-26", + "highlights": [ + { + "description": "`@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people", + "commit": "bd2c35a" + } + ], + "added": [ + { + "description": "`createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies", + "commit": "597b1d1" + }, + { + "description": "`InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke", + "commit": "bd2c35a" + }, + { + "description": "`InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones", + "commit": "39ff502" + } + ], + "tests": [ + { + "description": "Cover the invite API client and both pages", + "commit": "36d10c1" + }, + { + "description": "Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies", + "commit": "6ceeb55" + } + ], + "documentation": [ + { + "description": "`@plexusone/invites` package page", + "commit": "68d625f" + }, + { + "description": "Doc comments on the package's components and helpers", + "commit": "1685338" + }, + { + "description": "Adding invite pages guide, README section, and v0.5.0 release notes" + } + ] + }, { "version": "v0.4.0", "date": "2026-10-20", @@ -483,28 +530,5 @@ } ] } - ], - "unreleased": { - "highlights": [ - { - "description": "`@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people" - } - ], - "added": [ - { - "description": "`createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`" - }, - { - "description": "`InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke" - }, - { - "description": "`InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones" - } - ], - "documentation": [ - { - "description": "`@plexusone/invites` package page" - } - ] - } + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c439d5..67eea10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,19 +9,28 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro ## [Unreleased] +## [v0.5.0] - 2026-10-26 + ### Highlights -- `@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people +- `@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people ### Added -- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError` -- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke -- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones +- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies ([`597b1d1`](https://github.com/plexusone/systemforge-web/commit/597b1d1)) +- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke ([`bd2c35a`](https://github.com/plexusone/systemforge-web/commit/bd2c35a)) +- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones ([`39ff502`](https://github.com/plexusone/systemforge-web/commit/39ff502)) ### Documentation -- `@plexusone/invites` package page +- `@plexusone/invites` package page ([`68d625f`](https://github.com/plexusone/systemforge-web/commit/68d625f)) +- Doc comments on the package's components and helpers ([`1685338`](https://github.com/plexusone/systemforge-web/commit/1685338)) +- Adding invite pages guide, README section, and v0.5.0 release notes + +### Tests + +- Cover the invite API client and both pages ([`36d10c1`](https://github.com/plexusone/systemforge-web/commit/36d10c1)) +- Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies ([`6ceeb55`](https://github.com/plexusone/systemforge-web/commit/6ceeb55)) ## [v0.4.0] - 2026-10-20 @@ -246,7 +255,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro - Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d)) -[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...HEAD +[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...HEAD +[v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0 [v0.3.1]: https://github.com/plexusone/systemforge-web/compare/v0.3.0...v0.3.1 [v0.3.0]: https://github.com/plexusone/systemforge-web/compare/v0.2.3...v0.3.0 diff --git a/README.md b/README.md index f79bdbb..391b673 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,23 @@ function App() { } ``` +## Invite-gated sign-up + +`@plexusone/invites` adds the pages for launching invite-only with +SystemForge: operators issue invite codes and email invites and work through +the waitlist, and members invite people from their allowance. + +```tsx +import { createInvitesApi, InviteAdminPage, InvitePeoplePage } from '@plexusone/invites'; + +const invites = createInvitesApi('/invites/api'); + +} /> +} /> +``` + +See [Adding invite pages](docs/guides/invite-gated-signup.md). + ## Telemetry with ProductGraph Track user behavior, journeys, and performance with ProductGraph integration: diff --git a/docs/changelog.md b/docs/changelog.md index 4c439d5..67eea10 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,19 +9,28 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro ## [Unreleased] +## [v0.5.0] - 2026-10-26 + ### Highlights -- `@plexusone/invites`: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people +- `@plexusone/invites` 0.1.0: pages for SystemForge invite-gated sign-up, an operator page for invite codes, email invites, the waitlist and member allowances, and a member page to invite people ### Added -- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError` -- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke -- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones +- `createInvitesApi`: a typed client for SystemForge's invite API, with snake_case wire types and `ApiError`; requests carry the browser's cookies ([`597b1d1`](https://github.com/plexusone/systemforge-web/commit/597b1d1)) +- `InviteAdminPage`: codes, email invites, waitlist, allowances and joined accounts, with new codes and links shown once and inline revoke ([`bd2c35a`](https://github.com/plexusone/systemforge-web/commit/bd2c35a)) +- `InvitePeoplePage`: a member's allowance, create and copy an invite link, and revoke unused ones ([`39ff502`](https://github.com/plexusone/systemforge-web/commit/39ff502)) ### Documentation -- `@plexusone/invites` package page +- `@plexusone/invites` package page ([`68d625f`](https://github.com/plexusone/systemforge-web/commit/68d625f)) +- Doc comments on the package's components and helpers ([`1685338`](https://github.com/plexusone/systemforge-web/commit/1685338)) +- Adding invite pages guide, README section, and v0.5.0 release notes + +### Tests + +- Cover the invite API client and both pages ([`36d10c1`](https://github.com/plexusone/systemforge-web/commit/36d10c1)) +- Cover a new link surviving a failed reload, the copy note following the code, removed waitlist addresses leaving the selection, and requests sending cookies ([`6ceeb55`](https://github.com/plexusone/systemforge-web/commit/6ceeb55)) ## [v0.4.0] - 2026-10-20 @@ -246,7 +255,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro - Shared pnpm workflows from grokify/.github ([`bb07c1d`](https://github.com/plexusone/systemforge-web/commit/bb07c1d)) -[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...HEAD +[unreleased]: https://github.com/plexusone/systemforge-web/compare/v0.5.0...HEAD +[v0.5.0]: https://github.com/plexusone/systemforge-web/compare/v0.4.0...v0.5.0 [v0.4.0]: https://github.com/plexusone/systemforge-web/compare/v0.3.1...v0.4.0 [v0.3.1]: https://github.com/plexusone/systemforge-web/compare/v0.3.0...v0.3.1 [v0.3.0]: https://github.com/plexusone/systemforge-web/compare/v0.2.3...v0.3.0 diff --git a/docs/guides/invite-gated-signup.md b/docs/guides/invite-gated-signup.md new file mode 100644 index 0000000..6474fba --- /dev/null +++ b/docs/guides/invite-gated-signup.md @@ -0,0 +1,115 @@ +# Adding invite pages + +An application that launches invite-only, with SystemForge's invite-gated +sign-up, needs two pages: one for operators to issue invites and work through +the waitlist, and one for members to invite people from their allowance. +`@plexusone/invites` provides both. This guide wires them into an +application; SystemForge's +[invite-gated sign-up guide](https://plexusone.github.io/systemforge/guides/invite-gated-signup/) +covers turning it on in SystemAuth and what an invited person sees. + +## 1. Choose where the invite API runs + +The pages call SystemForge's invite API with the browser's cookies, so the +API must accept the session of the person using the page. There are two +places to serve it: + +| Where | Base URL | Use when | +| ---------------------------------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| **Behind the application's own session** | A path on the application, such as `/invites/api` | The application embeds SystemAuth and has its own sign-in. The pages stay same-origin, so cookies just work | +| **On the SystemAuth host** | `https://auth.example.com/signup-invites/api` | The person is signed in to SystemAuth itself (`Options.SignupAPI`) | + +Serving the API behind the application's session is the usual choice. In +the host binary, `Service.SignupAPI` takes a function that names the person +signed in to the application: + +```go +api, err := svc.SignupAPI(func(r *http.Request) (uuid.UUID, error) { + return mySession.PrincipalID(r) +}) +mux.Handle("/invites/api/", http.StripPrefix("/invites/api", api)) +``` + +On the SystemAuth host, the page and the API are on different origins: the +API must allow credentialed requests from the application's origin, and the +SystemAuth session cookie must reach it. + +## 2. Install and add the styles + +```bash +pnpm add @plexusone/invites @plexusone/ui +``` + +The pages use Tailwind classes with `@plexusone/ui`'s theme names. Set up +Tailwind as in [@plexusone/ui](../packages/ui.md), then add the package's +built files to Tailwind's sources so their classes are generated: + +```css +@source "../node_modules/@plexusone/invites/dist"; +``` + +## 3. Route the pages + +Create one client and route both pages where they belong in the +application. They are plain components, not an `AppModule`, because invites +are platform-wide rather than organization-scoped. + +```tsx +import { createInvitesApi, InviteAdminPage, InvitePeoplePage } from '@plexusone/invites'; + +const invites = createInvitesApi('/invites/api'); + +} /> +} /> +``` + +`InviteAdminPage` is for platform administrators. Anyone else who reaches it +sees the API's refusal, so the route needs no guard of its own, but the menu +should not offer it to them. + +## 4. Show the menu links to the right people + +`settings()` says whether the person is an administrator, and `myInvites()` +gives their allowance. Offer **Invites** to administrators, and **Invite +people** to members who have an allowance: + +```tsx +const [links, setLinks] = useState<{ admin: boolean; invite: boolean }>(); + +useEffect(() => { + void Promise.all([invites.settings(), invites.myInvites()]).then(([s, mine]) => + setLinks({ admin: s.is_admin, invite: mine.allowance > 0 }) + ); +}, []); +``` + +A member with no allowance can still open `/invite`; the page tells them they +have no invites to give yet. + +## 5. Point the sign-in page at the waitlist + +A refused sign-up is not told why. Link the application's sign-in page to +SystemAuth's waitlist, `https://auth.example.com/signup/waitlist`, so a +person without an invite has somewhere to go, and link it from any public +page that announces the launch. + +## What the pages do + +- **Invite people** shows how many people the member can still invite, + creates an invite link and shows it once with a copy button, and revokes a + link nobody has used, which gives the slot back. +- **Invites** has a section each for invite codes, email invites, the + waitlist, member allowances, and the accounts that joined. New codes and + links are shown once, because only a hash is stored; destructive actions + are confirmed inline. + +See [@plexusone/invites](../packages/invites.md) for each page's sections +and props, and the API client's methods. + +## Testing + +The pages fetch with the global `fetch`, so a test can stub it and answer +each route with JSON, as the package's own tests do. Answer `GET /settings` +and `GET /me/invites` for the member page, and the `/admin/...` routes for +the operator page. Use `vi.stubGlobal('fetch', ...)` in Vitest, and restore +it after each test. diff --git a/docs/packages/invites.md b/docs/packages/invites.md index 184edd6..e219a8b 100644 --- a/docs/packages/invites.md +++ b/docs/packages/invites.md @@ -4,6 +4,8 @@ Pages for SystemForge's invite-gated sign-up: an operator page to manage invite codes, email invites, the waitlist and member allowances, and a member page to invite people. Both call SystemForge's invite API (`signupapi`); see SystemForge's [invite-gated sign-up guide](https://plexusone.github.io/systemforge/guides/invite-gated-signup/). +[Adding invite pages](../guides/invite-gated-signup.md) walks through wiring +them into an application. ```bash pnpm add @plexusone/invites @plexusone/ui diff --git a/docs/releases/v0.5.0.md b/docs/releases/v0.5.0.md new file mode 100644 index 0000000..7bce7be --- /dev/null +++ b/docs/releases/v0.5.0.md @@ -0,0 +1,51 @@ +# v0.5.0 Release Notes + +**Release Date:** 2026-10-26 + +Adds `@plexusone/invites` at 0.1.0. The other packages are unchanged. + +## Highlights + +- **Pages for launching invite-only.** `@plexusone/invites` gives an + application the two pages SystemForge's invite-gated sign-up needs. + `InviteAdminPage` lets platform administrators create invite codes with a + use limit and expiry, invite people by email address, invite people from + the waitlist, set how many people each member may invite, and see who + joined with which invite. `InvitePeoplePage` lets a member create invite + links from their allowance and revoke ones nobody has used. See + [@plexusone/invites](../packages/invites.md). +- **Shown once.** SystemForge stores only a hash of each code, so a new code + or link is shown once, with a copy button, and the page says so. +- **A typed client.** `createInvitesApi` covers every route of SystemForge's + invite API, with snake_case wire types as the API sends them and + `ApiError` carrying the HTTP status and the API's message. +- **A guide.** [Adding invite pages](../guides/invite-gated-signup.md) covers + where to serve the invite API, styles, routes, which menu links to show to + whom, and testing. + +## Requirements + +- `@plexusone/invites` needs `@plexusone/ui` and React 19. +- systemforge v0.22.0 or later serves the invite API (`signupapi`), behind + the application's own session with `Service.SignupAPI` or on the SystemAuth + host with `Options.SignupAPI`. + +## Installation + +```bash +pnpm add @plexusone/invites @plexusone/ui +``` + +Add the package's built files to Tailwind's sources: + +```css +@source "../node_modules/@plexusone/invites/dist"; +``` + +## Upgrading + +Nothing to change: no existing package moved. + +## Not in this release + +- Nothing is emailed. Operators and members share invite links themselves. diff --git a/mkdocs.yml b/mkdocs.yml index 61d0082..9acfc26 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -50,6 +50,7 @@ nav: - Quick Start: getting-started/quickstart.md - Guides: - Writing an Application: guides/writing-an-application.md + - Adding Invite Pages: guides/invite-gated-signup.md - Releasing: guides/releasing.md - Packages: - Overview: packages/index.md @@ -86,6 +87,7 @@ nav: - Tasks: specs/initiatives/invention/productgraph/TASKS.md - Releases: - Changelog: changelog.md + - v0.5.0: releases/v0.5.0.md - v0.4.0: releases/v0.4.0.md - v0.3.1: releases/v0.3.1.md - v0.3.0: releases/v0.3.0.md