npm.io
1.0.103 • Published 2 weeks ago

@blazeo.com/calendar-client

Licence
UNLICENSED
Version
1.0.103
Deps
2
Size
439 kB
Vulns
0
Weekly
0

@blazeo.com/calendar-client

Private package – for Blazeo internal teams only. JavaScript client for the Blazeo Calendar / Appointment API. Use it in React (or any JS app) to call calendar and event endpoints via MobX State Tree models.

Install

From your npm registry (internal teams; ensure you're logged in to the scope):

npm install @blazeo.com/calendar-client

Or link locally:

cd calendar-client && npm run build && npm link
cd your-app && npm link @blazeo.com/calendar-client

Usage

Configure once at app startup, then use models and their methods:

import {
  configure,
  setAccessToken,
  setGetAccessToken,
  CalendarModel,
  createRootStore,
} from '@blazeo.com/calendar-client';

configure({
  baseUrl: 'https://your-appointment-api.example.com',
  consumer: 'my-app', // optional Consumer header
  accessToken: yourIdpAccessToken, // JWT from your identity provider
  expiresAtUtc: '2026-05-21T12:00:00Z', // optional — used before auto-refresh
  getAccessToken: async () => yourIdp.getAccessToken(), // optional refresh callback
});

// Calendar static methods (no store needed)
const timezones = await CalendarModel.getTimeZones();
const calendar = await CalendarModel.get('calendar-guid');

// Or use RootStore with models
const store = createRootStore();
const cal = store.addCalendar({ calendarId: 'my-cal', name: 'My Calendar' });
await cal.create(); // POST to backend
JWT authentication

The Appointment API validates Bearer JWTs from your identity provider (Authentication:Jwt:Authority on the server). This package does not exchange api keys for tokens — pass the JWT your app already has.

Function Purpose
setAccessToken(token, expiresAtUtc?) Store JWT from your IdP
setGetAccessToken(fn) Async callback to refresh when missing/near expiry
ensureValidAccessToken() Resolve token before a request (uses callback if needed)
clearAuth() Clear token and refresh callback

All reqGet / reqPost calls attach Authorization: Bearer … when a token is configured.

setAccessToken(session.accessToken, session.expiresAt);
setGetAccessToken(() => authService.acquireTokenSilent());
await LeadModel.requestExport('company-key', { userEmail: 'user@example.com' });

API overview

  • CalendarModel (static): get, getByCompany, getTimeZones, getTimeZone, getParticipants, getMonth, getEvents, etc.
  • EventModel (instance): get, create, cancel, delete, getCancellable, getAvailability, setReminder
  • EventModel (static): get, cancel, delete, getCancellable, createEvent, reschedule, updateEvent, … GET /event/cancel requires a bearer token (setAccessToken), like delete. The token, or the X-User-Email / X-User-Display-Name / X-User-Id headers a service token forwards, is what the AppointmentCanceled activity entry records as handledBy. cancel / delete resolve to the API envelope whose data is an EventCancellationData: besides EventId and Status it carries the appointment's Source / LeadSource, Host (participant), Location, CustomLocation, CalendarLocationValue, LeadId, dates and visitor details, so a cancellation can be recorded against the lead without re-fetching the event. The same object is the payload of the event/cancel / event/delete Consumer webhooks.
  • ActivityLogModel: get (GET /activitylog/get). Each entry exposes source / leadSource, location / customLocation / calendarLocationValue, handledBy (who performed the action) and host (the appointment's participant) for scheduled, rescheduled and canceled appointment activities. Canceled entries also carry cancellationReason, and their summary is that reason when one was given (booking notes otherwise).
  • FlowModel: Same pattern as Calendar — FlowModel.create({}, { env }) with no fields; static get, getRaw, list, createFlow, updateFlow, delete, duplicate, appearance/embed/public/preview helpers; instance methods mirror those using flowId on the snapshot
  • LeadModel: LeadModel.create({}, { env }); static get, getRaw, getByEmail, getByPhone, getByCompany, searchByCompany, saveColumnSelection, getColumnSelection, allowedColumns, requestExport; instance get, getByEmail, getByPhone, getByCompany, searchByCompany, saveColumnSelection, getColumnSelection, requestLeadExport. Pass { all: true } to getByEmail to include soft-deleted leads (GET /lead/getbyemail?all). Pass userId in getByCompany(companyKey, { userId }) to return only columns saved for that user. filters on first_name, last_name, and email that share the same text match when any of those fields contains it; other filters are combined together. searchByCompany calls GET /lead/company/search with the same arguments and returns a lead when any filter matches. For requestExport, pass userEmail (or rely on JWT email claim) so the server can publish a NotificationGenerated message when export completes.
  • ParticipantModel: static get, getByEmail, getByIds, getAll, …; instance getByEmail uses email + companyKey on the snapshot (see GET /participant/getbyemail)
  • AuthModel (calendar OAuth / Connect Calendar): getCalendarProviders, getAuthorizationUrl, startDirectAuthorization, getAuthorizationStatus, getDirectAuthorizationStatus, openOAuthPopup, onCalendarAuthMessage, waitForAuthorization, waitForDirectAuthorization — see Calendar authorization flow
  • RootStore: addCalendar, addEvent
Booking for a lead that already exists

When the booking is made from a lead the caller already has open (CRM lead page, SmartHub), send that lead's leadId. The booking is then attributed to that lead instead of being matched on the visitor details, so editing the email or phone on the form cannot create a second lead or raise a "new lead" notification:

await EventModel.createEvent({
  calendarId,
  leadId,
  visitorName: 'Jane Dyk',
  visitorEmail: 'jane@example.com',
  startDate, endDate, startHour, startMinute, endHour, endMinute,
}, { source: 'SmartHub' });

A leadId from another company is ignored, and the booking falls back to matching on email, then on the phone of a lead that has no email of its own. Omit leadId for public bookings, where the visitor is not a known lead yet.

Custom field order

Each field defined on a calendar has a sortOrder. CustomFieldModel.getAll and CustomFieldModel.getForm return fields with the lowest sortOrder first, then by created time. Send sortOrder on add or on each item in saveForm. When saveForm omits it, the array position is stored, so the list you submit is the order visitors see.

CRM lead custom fields on a booking

Answers to CRM lead custom fields travel in crmLeadCustomFieldsData, keyed by field label. Those option lists live in the CRM, not in the calendar, so an option id cannot be translated back to text here — send both halves or the confirmation email and booking details show the raw id:

await EventModel.createEvent({
  calendarId,
  visitorEmail: 'lead@example.com',
  startDate, endDate, startHour, startMinute, endHour, endMinute,
  crmLeadCustomFieldsData: {
    'Add Country': { id: '2', value: 'Pakistan' },
    'Add Language': { id: '192', value: 'Urdu' },
    'Add Interests': [{ id: '5', value: 'Sports' }, { id: '7', value: 'Music' }],
    'How did you hear about us?': 'A friend',
  },
}, { source: 'Booking Page' });

id is what the CRM receives; value is what people see. Free-text answers stay plain strings, and a bare id still works for backward compatibility (it just renders as the id). On the instance API the same data is passed through the create options: event.create({ offset, crmLeadCustomFieldsData }).

Per-calendar webhooks on booking

Both EventModel.createEvent and event.create() repeat the calendar id on the request URL as POST /event/create?calendar_id=<calendarId>. The endpoint itself ignores it; it is there so a Consumer webhook (Appointment.ConsumerEvent) can be registered for a single calendar rather than every booking the consumer makes:

event/create                                                   -> every booking by this consumer
event/create?calendar_id=2e789bbf-8eb9-42f0-bc8e-6044542f15d7  -> bookings on that calendar only

The registered path must be lower-case; the parameter is omitted when the payload has no calendar id.

Calendar authorization (direct UI)

When the user connects Google or Outlook from the Scheduling modal:

import {
  configure,
  AuthModel,
  CalendarEmailProvider,
  CALENDAR_AUTH_MESSAGE_TYPE,
} from '@blazeo.com/calendar-client';

configure({ baseUrl: 'https://your-appointment-api.example.com' });

const participantId = '...'; // logged-in participant GUID

// Optional: already connected?
const statusRes = await AuthModel.getAuthorizationStatus(participantId);
if (statusRes.status === 'success' && statusRes.data?.isAuthorized) {
  // show connected UI
}

// Load provider cards (Google / Gmail, Microsoft Outlook)
const providersRes = await AuthModel.getCalendarProviders();
const providers = providersRes.data; // [{ key: 'google', displayName: 'Google / Gmail', ... }, ...]

// User clicks Google
const urlRes = await AuthModel.getAuthorizationUrl(participantId, 'google');
const popup = AuthModel.openOAuthPopup(urlRes.data.authorizationUrl);

// Recommended: waitForAuthorization races the popup's postMessage against polling
// Auth/Status, so you still get a result even if postMessage never arrives (see "Why
// waitForAuthorization / waitForDirectAuthorization" below). Closes the popup for you.
const { promise } = AuthModel.waitForAuthorization(participantId, popup);
const result = await promise;
if (result.status === 'success') {
  // close modal, show success
} else {
  // result.status === 'error' | 'timeout' — result.message has the detail
}

// On modal unmount, if still waiting: cancel();

email_provider for getAuthorizationUrl: google, gmail, 1, outlook, microsoft, or 2.

All methods return Promise<{ status, data?, message? }>. Check response.status === 'success' and use response.data.

Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat below)
const unsubscribe = AuthModel.onCalendarAuthMessage(async (payload) => {
  if (payload.status === 'success') {
    const check = await AuthModel.getAuthorizationStatus(participantId);
    if (check.data?.isAuthorized) {
      // close modal, show success
    }
  }
});

// On modal unmount: unsubscribe(); popup?.close();
Why waitForAuthorization / waitForDirectAuthorization

The OAuth popup (Authorized.html / Error.html) reports back to your app by calling window.opener.postMessage(...). That works only while window.opener is still set. Once the popup navigates away to Google's or Microsoft's own sign-in pages, the browser frequently clears window.opener because of Cross-Origin-Opener-Policy — and it does not come back even when the popup later redirects back to our domain. When that happens, postMessage is never called and a plain onCalendarAuthMessage listener never fires, even though authorization succeeded.

waitForAuthorization and waitForDirectAuthorization fix this by also polling a status endpoint (Auth/Status / Auth/DirectAuthorizationStatus) in the background. Whichever signal arrives first — the postMessage or the poll — resolves the returned promise; the other is cleaned up automatically, and the popup is closed for you. Use onCalendarAuthMessage directly only if you need to keep your own polling/UI logic and don't mind the "postMessage may just never fire" caveat above.

Calendar authorization (self-serve — no participant yet)

Use this instead of getAuthorizationUrl when the logged-in user has no participant record yet and the UI should only show a Google/Outlook button — no name/email/calendar-type form. The backend creates the participant from the provider's own account email/name once OAuth completes, and reports the new id via the participantId field on the onCalendarAuthMessage success payload.

import { configure, AuthModel } from '@blazeo.com/calendar-client';

configure({ baseUrl: 'https://your-appointment-api.example.com' });

const companyKey = '...'; // logged-in user's company

// User clicks "Connect Google Calendar" — no form, just the provider choice.
const startRes = await AuthModel.startDirectAuthorization(companyKey, 'google');
const popup = AuthModel.openOAuthPopup(startRes.data.authorizationUrl);

// Recommended: waitForDirectAuthorization races the popup's postMessage against polling
// Auth/DirectAuthorizationStatus by requestId, so you still get the new participantId even if
// postMessage never arrives (see "Why waitForAuthorization / waitForDirectAuthorization" above).
const { promise } = AuthModel.waitForDirectAuthorization(startRes.data.requestId, popup);
const result = await promise;
if (result.status === 'success') {
  const participantId = result.participantId; // new participant created by the backend
  // store participantId, close modal, show success
} else {
  // result.status === 'error' | 'timeout'. On 'error', result.message may say the signed-in
  // Google/Microsoft account already belongs to another participant in this company.
}

// On modal unmount, if still waiting: cancel();

startDirectAuthorization never creates or updates a participant on the client — everything happens on the backend after the OAuth popup completes. requestId in the response is the OAuth state value; it is not a participant id, but do keep it in memory (e.g. component state) for the lifetime of the popup — waitForDirectAuthorization (and getDirectAuthorizationStatus) need it to poll for the result.

Manual alternative using onCalendarAuthMessage directly (not recommended — see caveat above)
const unsubscribe = AuthModel.onCalendarAuthMessage(async (payload) => {
  if (payload.status === 'success') {
    const participantId = payload.participantId; // new participant created by the backend
    // store participantId, close modal, show success
  } else {
    // e.g. the signed-in Google/Microsoft account already belongs to another
    // participant in this company — payload.message has the detail
  }
});

// On modal unmount: unsubscribe(); popup?.close();

Samples

  • sample/ – Uses the package via file:.. for local development (run npm run build in parent first).
  • sample-npm/ – Uses the package from npm: npm i @blazeo.com/calendar-client. For testing the published package.

Keywords