@acms-subir/client 0.1.18 → 0.1.20

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
@@ -49,18 +49,21 @@ language. Update the site before shipping an app build with a newer client.
49
49
  | `calendars()` | Public calendars |
50
50
  | `resources(opts)`, `resource(slug, opts)` | Public resources; slug lookup scans list pages |
51
51
  | `availability(opts)` | Required `resource`, `link`; optional `from`, `days`, `minutes` |
52
- | `rsvpStatus(slug, { start })` | One event date's RSVP status: `open` or `reason` (`disabled`, `ended`, `closed` past the event's deadline, `cancelled`, `full`), `closesAt` (when RSVPs to the date close, or `null`: open until it ends), 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 |
53
- | `book(opts)` | Resource booking or RSVP input (`RsvpInput` for an RSVP), optional `idempotencyKey`; sends the client's `locale` (or `opts.locale`) for its emails |
52
+ | `rsvpStatus(slug, { start })` | One event date's RSVP status: `open` or `reason` (`disabled`, `ended`, `closed` past the event's deadline, `cancelled`, `full`), `closesAt` (when RSVPs to the date close, or `null`: open until it ends), live `going`/`left` (held waitlist spots count as taken), `maxSeats` for one RSVP right now, `autoConfirm`, `waitlist` (`enabled`: a full date takes places in line; `waiting` people for `seats` seats; `maxSeats` for one place in line), 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 |
53
+ | `book(opts)` | Resource booking or RSVP input (`RsvpInput` for an RSVP; `waitlist: true` joins a full date's waitlist, status `waitlisted`), optional `idempotencyKey`; sends the client's `locale` (or `opts.locale`) for its emails |
54
54
  | `openapi()` | Raw OpenAPI document |
55
55
  | `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']`) |
56
56
  | `updateDevice(token, { topics?, locale?, channels? })` | Change a device's topics, language or channels |
57
- | `unregisterDevice(token)` | Opt out (idempotent) |
57
+ | `unregisterDevice(token)` | Opt out (idempotent); also unlinks a staff test phone |
58
58
  | `pushTopics()` | Topics to offer as notification toggles, named in the client's language (built-in and the site's own, `kind: 'site'`) |
59
+ | `linkStaffPhone(token, code)` | Staff only: link this registered device with the 6-digit code from the admin's Test phones, so it gets that person's test notifications; `{ linked, staff: { name } }`. A wrong, used or expired code is a 400, too many tries a 429; never retried |
60
+ | `unlinkStaffPhone(token)` | Unlink it (idempotent); `{ linked: false, staff: null }` |
59
61
  | `links()` | The links page: profile, socials and visible blocks (`link`, `email`, `phone`, `contact`, `heading`, `text`, `spacer`, `live`, `embed`) |
60
62
  | `live()` | Live video: the current broadcast (`snapshot` or `null`), `serverNow` and the owner's `display` choices. Never cached; 404 while `site.features.live` is false |
61
63
  | `announcements({ kind?, pinned?, page?, limit? })`, `announcement(id)`, `paginate.announcements()` | Announcements up right now in the client's language, newest first (`pinned: true`: the banners, urgent first); 404 while `site.features.announcements` is false |
62
64
  | `form(id)`, `submitForm(id, { answers, version?, idempotencyKey? })` | A published form's questions; send answers (checkbox groups as arrays). Each call makes one key; retries reuse it |
63
- | `manageBooking(token)`, `cancelBooking(token)` | The booker's own booking from its `manageToken`, and cancelling it; `can` says what is allowed |
65
+ | `manageBooking(token)`, `cancelBooking(token)` | The booker's own booking from its `manageToken`, and cancelling it (or leaving a waitlist, or releasing a held spot); `can` says what is allowed. A waitlisted RSVP has `booking.position` (its place in line), an offered one `booking.holdUntil` |
66
+ | `confirmBooking(token)` | Take a spot the waitlist offered (`can.confirm`): `confirmed`, or `pending` when RSVPs need approval. 410 once `holdUntil` passed. Core 0.3.40 or later |
64
67
  | `rescheduleAvailability(token, { from?, days?, minutes? })`, `rescheduleBooking(token, { start, minutes? })` | Open times for moving it (its own time counts as free), then the move |
65
68
  | `bookingIcs(token)` | The booking as an iCalendar file (string) |
66
69
  | `requestGuestCode(email)`, `guestSignIn({ email, code, deviceName? })`, `guestBookings(guestToken, { when? })`, `guestSignOut(guestToken)` | Guest sign-in for "My bookings" |
@@ -128,6 +131,14 @@ if (status.open) {
128
131
  // result.manageToken changes or cancels it later (manageBooking, cancelBooking), like the email's link:
129
132
  // keep it in secure storage only.
130
133
  console.log(result.booking.status);
134
+ } else if (status.reason === 'full' && status.waitlist.enabled) {
135
+ // a place in line (status 'waitlisted', no seats); when a spot opens the site emails them and holds it
136
+ // for 24 hours: then manageBooking shows status 'offered' and holdUntil, and confirmBooking(token) takes it
137
+ const result = await client.book({
138
+ event: date.event.slug, start: date.start, seats: 2, name: 'Example Guest', email: 'guest@example.com', waitlist: true,
139
+ ...(status.form ? { formVersion: status.form.version } : {}), idempotencyKey: crypto.randomUUID(),
140
+ });
141
+ console.log(result.booking.status);
131
142
  }
132
143
 
133
144
  const controller = new AbortController();
@@ -142,7 +153,7 @@ failures, 5xx and 429 responses at most twice (three attempts total), with
142
153
  250/500ms backoff and at least the server's `Retry-After` delay (seconds or HTTP
143
154
  date). A wait that would outlast `timeoutMs` throws the 429 straight away, with
144
155
  `retryAfter`. One-shot writes (guest code requests and sign-in, cancelling and
145
- moving a booking, guest sign-out) are never repeated automatically: on a
156
+ moving a booking, confirming a held spot, guest sign-out) are never repeated automatically: on a
146
157
  network error, check the state before trying again. Booking retries always
147
158
  reuse the same serialized body and UUID. By default each `book()` call creates
148
159
  a key using `crypto.randomUUID`, falling back to a Math.random UUID v4 where
@@ -172,6 +183,18 @@ await client.updateDevice(token, { topics: ['events'] });
172
183
  await client.unregisterDevice(token);
173
184
  ```
174
185
 
186
+ Staff can link their own phone for test notifications ("Send test to my
187
+ phone" in the admin): the admin's Test phones panel shows a one-time 6-digit
188
+ code (10 minutes, single use) that the app sends with this phone's token.
189
+ `device.staff` (from `registerDevice` and `updateDevice`) and the link
190
+ response name who it's linked to (their name only), or `null`. Test pushes
191
+ carry `data.test: true`.
192
+
193
+ ```ts
194
+ const { staff } = await client.linkStaffPhone(token, '123456'); // { name: 'Ana López' }; 400 for a wrong, used or expired code
195
+ await client.unlinkStaffPhone(token); // turning notifications off unlinks it too
196
+ ```
197
+
175
198
  ## Live video
176
199
 
177
200
  `site.features.live` is true while the site's live video (Integrations → Live
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, RsvpStatus, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus, Announcements, AnnouncementDetail, EventDetail } 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, StaffLink, PushTopics, LiveStatus, Announcements, AnnouncementDetail, EventDetail } from './types.js';
2
2
  export { AcmsError, isAcmsError } from './errors.js';
3
3
  export type * from './types.js';
4
4
  export * from './editor.js';
@@ -169,6 +169,14 @@ export declare const endpoints: {
169
169
  readonly method: "GET";
170
170
  readonly path: "/push/topics";
171
171
  };
172
+ readonly linkStaffPhone: {
173
+ readonly method: "POST";
174
+ readonly path: "/devices/{token}/staff-link";
175
+ };
176
+ readonly unlinkStaffPhone: {
177
+ readonly method: "DELETE";
178
+ readonly path: "/devices/{token}/staff-link";
179
+ };
172
180
  readonly links: {
173
181
  readonly method: "GET";
174
182
  readonly path: "/links";
@@ -201,6 +209,10 @@ export declare const endpoints: {
201
209
  readonly method: "POST";
202
210
  readonly path: "/bookings/manage/cancel";
203
211
  };
212
+ readonly confirmBooking: {
213
+ readonly method: "POST";
214
+ readonly path: "/bookings/manage/confirm";
215
+ };
204
216
  readonly rescheduleAvailability: {
205
217
  readonly method: "GET";
206
218
  readonly path: "/bookings/manage/availability";
@@ -275,6 +287,14 @@ export interface AcmsClient {
275
287
  unregisterDevice(token: string, o?: RequestOptions): Promise<DeviceRemoved>;
276
288
  /** Topics the app can offer as notification toggles. */
277
289
  pushTopics(o?: RequestOptions): Promise<PushTopics>;
290
+ /**
291
+ * Staff only: link this registered device to the staff member whose one-time code (Admin → Test phones) it sends,
292
+ * so it gets their test notifications. `{ linked: true, staff: { name } }`. A wrong, used or expired code is a 400;
293
+ * too many tries a 429 (never retried). Sites without it (or with push off) answer 404.
294
+ */
295
+ linkStaffPhone(token: string, code: string, o?: RequestOptions): Promise<StaffLink>;
296
+ /** Unlink it (idempotent). Turning notifications off (`unregisterDevice`) unlinks it too. */
297
+ unlinkStaffPhone(token: string, o?: RequestOptions): Promise<StaffLink>;
278
298
  /** The links page: profile, socials and visible blocks. Requires `site.features.links`. */
279
299
  links(o?: RequestOptions): Promise<Links>;
280
300
  /**
@@ -295,7 +315,13 @@ export interface AcmsClient {
295
315
  }, o?: RequestOptions): Promise<FormReceipt>;
296
316
  /** The booker's own booking, from its private token (`manageToken`). Never cached. Requires `site.features.bookingChanges`. */
297
317
  manageBooking(token: string, o?: RequestOptions): Promise<ManagedBooking>;
318
+ /** Cancel it; for an RSVP on a waitlist, leave the line or release a held spot. */
298
319
  cancelBooking(token: string, o?: RequestOptions): Promise<ManagedBooking>;
320
+ /**
321
+ * Take a spot an RSVP's waitlist offered (`booking.status` `offered`, `can.confirm`): confirmed, or
322
+ * pending when the event approves RSVPs. A spot past its `holdUntil` was released (410). Requires core 0.3.40.
323
+ */
324
+ confirmBooking(token: string, o?: RequestOptions): Promise<ManagedBooking>;
299
325
  /** Open times for moving the booking (its own time counts as free). */
300
326
  rescheduleAvailability(token: string, q?: ManageAvailabilityQuery, o?: RequestOptions): Promise<Availability>;
301
327
  rescheduleBooking(token: string, input: RescheduleInput, o?: RequestOptions): Promise<ManagedBooking>;
package/dist/index.js CHANGED
@@ -15,6 +15,8 @@ export const endpoints = {
15
15
  // push notifications (only when the site enables push; 404 otherwise)
16
16
  registerDevice: { method: 'POST', path: '/devices' }, updateDevice: { method: 'PUT', path: '/devices/{token}' },
17
17
  unregisterDevice: { method: 'DELETE', path: '/devices/{token}' }, pushTopics: { method: 'GET', path: '/push/topics' },
18
+ // a staff member's own test phone (a one-time code from the admin's Test phones)
19
+ linkStaffPhone: { method: 'POST', path: '/devices/{token}/staff-link' }, unlinkStaffPhone: { method: 'DELETE', path: '/devices/{token}/staff-link' },
18
20
  // links page and forms (owner switches in Admin → Mobile app)
19
21
  links: { method: 'GET', path: '/links' }, form: { method: 'GET', path: '/forms/{id}' }, submitForm: { method: 'POST', path: '/forms/{id}/responses' },
20
22
  // live video status (404 while the site's live video is off)
@@ -23,6 +25,8 @@ export const endpoints = {
23
25
  announcements: { method: 'GET', path: '/announcements' }, announcement: { method: 'GET', path: '/announcements/{id}' },
24
26
  // the booker's own booking: its private token goes in the Booking-Token header
25
27
  manageBooking: { method: 'GET', path: '/bookings/manage' }, cancelBooking: { method: 'POST', path: '/bookings/manage/cancel' },
28
+ // take a spot an RSVP's waitlist offered
29
+ confirmBooking: { method: 'POST', path: '/bookings/manage/confirm' },
26
30
  rescheduleAvailability: { method: 'GET', path: '/bookings/manage/availability' }, rescheduleBooking: { method: 'POST', path: '/bookings/manage/reschedule' },
27
31
  bookingIcs: { method: 'GET', path: '/bookings/manage/ics' },
28
32
  // guest sign-in for "My bookings" (emailed code → guest token)
@@ -311,6 +315,8 @@ export function createClient(options) {
311
315
  updateDevice: (token, input, o = {}) => request('updateDevice', o, {}, token, input),
312
316
  unregisterDevice: (token, o = {}) => request('unregisterDevice', o, {}, token),
313
317
  pushTopics: (o = {}) => request('pushTopics', o),
318
+ linkStaffPhone: (token, code, o = {}) => request('linkStaffPhone', o, {}, token, { code }),
319
+ unlinkStaffPhone: (token, o = {}) => request('unlinkStaffPhone', o, {}, token),
314
320
  links: (o = {}) => request('links', o),
315
321
  live: (o = {}) => request('live', o),
316
322
  form: (id, o = {}) => request('form', o, {}, id),
@@ -320,6 +326,7 @@ export function createClient(options) {
320
326
  },
321
327
  manageBooking: (token, o = {}) => request('manageBooking', o, {}, undefined, undefined, undefined, bookingAuth(token)),
322
328
  cancelBooking: (token, o = {}) => request('cancelBooking', o, {}, undefined, undefined, undefined, bookingAuth(token)),
329
+ confirmBooking: (token, o = {}) => request('confirmBooking', o, {}, undefined, undefined, undefined, bookingAuth(token)),
323
330
  rescheduleAvailability: (token, q = {}, o = {}) => request('rescheduleAvailability', o, { from: q.from, days: q.days, minutes: q.minutes }, undefined, undefined, undefined, bookingAuth(token)),
324
331
  rescheduleBooking: (token, input, o = {}) => request('rescheduleBooking', o, {}, undefined, input, undefined, bookingAuth(token)),
325
332
  bookingIcs: (token, o = {}) => request('bookingIcs', o, {}, undefined, undefined, undefined, { ...bookingAuth(token), text: true }),
package/dist/types.d.ts CHANGED
@@ -223,6 +223,7 @@ export type Event = {
223
223
  "maxSeats": number;
224
224
  "closesAt": string | null;
225
225
  "closeBefore": number;
226
+ "waitlist": boolean;
226
227
  "form"?: {
227
228
  "title": string;
228
229
  "intro": string;
@@ -315,6 +316,7 @@ export type Events = {
315
316
  "maxSeats": number;
316
317
  "closesAt": string | null;
317
318
  "closeBefore": number;
319
+ "waitlist": boolean;
318
320
  "form"?: {
319
321
  "title": string;
320
322
  "intro": string;
@@ -402,6 +404,7 @@ export type EventDetail = {
402
404
  "maxSeats": number;
403
405
  "closesAt": string | null;
404
406
  "closeBefore": number;
407
+ "waitlist": boolean;
405
408
  "form"?: {
406
409
  "title": string;
407
410
  "intro": string;
@@ -542,6 +545,7 @@ export type SearchResults = {
542
545
  "maxSeats": number;
543
546
  "closesAt": string | null;
544
547
  "closeBefore": number;
548
+ "waitlist": boolean;
545
549
  "form"?: {
546
550
  "title": string;
547
551
  "intro": string;
@@ -709,6 +713,12 @@ export type RsvpStatus = {
709
713
  "maxSeats": number;
710
714
  "autoConfirm": boolean;
711
715
  "closesAt": (string) | (null);
716
+ "waitlist": {
717
+ "enabled": boolean;
718
+ "waiting": number;
719
+ "seats": number;
720
+ "maxSeats": number;
721
+ };
712
722
  "form": ({
713
723
  "version": string;
714
724
  "title": string;
@@ -733,7 +743,7 @@ export type ManagedBooking = {
733
743
  "id": string;
734
744
  "reference": string;
735
745
  "kind": "booking" | "rsvp";
736
- "status": "pending" | "confirmed" | "declined" | "cancelled";
746
+ "status": "pending" | "confirmed" | "declined" | "cancelled" | "waitlisted" | "offered" | "expired";
737
747
  "start": string;
738
748
  "end": string;
739
749
  "allDay": boolean;
@@ -746,6 +756,8 @@ export type ManagedBooking = {
746
756
  [key: string]: string;
747
757
  };
748
758
  "createdAt": string;
759
+ "position": (number) | (null);
760
+ "holdUntil": (string) | (null);
749
761
  };
750
762
  "title": string;
751
763
  "location": string;
@@ -768,6 +780,7 @@ export type ManagedBooking = {
768
780
  "can": {
769
781
  "cancel": boolean;
770
782
  "reschedule": boolean;
783
+ "confirm": boolean;
771
784
  "cutoffHours": number;
772
785
  "closed": boolean;
773
786
  };
@@ -794,7 +807,7 @@ export type GuestBookings = {
794
807
  "id": string;
795
808
  "reference": string;
796
809
  "kind": "booking" | "rsvp";
797
- "status": "pending" | "confirmed" | "declined" | "cancelled";
810
+ "status": "pending" | "confirmed" | "declined" | "cancelled" | "waitlisted" | "offered" | "expired";
798
811
  "start": string;
799
812
  "end": string;
800
813
  "seats": number;
@@ -804,6 +817,7 @@ export type GuestBookings = {
804
817
  "can": {
805
818
  "cancel": boolean;
806
819
  "reschedule": boolean;
820
+ "confirm": boolean;
807
821
  "cutoffHours": number;
808
822
  "closed": boolean;
809
823
  };
@@ -881,7 +895,7 @@ export type BookingResult = {
881
895
  "booking": {
882
896
  "id": string;
883
897
  "kind": "booking" | "block" | "rsvp";
884
- "status": "pending" | "confirmed" | "declined" | "cancelled";
898
+ "status": "pending" | "confirmed" | "declined" | "cancelled" | "waitlisted" | "offered" | "expired";
885
899
  "name": string;
886
900
  "email": string;
887
901
  "phone": string;
@@ -929,6 +943,9 @@ export type Device = {
929
943
  "channels": Array<string>;
930
944
  "registeredAt": string;
931
945
  "lastSeenAt": string;
946
+ "staff": ({
947
+ "name": string;
948
+ }) | (null);
932
949
  };
933
950
  export type DeviceRegistration = {
934
951
  "created": boolean;
@@ -940,12 +957,21 @@ export type DeviceRegistration = {
940
957
  "channels": Array<string>;
941
958
  "registeredAt": string;
942
959
  "lastSeenAt": string;
960
+ "staff": ({
961
+ "name": string;
962
+ }) | (null);
943
963
  };
944
964
  };
945
965
  export type DeviceRemoved = {
946
966
  "token": string;
947
967
  "deleted": true;
948
968
  };
969
+ export type StaffLink = {
970
+ "linked": boolean;
971
+ "staff": ({
972
+ "name": string;
973
+ }) | (null);
974
+ };
949
975
  export type PushTopics = {
950
976
  "locale": string | null;
951
977
  "topics": Array<{
@@ -1249,6 +1275,7 @@ export type EditorDocument = {
1249
1275
  "closesAt": string | null;
1250
1276
  "closeBefore": number;
1251
1277
  "notifyStaff": boolean;
1278
+ "waitlist": boolean;
1252
1279
  "form"?: {
1253
1280
  "title": string;
1254
1281
  "intro": string;
@@ -1442,7 +1469,7 @@ export type EditorMediaList = {
1442
1469
  export type EditorBooking = {
1443
1470
  "id": string;
1444
1471
  "kind": "booking" | "block" | "rsvp";
1445
- "status": "pending" | "confirmed" | "declined" | "cancelled";
1472
+ "status": "pending" | "confirmed" | "declined" | "cancelled" | "waitlisted" | "offered" | "expired";
1446
1473
  "name": string;
1447
1474
  "email": string;
1448
1475
  "phone": string;
@@ -1489,7 +1516,7 @@ export type EditorBookingList = {
1489
1516
  "items": Array<{
1490
1517
  "id": string;
1491
1518
  "kind": "booking" | "block" | "rsvp";
1492
- "status": "pending" | "confirmed" | "declined" | "cancelled";
1519
+ "status": "pending" | "confirmed" | "declined" | "cancelled" | "waitlisted" | "offered" | "expired";
1493
1520
  "name": string;
1494
1521
  "email": string;
1495
1522
  "phone": string;
@@ -2084,6 +2111,7 @@ export type BookingInput = ({
2084
2111
  "event": string;
2085
2112
  "seats"?: number;
2086
2113
  "formVersion"?: string;
2114
+ "waitlist"?: boolean;
2087
2115
  });
2088
2116
  export type DeviceInput = {
2089
2117
  "token": string;
@@ -2182,6 +2210,7 @@ export type EditorSaveInput = {
2182
2210
  "closesAt"?: (string) | (null);
2183
2211
  "closeBefore"?: number;
2184
2212
  "notifyStaff"?: boolean;
2213
+ "waitlist"?: boolean;
2185
2214
  "form"?: {
2186
2215
  "title"?: string;
2187
2216
  "intro"?: string;
@@ -2221,7 +2250,7 @@ export type EditorMediaQuery = {
2221
2250
  "limit"?: number;
2222
2251
  };
2223
2252
  export type EditorBookingsQuery = {
2224
- "status"?: "active" | "pending" | "confirmed" | "declined" | "cancelled" | "all";
2253
+ "status"?: "active" | "pending" | "confirmed" | "waitlist" | "waitlisted" | "offered" | "expired" | "declined" | "cancelled" | "all";
2225
2254
  "from"?: (undefined) | (string);
2226
2255
  "page"?: number;
2227
2256
  "limit"?: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/client",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Typed, dependency-free client for the ACMS public mobile API.",
5
5
  "type": "module",
6
6
  "license": "MIT",