@acms-subir/client 0.1.12 → 0.1.14

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
@@ -25,7 +25,10 @@ const english = client.withLocale('en');
25
25
  hosting prefix. All methods return **unwrapped `data`**; `openapi()` returns the
26
26
  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
- the cache and transport configuration with the original client.
28
+ the cache and transport configuration with the original client. `site()` and
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.
29
32
 
30
33
  | Method | Options / behavior |
31
34
  | --- | --- |
@@ -44,12 +47,13 @@ the cache and transport configuration with the original client.
44
47
  | `availability(opts)` | Required `resource`, `link`; optional `from`, `days`, `minutes` |
45
48
  | `book(opts)` | Resource booking or RSVP input, optional `idempotencyKey` |
46
49
  | `openapi()` | Raw OpenAPI document |
47
- | `registerDevice({ token, platform, locale?, topics? })` | Register or refresh an Expo push token (call on every app start); `{ created, device }` |
48
- | `updateDevice(token, { topics?, locale? })` | Change a device's topics or language |
50
+ | `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
+ | `updateDevice(token, { topics?, locale?, channels? })` | Change a device's topics, language or channels |
49
52
  | `unregisterDevice(token)` | Opt out (idempotent) |
50
53
  | `pushTopics()` | Topics to offer as notification toggles, named in the client's language (built-in and the site's own, `kind: 'site'`) |
51
54
  | `links()` | The links page: profile, socials and visible blocks (`link`, `email`, `phone`, `contact`, `heading`, `text`, `spacer`, `live`, `embed`) |
52
55
  | `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 |
56
+ | `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 |
53
57
  | `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 |
54
58
  | `manageBooking(token)`, `cancelBooking(token)` | The booker's own booking from its `manageToken`, and cancelling it; `can` says what is allowed |
55
59
  | `rescheduleAvailability(token, { from?, days?, minutes? })`, `rescheduleBooking(token, { start, minutes? })` | Open times for moving it (its own time counts as free), then the move |
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 } 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, 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';
@@ -44,6 +44,11 @@ export interface SearchOptions extends RequestOptions {
44
44
  /** Matches per group, 1–20; default 5. */
45
45
  limit?: number;
46
46
  }
47
+ export interface AnnouncementOptions extends ListOptions {
48
+ kind?: 'event' | 'registration' | 'help' | 'info' | 'urgent';
49
+ /** Only the banners (pinned), urgent first. */
50
+ pinned?: boolean;
51
+ }
47
52
  export interface AvailabilityOptions extends RequestOptions {
48
53
  resource: string;
49
54
  link: string;
@@ -168,6 +173,14 @@ export declare const endpoints: {
168
173
  readonly method: "GET";
169
174
  readonly path: "/live";
170
175
  };
176
+ readonly announcements: {
177
+ readonly method: "GET";
178
+ readonly path: "/announcements";
179
+ };
180
+ readonly announcement: {
181
+ readonly method: "GET";
182
+ readonly path: "/announcements/{id}";
183
+ };
171
184
  readonly manageBooking: {
172
185
  readonly method: "GET";
173
186
  readonly path: "/bookings/manage";
@@ -246,6 +259,10 @@ export interface AcmsClient {
246
259
  * clock, even if later requests fail. Requires `site.features.live`.
247
260
  */
248
261
  live(o?: RequestOptions): Promise<LiveStatus>;
262
+ /** Announcements up right now, newest first (`pinned: true`: only the banners, urgent first). Requires `site.features.announcements`. */
263
+ announcements(o?: AnnouncementOptions): Promise<Announcements>;
264
+ /** One announcement that is up; 404 once it came down. */
265
+ announcement(id: string, o?: RequestOptions): Promise<AnnouncementDetail>;
249
266
  /** A published form's questions. 410 when it stopped accepting responses. Requires `site.features.forms`. */
250
267
  form(id: string, o?: RequestOptions): Promise<Form>;
251
268
  /** Send answers (checkbox groups as arrays). Retries reuse the same Idempotency-Key, so they never duplicate. */
@@ -278,5 +295,6 @@ export interface AcmsClient {
278
295
  pages(o?: ListOptions): AsyncGenerator<ContentList['items'][number]>;
279
296
  events(o?: EventOptions): AsyncGenerator<Events['occurrences'][number]>;
280
297
  resources(o?: ListOptions): AsyncGenerator<Resource>;
298
+ announcements(o?: AnnouncementOptions): AsyncGenerator<Announcements['announcements'][number]>;
281
299
  };
282
300
  }
package/dist/index.js CHANGED
@@ -17,6 +17,8 @@ export const endpoints = {
17
17
  links: { method: 'GET', path: '/links' }, form: { method: 'GET', path: '/forms/{id}' }, submitForm: { method: 'POST', path: '/forms/{id}/responses' },
18
18
  // live video status (404 while the site's live video is off)
19
19
  live: { method: 'GET', path: '/live' },
20
+ // announcements (404 unless the site turns them on)
21
+ announcements: { method: 'GET', path: '/announcements' }, announcement: { method: 'GET', path: '/announcements/{id}' },
20
22
  // the booker's own booking: its private token goes in the Booking-Token header
21
23
  manageBooking: { method: 'GET', path: '/bookings/manage' }, cancelBooking: { method: 'POST', path: '/bookings/manage/cancel' },
22
24
  rescheduleAvailability: { method: 'GET', path: '/bookings/manage/availability' }, rescheduleBooking: { method: 'POST', path: '/bookings/manage/reschedule' },
@@ -26,7 +28,9 @@ export const endpoints = {
26
28
  guestBookings: { method: 'GET', path: '/guest/bookings' }, guestSignOut: { method: 'POST', path: '/guest/revoke' },
27
29
  };
28
30
  // site: its legal links; pushTopics: topic names
29
- const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'site', 'pushTopics']);
31
+ const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'site', 'pushTopics', 'announcements', 'announcement']);
32
+ // localized since core 0.3.32; older sites answer ?locale= with a 400, so ask them again without it
33
+ const lateLocalized = new Set(['site', 'pushTopics']);
30
34
  function uuid() {
31
35
  if (globalThis.crypto?.randomUUID)
32
36
  return globalThis.crypto.randomUUID();
@@ -98,7 +102,7 @@ export function createClient(options) {
98
102
  if (v !== undefined)
99
103
  url.searchParams.set(k, String(v));
100
104
  const locale = opts.locale ?? options.locale;
101
- if (localized.has(name) && locale !== undefined)
105
+ if (localized.has(name) && locale !== undefined && !extra.noLocale)
102
106
  url.searchParams.set('locale', locale);
103
107
  const controller = new AbortController();
104
108
  const abort = () => controller.abort(new AcmsError(0, 'aborted', 'Request aborted.', undefined, undefined, key));
@@ -178,6 +182,9 @@ export function createClient(options) {
178
182
  }
179
183
  throw new AcmsError(res.status, res.ok ? 'invalid_response' : 'http_error', `HTTP ${res.status}: expected a JSON response.`, retryAfter, undefined, key);
180
184
  }
185
+ // a site older than the client: unlocalized words beat no site info at all
186
+ if (res.status === 400 && lateLocalized.has(name) && url.searchParams.has('locale') && /locale/i.test(JSON.stringify(json?.error ?? '')))
187
+ return await request(name, opts, query, param, payload, key, { ...extra, noLocale: true });
181
188
  if (!res.ok)
182
189
  throw new AcmsError(res.status, json?.error?.code ?? 'http_error', json?.error?.message ?? `HTTP ${res.status}`, retryAfter, json?.error?.issues, key);
183
190
  if (name !== 'openapi' && (json?.version !== 1 || !Object.prototype.hasOwnProperty.call(json, 'data')))
@@ -225,19 +232,22 @@ export function createClient(options) {
225
232
  const pages = (o = {}) => content({ ...o, kind: 'page' });
226
233
  const events = (o = {}) => request('events', o, { ...paging(o), from: o.from, to: o.to, calendar: o.calendar, tag: o.tag, excludeTag: o.excludeTag, q: o.q });
227
234
  const resources = (o = {}) => request('resources', o, paging(o));
235
+ const announcements = (o = {}) => request('announcements', o, { ...paging(o), kind: o.kind, pinned: o.pinned ? 'true' : undefined });
228
236
  const paginate = {
229
237
  content: (o = {}) => iterate(content, 'items', o),
230
238
  posts: (o = {}) => iterate(posts, 'items', o),
231
239
  pages: (o = {}) => iterate(pages, 'items', o),
232
240
  events: (o = {}) => iterate(events, 'occurrences', o),
233
241
  resources: (o = {}) => iterate(resources, 'resources', o),
242
+ announcements: (o = {}) => iterate(announcements, 'announcements', o),
234
243
  };
235
244
  return {
236
245
  site: (o = {}) => request('site', o),
237
246
  theme: (o = {}) => request('theme', o),
238
247
  home: (o = {}) => request('home', o),
239
248
  page: (slug, o = {}) => request('page', o, {}, slug),
240
- content, posts, pages, events, resources, paginate,
249
+ content, posts, pages, events, resources, announcements, paginate,
250
+ announcement: (id, o = {}) => request('announcement', o, {}, id),
241
251
  async search(q, o = {}) {
242
252
  const query = q.trim();
243
253
  // blank input (an emptied search box) needs no round trip
package/dist/types.d.ts CHANGED
@@ -35,6 +35,7 @@ export type Site = {
35
35
  "forms": boolean;
36
36
  "links": boolean;
37
37
  "live": boolean;
38
+ "announcements": boolean;
38
39
  };
39
40
  "legal": {
40
41
  "privacy": string | null;
@@ -787,6 +788,7 @@ export type Device = {
787
788
  "platform": "ios" | "android";
788
789
  "locale": string | null;
789
790
  "topics": Array<string>;
791
+ "channels": Array<string>;
790
792
  "registeredAt": string;
791
793
  "lastSeenAt": string;
792
794
  };
@@ -797,6 +799,7 @@ export type DeviceRegistration = {
797
799
  "platform": "ios" | "android";
798
800
  "locale": string | null;
799
801
  "topics": Array<string>;
802
+ "channels": Array<string>;
800
803
  "registeredAt": string;
801
804
  "lastSeenAt": string;
802
805
  };
@@ -814,6 +817,69 @@ export type PushTopics = {
814
817
  "description"?: string;
815
818
  }>;
816
819
  };
820
+ export type Announcement = {
821
+ "id": string;
822
+ "kind": "event" | "registration" | "help" | "info" | "urgent";
823
+ "kindLabel": string;
824
+ "title": string;
825
+ "message": string;
826
+ "details": string;
827
+ "button": ({
828
+ "label": string;
829
+ "url": string;
830
+ }) | (null);
831
+ "url": string | null;
832
+ "pinned": boolean;
833
+ "urgent": boolean;
834
+ "publishedAt": string;
835
+ "showUntil": (string) | (null);
836
+ "translated": boolean;
837
+ };
838
+ export type Announcements = {
839
+ "locale": string | null;
840
+ "announcements": Array<{
841
+ "id": string;
842
+ "kind": "event" | "registration" | "help" | "info" | "urgent";
843
+ "kindLabel": string;
844
+ "title": string;
845
+ "message": string;
846
+ "details": string;
847
+ "button": ({
848
+ "label": string;
849
+ "url": string;
850
+ }) | (null);
851
+ "url": string | null;
852
+ "pinned": boolean;
853
+ "urgent": boolean;
854
+ "publishedAt": string;
855
+ "showUntil": (string) | (null);
856
+ "translated": boolean;
857
+ }>;
858
+ "pagination": {
859
+ "page": number;
860
+ "limit": number;
861
+ "nextPage": (number) | (null);
862
+ };
863
+ };
864
+ export type AnnouncementDetail = {
865
+ "id": string;
866
+ "kind": "event" | "registration" | "help" | "info" | "urgent";
867
+ "kindLabel": string;
868
+ "title": string;
869
+ "message": string;
870
+ "details": string;
871
+ "button": ({
872
+ "label": string;
873
+ "url": string;
874
+ }) | (null);
875
+ "url": string | null;
876
+ "pinned": boolean;
877
+ "urgent": boolean;
878
+ "publishedAt": string;
879
+ "showUntil": (string) | (null);
880
+ "translated": boolean;
881
+ "locale": string | null;
882
+ };
817
883
  export type LiveSnapshot = {
818
884
  "status": "live" | "upcoming" | "offline" | "unknown";
819
885
  "source": "automatic" | "manual";
@@ -1881,10 +1947,12 @@ export type DeviceInput = {
1881
1947
  "platform": "ios" | "android";
1882
1948
  "locale"?: string;
1883
1949
  "topics"?: Array<string>;
1950
+ "channels"?: Array<string>;
1884
1951
  };
1885
1952
  export type DeviceUpdate = {
1886
1953
  "locale"?: string;
1887
1954
  "topics"?: Array<string>;
1955
+ "channels"?: Array<string>;
1888
1956
  };
1889
1957
  export type EditorCreateInput = ({
1890
1958
  "kind": "post";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/client",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Typed, dependency-free client for the ACMS public mobile API.",
5
5
  "type": "module",
6
6
  "license": "MIT",