@acms-subir/client 0.1.14 → 0.1.16

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/README.md CHANGED
@@ -27,8 +27,11 @@ raw document. Every method accepts `locale` and `signal` in its options object.
27
27
  Locale is sent only for routes that support translations. `withLocale` shares
28
28
  the cache and transport configuration with the original client. `site()` and
29
29
  `pushTopics()` take a language since core 0.3.32; an older site rejects it, so
30
- the client asks once more without it and gets the main language. Update the
31
- site before shipping an app build with a newer client.
30
+ the client asks once more without it and gets the main language. `book()`
31
+ sends the client's `locale` in its body, so the booking's emails (and the links
32
+ in them) speak the app's language; a site before core 0.3.38 refuses that field,
33
+ so the client books once more without it, with the same key, in the site's main
34
+ language. Update the site before shipping an app build with a newer client.
32
35
 
33
36
  | Method | Options / behavior |
34
37
  | --- | --- |
@@ -45,7 +48,8 @@ site before shipping an app build with a newer client.
45
48
  | `calendars()` | Public calendars |
46
49
  | `resources(opts)`, `resource(slug, opts)` | Public resources; slug lookup scans list pages |
47
50
  | `availability(opts)` | Required `resource`, `link`; optional `from`, `days`, `minutes` |
48
- | `book(opts)` | Resource booking or RSVP input, optional `idempotencyKey` |
51
+ | `rsvpStatus(slug, { start })` | One event date's RSVP status: `open` or `reason` (`disabled`, `ended`, `cancelled`, `full`), live `going`/`left`, `maxSeats` for one RSVP right now, `autoConfirm`, and `form` (questions and `version`, or `null`). `start` is the occurrence's start exactly as `events()` returns it (a UTC ISO string such as `2026-10-05T14:00:00.000Z`). Never cached |
52
+ | `book(opts)` | Resource booking or RSVP input (`RsvpInput` for an RSVP), optional `idempotencyKey`; sends the client's `locale` (or `opts.locale`) for its emails |
49
53
  | `openapi()` | Raw OpenAPI document |
50
54
  | `registerDevice({ token, platform, locale?, topics?, channels? })` | Register or refresh an Expo push token (call on every app start); `{ created, device }`. `channels`: Android channels the app created (e.g. `['urgent']`) |
51
55
  | `updateDevice(token, { topics?, locale?, channels? })` | Change a device's topics, language or channels |
@@ -87,6 +91,8 @@ server results or provide snapshot consistency; deduplicate IDs in your app.
87
91
  ## Bookings, errors and cancellation
88
92
 
89
93
  ```ts
94
+ import type { RsvpInput } from '@acms-subir/client';
95
+
90
96
  // Persist a UUID v4 with this booking intent before submitting; reuse it until
91
97
  // the outcome is known. Use your platform's secure UUID/storage facilities.
92
98
  const idempotencyKey = crypto.randomUUID();
@@ -103,6 +109,25 @@ try {
103
109
  }
104
110
  }
105
111
 
112
+ // RSVP to one date of an event (needs only the calendar: site.features.rsvp)
113
+ const { occurrences } = await client.events({ q: 'supper' });
114
+ const date = occurrences[0];
115
+ const status = await client.rsvpStatus(date.event.slug, { start: date.start });
116
+ if (status.open) {
117
+ const rsvp: RsvpInput = {
118
+ event: date.event.slug, start: date.start, seats: 2, name: 'Example Guest', email: 'guest@example.com',
119
+ // field id → answer; checkbox groups send a list
120
+ answers: { meal: 'Vegetarian', help: ['Setup', 'Music'] },
121
+ // required when the event asks questions: a changed form answers 409, so read the status again
122
+ ...(status.form ? { formVersion: status.form.version } : {}),
123
+ };
124
+ const result = await client.book({ ...rsvp, idempotencyKey: crypto.randomUUID() });
125
+ // pending when the organiser approves RSVPs; status.form?.confirmation is the thank-you text.
126
+ // result.manageToken changes or cancels it later (manageBooking, cancelBooking), like the email's link:
127
+ // keep it in secure storage only.
128
+ console.log(result.booking.status);
129
+ }
130
+
106
131
  const controller = new AbortController();
107
132
  const pending = client.home({ signal: controller.signal });
108
133
  controller.abort();
@@ -172,7 +197,7 @@ if (current?.status === 'live') console.log(`Live: ${current.title} → ${curren
172
197
  ETag revalidation defaults to an in-memory URL-keyed cache. Each read contacts
173
198
  the server with `If-None-Match`; a 304 reuses the saved data. This is not an
174
199
  offline fallback. Treat returned cached objects as immutable. Use `cache: false`
175
- to disable it. Availability, live status, booking results and errors are never cached.
200
+ to disable it. Availability, live status, RSVP status, booking results and errors are never cached.
176
201
  Responses without an ETag or with `no-store` invalidate an older entry.
177
202
 
178
203
  ```ts
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus, Announcements, AnnouncementDetail } from './types.js';
1
+ import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, RsvpStatus, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus, Announcements, AnnouncementDetail } from './types.js';
2
2
  export { AcmsError, isAcmsError } from './errors.js';
3
3
  export type * from './types.js';
4
4
  export * from './editor.js';
@@ -56,6 +56,10 @@ export interface AvailabilityOptions extends RequestOptions {
56
56
  days?: number;
57
57
  minutes?: number;
58
58
  }
59
+ /** `start`: the occurrence's start exactly as `events()` returns it, a UTC ISO string such as `'2026-10-05T14:00:00.000Z'`. */
60
+ export interface RsvpStatusOptions extends RequestOptions {
61
+ start: string;
62
+ }
59
63
  export type BookOptions = BookingInput & RequestOptions & {
60
64
  idempotencyKey?: string;
61
65
  };
@@ -125,6 +129,10 @@ export declare const endpoints: {
125
129
  readonly method: "GET";
126
130
  readonly path: "/calendars";
127
131
  };
132
+ readonly rsvpStatus: {
133
+ readonly method: "GET";
134
+ readonly path: "/events/{slug}/rsvp";
135
+ };
128
136
  readonly resources: {
129
137
  readonly method: "GET";
130
138
  readonly path: "/resources";
@@ -241,6 +249,13 @@ export interface AcmsClient {
241
249
  resources(o?: ListOptions): Promise<Resources>;
242
250
  resource(slug: string, o?: RequestOptions): Promise<Resource>;
243
251
  availability(o: AvailabilityOptions): Promise<Availability>;
252
+ /**
253
+ * One date's RSVP status: `open` (or `reason`: disabled, ended, cancelled, full), live `going`/`left`,
254
+ * `maxSeats` for one RSVP right now, and `form` (questions and `version`, or null). Never cached.
255
+ * RSVP with `book({ event, start, seats, name, email, answers, formVersion: form.version })`.
256
+ */
257
+ rsvpStatus(slug: string, o: RsvpStatusOptions): Promise<RsvpStatus>;
258
+ /** A room booking or an RSVP. Sends the client's `locale` (or `o.locale`), so its emails and links speak it. */
244
259
  book(o: BookOptions): Promise<BookingResult>;
245
260
  openapi(o?: RequestOptions): Promise<OpenApiDocument>;
246
261
  /** Register or refresh this device's Expo push token. Call on every app start. Requires push on the site. */
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ export const endpoints = {
8
8
  content: { method: 'GET', path: '/content' }, contentById: { method: 'GET', path: '/content/{id}' },
9
9
  search: { method: 'GET', path: '/search' },
10
10
  events: { method: 'GET', path: '/events' }, calendars: { method: 'GET', path: '/calendars' },
11
+ // an event date's RSVP status: live seats and questions (never cached); RSVP with book()
12
+ rsvpStatus: { method: 'GET', path: '/events/{slug}/rsvp' },
11
13
  resources: { method: 'GET', path: '/resources' }, availability: { method: 'GET', path: '/availability' },
12
14
  book: { method: 'POST', path: '/bookings' }, openapi: { method: 'GET', path: '/openapi.json' },
13
15
  // push notifications (only when the site enables push; 404 otherwise)
@@ -28,7 +30,7 @@ export const endpoints = {
28
30
  guestBookings: { method: 'GET', path: '/guest/bookings' }, guestSignOut: { method: 'POST', path: '/guest/revoke' },
29
31
  };
30
32
  // site: its legal links; pushTopics: topic names
31
- const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'site', 'pushTopics', 'announcements', 'announcement']);
33
+ const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'rsvpStatus', 'site', 'pushTopics', 'announcements', 'announcement']);
32
34
  // localized since core 0.3.32; older sites answer ?locale= with a 400, so ask them again without it
33
35
  const lateLocalized = new Set(['site', 'pushTopics']);
34
36
  function uuid() {
@@ -112,8 +114,8 @@ export function createClient(options) {
112
114
  opts.signal?.addEventListener('abort', abort, { once: true });
113
115
  const timer = setTimeout(() => controller.abort(new AcmsError(0, 'timeout', 'Request timed out.', undefined, undefined, key)), timeoutMs);
114
116
  const signal = controller.signal;
115
- // availability and live status are never served from the cache
116
- const cacheable = endpoint.method === 'GET' && name !== 'availability' && name !== 'live' && !extra.headers && cache;
117
+ // availability, live status and RSVP seats are never served from the cache
118
+ const cacheable = endpoint.method === 'GET' && name !== 'availability' && name !== 'live' && name !== 'rsvpStatus' && !extra.headers && cache;
117
119
  // only replay what the server treats as repeatable: a lost reply to a code
118
120
  // exchange or a move must not run it twice
119
121
  const replayable = endpoint.method !== 'POST' || !!key || name === 'registerDevice';
@@ -277,10 +279,23 @@ export function createClient(options) {
277
279
  },
278
280
  calendars: (o = {}) => request('calendars', o),
279
281
  availability: (o) => request('availability', o, { resource: o.resource, link: o.link, from: o.from, days: o.days, minutes: o.minutes }),
280
- book: (o) => {
281
- const { locale, signal, idempotencyKey, ...body } = o;
282
+ rsvpStatus: (slug, o) => request('rsvpStatus', o, { start: o.start }, slug),
283
+ async book(o) {
284
+ const { locale = options.locale, signal, idempotencyKey, ...body } = o;
282
285
  const key = idempotencyKey ?? defaultHeaders.get('Idempotency-Key') ?? uuid();
283
- return request('book', { locale, signal }, {}, undefined, body, key);
286
+ // the client's language rides in the body: the booking's emails and links speak it
287
+ if (locale === undefined)
288
+ return request('book', { signal }, {}, undefined, body, key);
289
+ try {
290
+ return await request('book', { signal }, {}, undefined, { ...body, locale }, key);
291
+ }
292
+ catch (error) {
293
+ // sites before core 0.3.38 refuse the field (and one the site doesn't have): book without it, in
294
+ // the site's main language. A refused request doesn't use up the key.
295
+ if (!(error instanceof AcmsError) || error.status !== 400 || !/locale/i.test(JSON.stringify([error.message, error.issues ?? []])))
296
+ throw error;
297
+ return request('book', { signal }, {}, undefined, body, key);
298
+ }
284
299
  },
285
300
  openapi: (o = {}) => request('openapi', o),
286
301
  registerDevice: (input, o = {}) => request('registerDevice', o, {}, undefined, input),
package/dist/types.d.ts CHANGED
@@ -29,6 +29,7 @@ export type Site = {
29
29
  }>;
30
30
  "calendar": boolean;
31
31
  "bookings": boolean;
32
+ "rsvp": boolean;
32
33
  "search": boolean;
33
34
  "bookingChanges": boolean;
34
35
  "guestSignIn": boolean;
@@ -250,6 +251,9 @@ export type Event = {
250
251
  "name": string;
251
252
  "color": string;
252
253
  }>;
254
+ "notify": {
255
+ "changes": Array<string>;
256
+ };
253
257
  };
254
258
  export type Events = {
255
259
  "locale": string | null;
@@ -337,6 +341,9 @@ export type Events = {
337
341
  "name": string;
338
342
  "color": string;
339
343
  }>;
344
+ "notify": {
345
+ "changes": Array<string>;
346
+ };
340
347
  };
341
348
  }>;
342
349
  };
@@ -473,6 +480,9 @@ export type SearchResults = {
473
480
  "name": string;
474
481
  "color": string;
475
482
  }>;
483
+ "notify": {
484
+ "changes": Array<string>;
485
+ };
476
486
  };
477
487
  }>;
478
488
  "more": {
@@ -591,6 +601,40 @@ export type FormReceipt = {
591
601
  "received": true;
592
602
  "confirmation": string;
593
603
  };
604
+ export type RsvpStatus = {
605
+ "event": {
606
+ "id": string;
607
+ "slug": string;
608
+ "title": string;
609
+ };
610
+ "start": string;
611
+ "end": string;
612
+ "open": boolean;
613
+ "reason": ("disabled" | "ended" | "cancelled" | "full") | (null);
614
+ "going": number;
615
+ "left": (number) | (null);
616
+ "capacity": number;
617
+ "maxSeats": number;
618
+ "autoConfirm": boolean;
619
+ "form": ({
620
+ "version": string;
621
+ "title": string;
622
+ "intro": string;
623
+ "submitLabel": string;
624
+ "confirmation": string;
625
+ "fields": Array<{
626
+ "id": string;
627
+ "type": "text" | "textarea" | "email" | "phone" | "number" | "select" | "checkbox" | "date" | "radio" | "checkboxes" | "url" | "consent" | "heading";
628
+ "label": string;
629
+ "required": boolean;
630
+ "help": string;
631
+ "options": Array<string>;
632
+ "placeholder": string;
633
+ "width": "full" | "half";
634
+ "allowOther": boolean;
635
+ }>;
636
+ }) | (null);
637
+ };
594
638
  export type ManagedBooking = {
595
639
  "booking": {
596
640
  "id": string;
@@ -634,6 +678,7 @@ export type ManagedBooking = {
634
678
  "cutoffHours": number;
635
679
  "closed": boolean;
636
680
  };
681
+ "confirmation": string | null;
637
682
  };
638
683
  export type GuestCodeSent = {
639
684
  "sent": true;
@@ -774,7 +819,7 @@ export type BookingResult = {
774
819
  [key: string]: string;
775
820
  };
776
821
  };
777
- "source": "web" | "admin" | "api" | "agent";
822
+ "source": "web" | "app" | "admin" | "api" | "agent";
778
823
  "start": string;
779
824
  "end": string;
780
825
  "resourceId": string | null;
@@ -1332,7 +1377,7 @@ export type EditorBooking = {
1332
1377
  [key: string]: string;
1333
1378
  };
1334
1379
  };
1335
- "source": "web" | "admin" | "api" | "agent";
1380
+ "source": "web" | "app" | "admin" | "api" | "agent";
1336
1381
  "notes": string;
1337
1382
  "start": string;
1338
1383
  "end": string;
@@ -1379,7 +1424,7 @@ export type EditorBookingList = {
1379
1424
  [key: string]: string;
1380
1425
  };
1381
1426
  };
1382
- "source": "web" | "admin" | "api" | "agent";
1427
+ "source": "web" | "app" | "admin" | "api" | "agent";
1383
1428
  "notes": string;
1384
1429
  "start": string;
1385
1430
  "end": string;
@@ -1924,22 +1969,24 @@ export type BookingInput = ({
1924
1969
  "name": string;
1925
1970
  "email": string;
1926
1971
  "phone"?: string;
1972
+ "answers"?: {
1973
+ [key: string]: (string) | (Array<string>);
1974
+ };
1975
+ "locale"?: string;
1927
1976
  "resource": string;
1928
1977
  "link": string;
1929
1978
  "minutes"?: number;
1930
- "answers"?: {
1931
- [key: string]: string;
1932
- };
1933
1979
  }) | ({
1934
1980
  "start": string;
1935
1981
  "name": string;
1936
1982
  "email": string;
1937
1983
  "phone"?: string;
1938
- "event": string;
1939
- "seats"?: number;
1940
1984
  "answers"?: {
1941
- [key: string]: string;
1985
+ [key: string]: (string) | (Array<string>);
1942
1986
  };
1987
+ "locale"?: string;
1988
+ "event": string;
1989
+ "seats"?: number;
1943
1990
  "formVersion"?: string;
1944
1991
  });
1945
1992
  export type DeviceInput = {
@@ -2429,4 +2476,8 @@ export type SearchEventHit = SearchResults["events"][number];
2429
2476
  export type LinkItem = Links["items"][number];
2430
2477
  export type FormField = Form["fields"][number];
2431
2478
  export type GuestBooking = GuestBookings["items"][number];
2479
+ export type RsvpInput = Extract<BookingInput, {
2480
+ "event": string;
2481
+ }>;
2482
+ export type RsvpForm = NonNullable<RsvpStatus["form"]>;
2432
2483
  export type LiveDisplay = LiveStatus["display"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/client",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "Typed, dependency-free client for the ACMS public mobile API.",
5
5
  "type": "module",
6
6
  "license": "MIT",