@bookla-app/react-client-sdk
Version:
React SDK for Bookla Client API
746 lines (730 loc) • 26 kB
JavaScript
'use strict';
var zod = require('zod');
class InterceptorManager {
constructor() {
this.interceptors = [];
}
use(interceptor) {
this.interceptors.push(interceptor);
return () => {
const index = this.interceptors.indexOf(interceptor);
if (index !== -1) {
this.interceptors.splice(index, 1);
}
};
}
async execute(config) {
let result = config;
for (const interceptor of this.interceptors) {
result = await interceptor(result);
}
return result;
}
}
class BooklaError extends Error {
constructor(message, code, status) {
super(message);
this.code = code;
this.status = status;
this.name = "BooklaError";
}
}
class BooklaValidationError extends BooklaError {
constructor(message, details) {
super(message, "VALIDATION_ERROR");
this.details = details;
this.name = "BooklaValidationError";
}
}
class CancelToken {
constructor() {
this.isCancelled = false;
}
cancel() {
this.isCancelled = true;
}
get cancelled() {
return this.isCancelled;
}
}
const storage = {
setTokens(tokens) {
localStorage.setItem("bookla_access_token", tokens.accessToken);
localStorage.setItem("bookla_refresh_token", tokens.refreshToken);
localStorage.setItem("bookla_expires_at", tokens.expiresAt);
},
getTokens() {
const accessToken = localStorage.getItem("bookla_access_token");
const refreshToken = localStorage.getItem("bookla_refresh_token");
const expiresAt = localStorage.getItem("bookla_expires_at");
if (accessToken && refreshToken && expiresAt) {
return { accessToken, refreshToken, expiresAt };
}
return null;
},
clearTokens() {
localStorage.removeItem("bookla_access_token");
localStorage.removeItem("bookla_refresh_token");
localStorage.removeItem("bookla_expires_at");
},
};
const ENDPOINTS = {
bookings: {
list: { path: "/v1/client/bookings", auth: "bearer" },
get: { path: "/v1/client/bookings/{id}", auth: "bearer" },
create: { path: "/v1/client/bookings", auth: "apiKeyOrBearer" },
cancel: {
path: "/v1/client/bookings/{id}/cancel",
auth: "bearer",
},
},
services: {
list: {
path: "/v1/client/companies/{companyId}/services",
auth: "apiKey",
},
get: {
path: "/v1/client/companies/{companyId}/services/{id}",
auth: "apiKey",
},
getTimes: {
path: "/v1/client/companies/{companyId}/services/{id}/times",
auth: "apiKey",
},
getDates: {
path: "/v1/client/companies/{companyId}/services/{id}/dates",
auth: "apiKey",
},
},
resources: {
list: {
path: "/v1/client/companies/{companyId}/resources",
auth: "apiKey",
},
get: {
path: "/v1/client/companies/{companyId}/resources/{id}",
auth: "apiKey",
},
},
subscriptions: {
cart: {
get: {
path: "/v1/companies/{companyId}/plugins/subscription/client/cart",
auth: "bearer",
},
add: {
path: "/v1/companies/{companyId}/plugins/subscription/client/cart",
auth: "bearer",
},
remove: {
path: "/v1/companies/{companyId}/plugins/subscription/client/cart/{itemId}",
auth: "bearer",
},
checkout: {
path: "/v1/companies/{companyId}/plugins/subscription/client/cart/checkout",
auth: "bearer",
},
},
purchases: {
list: {
path: "/v1/companies/{companyId}/plugins/subscription/client/purchases",
auth: "bearer",
},
get: {
path: "/v1/companies/{companyId}/plugins/subscription/client/purchases/{itemId}",
auth: "bearer",
},
renew: {
path: "/v1/companies/{companyId}/plugins/subscription/client/purchases/renew",
auth: "bearer",
},
create: {
path: "/v1/companies/{companyId}/plugins/subscription/client/purchases",
auth: "apiKeyOrBearer",
},
},
available: {
path: "/v1/companies/{companyId}/plugins/subscription/client/subscriptions",
auth: "apiKey",
},
},
giftCards: {
purchases: {
list: {
path: "/v1/companies/{companyId}/plugins/giftcards/client/orders",
auth: "bearer",
},
get: {
path: "/v1/companies/{companyId}/plugins/giftcards/client/orders/{itemId}",
auth: "bearer",
},
create: {
path: "/v1/companies/{companyId}/plugins/giftcards/client/orders",
auth: "apiKeyOrBearer",
},
},
available: {
path: "/v1/companies/{companyId}/plugins/giftcards/client/giftcards",
auth: "apiKey",
},
},
codes: {
validate: {
path: "/v1/client/codes/{code}/validate",
auth: "apiKeyOrBearer",
},
},
auth: {
refresh: {
path: "/v1/client/auth/refresh",
auth: "bearer",
},
},
};
class HttpClient {
constructor(config) {
this.tokens = null;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager(),
};
this.baseURL = config.apiUrl;
this.apiKey = config.apiKey;
this.timeout = config.timeout || 30000;
this.retry = config.retry || {
maxAttempts: 3,
delayMs: 1000,
statusCodesToRetry: [408, 429, 500, 502, 503, 504],
};
this.debug = config.debug || false;
}
setTokens(tokens) {
this.tokens = tokens;
this.saveTokensToStorage(tokens);
}
async isAuthenticated() {
var _a;
if ((_a = this.tokens) === null || _a === void 0 ? void 0 : _a.accessToken) {
const expiresAt = Date.parse(this.tokens.expiresAt);
if (expiresAt < Date.now()) {
const refreshed = await this.refreshToken();
if (!refreshed) {
this.clearAuth();
return { isAuthenticated: false };
}
return {
isAuthenticated: true,
accessToken: this.tokens.accessToken,
expiresAt: Date.parse(this.tokens.expiresAt),
};
}
return {
isAuthenticated: true,
accessToken: this.tokens.accessToken,
expiresAt: Date.parse(this.tokens.expiresAt),
};
}
// Check localStorage as fallback
const storedTokens = this.loadTokensFromStorage();
if (storedTokens) {
this.tokens = storedTokens; // Update current tokens
const expiresAt = Date.parse(storedTokens.expiresAt);
if (expiresAt < Date.now()) {
const refreshed = await this.refreshToken();
if (!refreshed) {
this.clearAuth();
return { isAuthenticated: false };
}
return {
isAuthenticated: true,
accessToken: this.tokens.accessToken,
expiresAt: Date.parse(this.tokens.expiresAt),
};
}
return {
isAuthenticated: true,
accessToken: storedTokens.accessToken,
expiresAt: expiresAt,
};
}
return { isAuthenticated: false };
}
clearAuth() {
this.tokens = null;
storage.clearTokens();
}
saveTokensToStorage(tokens) {
storage.setTokens(tokens);
}
loadTokensFromStorage() {
return storage.getTokens();
}
validateAuth(config) {
var _a;
if (config.auth === "bearer" && !((_a = this.tokens) === null || _a === void 0 ? void 0 : _a.accessToken)) {
throw new BooklaError("Authentication required for this endpoint", "AUTH_REQUIRED");
}
}
async request(config) {
var _a;
this.validateAuth(config);
let attempt = 0;
while (attempt < this.retry.maxAttempts) {
try {
// Check for cancellation
if ((_a = config.cancelToken) === null || _a === void 0 ? void 0 : _a.cancelled) {
throw new BooklaError("Request cancelled", "REQUEST_CANCELLED");
}
// Apply request interceptors
const modifiedConfig = await this.interceptors.request.execute({
...config,
headers: this.getHeaders(config),
});
// Make request
if (this.debug) {
console.log(`[Bookla SDK] Making request:`, modifiedConfig);
}
const response = await fetch(`${this.baseURL}${config.url}`, {
method: config.method,
headers: modifiedConfig.headers,
body: config.data ? JSON.stringify(config.data) : undefined,
signal: AbortSignal.timeout(config.timeout || this.timeout),
});
// Handle response
if (!response.ok) {
throw new BooklaError(response.statusText, "API_ERROR", response.status);
}
const data = await response.json();
// Apply response interceptors
const modifiedResponse = await this.interceptors.response.execute(data);
if (this.debug) {
console.log(`[Bookla SDK] Response received:`, modifiedResponse);
}
return modifiedResponse;
}
catch (error) {
// Handle specific error cases
if (error instanceof BooklaError) {
if (error.code === "REQUEST_CANCELLED") {
throw error;
}
if (error.status === 401) {
const refreshed = await this.refreshToken();
if (!refreshed) {
throw new BooklaError("Token refresh failed", "TOKEN_REFRESH_FAILED");
}
// Retry with new token
attempt++;
continue;
}
if (!this.retry.statusCodesToRetry.includes(error.status || 0)) {
throw error;
}
}
// Retry logic
attempt++;
if (attempt === this.retry.maxAttempts) {
throw new BooklaError("Max retry attempts reached", "MAX_RETRIES_EXCEEDED");
}
await new Promise((resolve) => setTimeout(resolve, this.retry.delayMs * attempt));
}
}
throw new BooklaError("Request failed", "REQUEST_FAILED");
}
getHeaders(config) {
var _a;
const headers = {
"Content-Type": "application/json",
};
if ((config.auth === "bearer" || config.auth === "apiKeyOrBearer") &&
((_a = this.tokens) === null || _a === void 0 ? void 0 : _a.accessToken)) {
headers["Authorization"] = `Bearer ${this.tokens.accessToken}`;
}
else {
headers["X-API-Key"] = this.apiKey;
}
return headers;
}
async refreshToken() {
var _a;
try {
if (!((_a = this.tokens) === null || _a === void 0 ? void 0 : _a.refreshToken)) {
return false;
}
const response = await fetch(`${this.baseURL}${ENDPOINTS.auth.refresh.path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.tokens.refreshToken}`,
},
});
if (!response.ok) {
return false;
}
const newTokens = await response.json();
this.setTokens(newTokens);
return true;
}
catch (error) {
return false;
}
}
async get(endpoint, options) {
return this.request({
method: "GET",
url: endpoint.path,
auth: endpoint.auth,
...options,
});
}
async post(endpoint, data, options) {
return this.request({
method: "POST",
url: endpoint.path,
data,
auth: endpoint.auth,
...options,
});
}
async delete(endpoint, options) {
return this.request({
method: "DELETE",
url: endpoint.path,
auth: endpoint.auth,
...options,
});
}
createCancelToken() {
return new CancelToken();
}
isCancelledError(error) {
return error instanceof BooklaError && error.code === "REQUEST_CANCELLED";
}
}
class BookingsService {
constructor(client) {
this.client = client;
}
async list(options) {
return this.client.get(ENDPOINTS.bookings.list, options);
}
async get(id, options) {
return this.client.get({
...ENDPOINTS.bookings.get,
path: ENDPOINTS.bookings.get.path.replace("{id}", id),
}, options);
}
async request(data, options) {
this.validateCreateBookingRequest(data);
return this.client.post(ENDPOINTS.bookings.create, data, options);
}
async cancel(bookingId, reason, options) {
return this.client.post({
...ENDPOINTS.bookings.cancel,
path: ENDPOINTS.bookings.cancel.path.replace("{Id}", bookingId),
}, { reason }, options);
}
validateCreateBookingRequest(data) {
const schema = zod.z.object({
companyID: zod.z.string().min(1),
serviceID: zod.z.string().min(1),
resourceID: zod.z.string().optional(),
startTime: zod.z.string().datetime(),
spots: zod.z.number().optional(),
duration: zod.z.string().optional(),
metaData: zod.z.record(zod.z.unknown()).optional(),
pluginData: zod.z.record(zod.z.unknown()).optional(),
tickets: zod.z.record(zod.z.number()).optional(),
customPurchaseDescription: zod.z.string().optional(),
client: zod.z
.object({
id: zod.z.string().optional(),
booklaID: zod.z.string().optional(),
firstName: zod.z.string().optional(),
lastName: zod.z.string().optional(),
email: zod.z.string().email().optional(),
})
.nullish(),
});
return schema.parse(data);
}
}
class ServicesService {
constructor(client) {
this.client = client;
}
async list(companyId, options) {
return this.client.get({
...ENDPOINTS.services.list,
path: ENDPOINTS.services.list.path.replace("{companyId}", companyId),
}, options);
}
async get(companyId, serviceId, options) {
return this.client.get({
...ENDPOINTS.services.list,
path: ENDPOINTS.services.get.path
.replace("{companyId}", companyId)
.replace("{id}", serviceId),
}, options);
}
async getTimes(companyId, serviceId, data, options) {
this.validateGetTimesRequest(data);
return this.client.post({
...ENDPOINTS.services.getTimes,
path: ENDPOINTS.services.getTimes.path
.replace("{companyId}", companyId)
.replace("{id}", serviceId),
}, data, options);
}
async getDates(companyId, serviceId, data, options) {
this.validateGetDatesRequest(data);
return this.client.post({
...ENDPOINTS.services.getDates,
path: ENDPOINTS.services.getDates.path
.replace("{companyId}", companyId)
.replace("{id}", serviceId),
}, data, options);
}
validateGetTimesRequest(data) {
const schema = zod.z.object({
from: zod.z.string().datetime(),
to: zod.z.string().datetime(),
duration: zod.z.string().optional(),
spots: zod.z.number().optional(),
resourceIDs: zod.z.array(zod.z.string()).optional(),
tickets: zod.z.record(zod.z.number()).optional(),
});
return schema.parse(data);
}
validateGetDatesRequest(data) {
const schema = zod.z.object({
from: zod.z.string().datetime(),
to: zod.z.string().datetime(),
duration: zod.z.string().optional(),
spots: zod.z.number().optional(),
resourceIDs: zod.z.array(zod.z.string()).optional(),
tickets: zod.z.record(zod.z.number()).optional(),
});
return schema.parse(data);
}
}
class ResourcesService {
constructor(client) {
this.client = client;
}
async list(companyId, options) {
return this.client.get({
...ENDPOINTS.resources.list,
path: ENDPOINTS.resources.list.path.replace("{companyId}", companyId),
}, options);
}
async get(companyId, resourceId, options) {
return this.client.get({
...ENDPOINTS.resources.get,
path: ENDPOINTS.resources.get.path
.replace("{companyId}", companyId)
.replace("{id}", resourceId),
}, options);
}
}
class CodesService {
constructor(client) {
this.client = client;
}
async validateCode(code, data, options) {
this.validateCodeRequest(data);
return this.client.post({
...ENDPOINTS.codes.validate,
path: ENDPOINTS.codes.validate.path.replace("{code}", code),
}, data, options);
}
validateCodeRequest(data) {
const schema = zod.z.object({
companyID: zod.z.string().min(1),
serviceID: zod.z.string().min(1),
resourceID: zod.z.string().min(1),
startTime: zod.z.string().datetime(),
duration: zod.z.string().optional(),
spots: zod.z.number().min(1),
tickets: zod.z.record(zod.z.number()).optional(),
});
return schema.parse(data);
}
}
class ClientSubscriptionService {
constructor(client) {
this.client = client;
}
validateAddToCartRequest(data) {
const schema = zod.z.object({
items: zod.z.array(zod.z.object({
subscriptionID: zod.z.string().min(1),
metaData: zod.z.record(zod.z.unknown()).optional(),
})),
});
return schema.parse(data);
}
validateRenewRequest(data) {
const schema = zod.z.object({
ids: zod.z.array(zod.z.string().min(1)),
});
return schema.parse(data);
}
async getCart(companyId, options) {
return this.client.get({
...ENDPOINTS.subscriptions.cart.get,
path: ENDPOINTS.subscriptions.cart.get.path.replace("{companyId}", companyId),
}, options);
}
async addToCart(companyId, data, options) {
this.validateAddToCartRequest(data);
return this.client.post({
...ENDPOINTS.subscriptions.cart.add,
path: ENDPOINTS.subscriptions.cart.add.path.replace("{companyId}", companyId),
}, data, options);
}
async removeFromCart(companyId, itemId, options) {
return this.client.delete({
...ENDPOINTS.subscriptions.cart.remove,
path: ENDPOINTS.subscriptions.cart.remove.path
.replace("{companyId}", companyId)
.replace("{itemId}", itemId),
}, options);
}
async checkout(companyId, options) {
return this.client.post({
...ENDPOINTS.subscriptions.cart.checkout,
path: ENDPOINTS.subscriptions.cart.checkout.path.replace("{companyId}", companyId),
}, undefined, options);
}
async getPurchases(companyId, options) {
return this.client.get({
...ENDPOINTS.subscriptions.purchases.list,
path: ENDPOINTS.subscriptions.purchases.list.path.replace("{companyId}", companyId),
}, options);
}
async getPurchase(companyId, itemId, options) {
return this.client.get({
...ENDPOINTS.subscriptions.purchases.get,
path: ENDPOINTS.subscriptions.purchases.get.path
.replace("{companyId}", companyId)
.replace("{itemId}", itemId),
}, options);
}
async renewPurchases(companyId, data, options) {
this.validateRenewRequest(data);
return this.client.post({
...ENDPOINTS.subscriptions.purchases.renew,
path: ENDPOINTS.subscriptions.purchases.renew.path.replace("{companyId}", companyId),
}, data, options);
}
async getAvailableSubscriptions(companyId, ids, options) {
const queryParams = ids ? `?ids=${ids.join(",")}` : "";
return this.client.get({
...ENDPOINTS.subscriptions.available,
path: `${ENDPOINTS.subscriptions.available.path.replace("{companyId}", companyId)}${queryParams}`,
}, options);
}
async createPurchase(companyId, data, options) {
const schema = zod.z.object({
items: zod.z.array(zod.z.object({
subscriptionID: zod.z.string().min(1),
metaData: zod.z.record(zod.z.unknown()).optional(),
})),
client: zod.z
.object({
id: zod.z.string().optional(),
booklaID: zod.z.string().optional(),
firstName: zod.z.string().optional(),
lastName: zod.z.string().optional(),
email: zod.z.string().email().optional(),
})
.nullish(),
});
const validatedData = schema.parse(data);
return this.client.post({
...ENDPOINTS.subscriptions.purchases.create,
path: ENDPOINTS.subscriptions.purchases.create.path.replace("{companyId}", companyId),
}, validatedData, options);
}
}
class ClientGiftCardsService {
constructor(client) {
this.client = client;
}
async getPurchases(companyId, options) {
return this.client.get({
...ENDPOINTS.giftCards.purchases.list,
path: ENDPOINTS.giftCards.purchases.list.path.replace("{companyId}", companyId),
}, options);
}
async getPurchase(companyId, itemId, options) {
return this.client.get({
...ENDPOINTS.giftCards.purchases.get,
path: ENDPOINTS.giftCards.purchases.get.path
.replace("{companyId}", companyId)
.replace("{itemId}", itemId),
}, options);
}
async getAvailableGiftCards(companyId, ids, options) {
const queryParams = ids ? `?ids=${ids.join(",")}` : "";
return this.client.get({
...ENDPOINTS.giftCards.available,
path: `${ENDPOINTS.giftCards.available.path.replace("{companyId}", companyId)}${queryParams}`,
}, options);
}
async createPurchase(companyId, data, options) {
const schema = zod.z.object({
giftCardID: zod.z.string().min(1),
metaData: zod.z.record(zod.z.unknown()).optional(),
client: zod.z
.object({
id: zod.z.string().optional(),
booklaID: zod.z.string().optional(),
firstName: zod.z.string().optional(),
lastName: zod.z.string().optional(),
email: zod.z.string().email().optional(),
})
.nullish(),
});
const validatedData = schema.parse(data);
return this.client.post({
...ENDPOINTS.giftCards.purchases.create,
path: ENDPOINTS.giftCards.purchases.create.path.replace("{companyId}", companyId),
}, validatedData, options);
}
}
class BooklaSDK {
constructor(config) {
this.client = new HttpClient(config);
this.bookings = new BookingsService(this.client);
this.services = new ServicesService(this.client);
this.resources = new ResourcesService(this.client);
this.codes = new CodesService(this.client);
this.subscriptions = new ClientSubscriptionService(this.client);
this.giftCards = new ClientGiftCardsService(this.client);
}
setAuthTokens(tokens) {
this.client.setTokens(tokens);
}
async isAuthenticated() {
return this.client.isAuthenticated();
}
clearAuth() {
return this.client.clearAuth();
}
get interceptors() {
return this.client.interceptors;
}
createCancelToken() {
return this.client.createCancelToken();
}
isCancelledError(error) {
return this.client.isCancelledError(error);
}
}
exports.BooklaError = BooklaError;
exports.BooklaSDK = BooklaSDK;
exports.BooklaValidationError = BooklaValidationError;
exports.CancelToken = CancelToken;
//# sourceMappingURL=index.js.map