UNPKG

@trycourier/courier-js

Version:

A browser-safe API wrapper

1,588 lines (1,587 loc) 49.4 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); var ClientAction = /* @__PURE__ */ ((ClientAction2) => { ClientAction2["Subscribe"] = "subscribe"; ClientAction2["Unsubscribe"] = "unsubscribe"; ClientAction2["Pong"] = "pong"; ClientAction2["Ping"] = "ping"; ClientAction2["GetConfig"] = "get-config"; return ClientAction2; })(ClientAction || {}); var ServerAction = /* @__PURE__ */ ((ServerAction2) => { ServerAction2["Ping"] = "ping"; return ServerAction2; })(ServerAction || {}); var InboxMessageEvent = /* @__PURE__ */ ((InboxMessageEvent2) => { InboxMessageEvent2["NewMessage"] = "message"; InboxMessageEvent2["Archive"] = "archive"; InboxMessageEvent2["ArchiveAll"] = "archive-all"; InboxMessageEvent2["ArchiveRead"] = "archive-read"; InboxMessageEvent2["Clicked"] = "clicked"; InboxMessageEvent2["MarkAllRead"] = "mark-all-read"; InboxMessageEvent2["Opened"] = "opened"; InboxMessageEvent2["Read"] = "read"; InboxMessageEvent2["Unarchive"] = "unarchive"; InboxMessageEvent2["Unopened"] = "unopened"; InboxMessageEvent2["Unread"] = "unread"; return InboxMessageEvent2; })(InboxMessageEvent || {}); const getCourierApiUrls = (urls) => ({ courier: { rest: (urls == null ? void 0 : urls.courier.rest) || "https://api.courier.com", graphql: (urls == null ? void 0 : urls.courier.graphql) || "https://api.courier.com/client/q" }, inbox: { graphql: (urls == null ? void 0 : urls.inbox.graphql) || "https://inbox.courier.com/q", webSocket: (urls == null ? void 0 : urls.inbox.webSocket) || "wss://realtime.courier.io" } }); class Logger { constructor(showLogs) { __publicField(this, "PREFIX", "[COURIER]"); this.showLogs = showLogs; } warn(message, ...args) { if (this.showLogs) { console.warn(`${this.PREFIX} ${message}`, ...args); } } log(message, ...args) { if (this.showLogs) { console.log(`${this.PREFIX} ${message}`, ...args); } } error(message, ...args) { if (this.showLogs) { console.error(`${this.PREFIX} ${message}`, ...args); } } debug(message, ...args) { if (this.showLogs) { console.debug(`${this.PREFIX} ${message}`, ...args); } } info(message, ...args) { if (this.showLogs) { console.info(`${this.PREFIX} ${message}`, ...args); } } } const _UUID = class _UUID { /** * nanoid * Copyright 2017 Andrey Sitnik <andrey@sitnik.ru> * * https://github.com/ai/nanoid/blob/main/LICENSE * * @param size - The size of the UUID to generate. * @returns A string representing the UUID. */ static nanoid(size = 21) { let id = ""; let bytes = crypto.getRandomValues(new Uint8Array(size |= 0)); while (size--) { id += _UUID.ALPHABET[bytes[size] & 63]; } return id; } }; __publicField(_UUID, "ALPHABET", "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"); let UUID = _UUID; class CourierRequestError extends Error { constructor(code, message, type) { super(message); this.code = code; this.type = type; this.name = "CourierRequestError"; } } function logRequest(logger, uid, type, data) { logger.log(` 📡 New Courier ${type} Request: ${uid} URL: ${data.url} ${data.method ? `Method: ${data.method}` : ""} ${data.query ? `Query: ${data.query}` : ""} ${data.variables ? `Variables: ${JSON.stringify(data.variables, null, 2)}` : ""} Headers: ${JSON.stringify(data.headers, null, 2)} Body: ${data.body ? JSON.stringify(data.body, null, 2) : "Empty"} `); } function logResponse(logger, uid, type, data) { logger.log(` 📡 New Courier ${type} Response: ${uid} Status Code: ${data.status} Response JSON: ${JSON.stringify(data.response, null, 2)} `); } async function http(props) { const validCodes = props.validCodes ?? [200]; const uid = props.options.showLogs ? UUID.nanoid() : void 0; const request = new Request(props.url, { method: props.method, headers: { "Content-Type": "application/json", ...props.headers }, body: props.body ? JSON.stringify(props.body) : void 0 }); if (uid) { logRequest(props.options.logger, uid, "HTTP", { url: request.url, method: request.method, headers: Object.fromEntries(request.headers.entries()), body: props.body }); } const response = await fetch(request); if (response.status === 204) { return; } let data; try { data = await response.json(); } catch (error) { if (response.status === 200) { return; } throw new CourierRequestError( response.status, "Failed to parse response as JSON", "PARSE_ERROR" ); } if (uid) { logResponse(props.options.logger, uid, "HTTP", { status: response.status, response: data }); } if (!validCodes.includes(response.status)) { throw new CourierRequestError( response.status, (data == null ? void 0 : data.message) || "Unknown Error", data == null ? void 0 : data.type ); } return data; } async function graphql(props) { const uid = props.options.showLogs ? UUID.nanoid() : void 0; if (uid) { logRequest(props.options.logger, uid, "GraphQL", { url: props.url, headers: props.headers, query: props.query, variables: props.variables }); } const response = await fetch(props.url, { method: "POST", headers: { "Content-Type": "application/json", ...props.headers }, body: JSON.stringify({ query: props.query, variables: props.variables }) }); let data; try { data = await response.json(); } catch (error) { throw new CourierRequestError( response.status, "Failed to parse response as JSON", "PARSE_ERROR" ); } if (uid) { logResponse(props.options.logger, uid, "GraphQL", { status: response.status, response: data }); } if (!response.ok) { throw new CourierRequestError( response.status, (data == null ? void 0 : data.message) || "Unknown Error", data == null ? void 0 : data.type ); } return data; } class Client { constructor(options) { this.options = options; } } class BrandClient extends Client { /** * Get a brand by ID using GraphQL * @param brandId - The ID of the brand to retrieve * @returns Promise resolving to the requested brand */ async getBrand(props) { const query = ` query GetBrand { brand(brandId: "${props.brandId}") { settings { colors { primary secondary tertiary } inapp { borderRadius disableCourierFooter } } } } `; const json = await graphql({ options: this.options, url: this.options.apiUrls.courier.graphql, headers: { "x-courier-user-id": this.options.userId, "x-courier-client-key": "empty", // Empty for now. Will be removed in future. "Authorization": `Bearer ${this.options.accessToken}` }, query, variables: { brandId: props.brandId } }); return json.data.brand; } } const CLOSE_CODE_NORMAL_CLOSURE = 1e3; const INBOX_WIRE_PROTOCOL_VERSION = "v1"; const _CourierSocket = class _CourierSocket { constructor(options) { /** The WebSocket instance, which may be null if the connection is not established. */ __publicField(this, "webSocket", null); /** The number of connection retry attempts so far, reset after a successful connection. */ __publicField(this, "retryAttempt", 0); /** The timeout ID for the current connectionretry attempt, reset when we attempt to connect. */ __publicField(this, "retryTimeoutId", null); /** * Flag indicating the application initiated a {@link CourierSocket#close} call. * * An application-initiated close may look like an abnormal closure (code 1006) * if it occurs before the connection is established. We differentiate to * prevent retrying the connection when the socket is closed intentionally. */ __publicField(this, "closeRequested", false); __publicField(this, "url"); __publicField(this, "options"); this.url = options.apiUrls.inbox.webSocket; this.options = options; } /** * Connects to the Courier WebSocket server. * * If the connection is already established, this is a no-op. * * @returns A promise that resolves when the connection is established or rejects if the connection could not be established. */ async connect() { var _a, _b; if (this.isConnecting || this.isOpen) { (_b = this.options.logger) == null ? void 0 : _b.info(`Attempted to open a WebSocket connection, but one already exists in state '${(_a = this.webSocket) == null ? void 0 : _a.readyState}'.`); return Promise.resolve(); } this.clearRetryTimeout(); this.closeRequested = false; return new Promise((resolve, reject) => { this.webSocket = new WebSocket(this.getWebSocketUrl()); this.webSocket.addEventListener("open", (event) => { this.retryAttempt = 0; this.onOpen(event); resolve(); }); this.webSocket.addEventListener("message", async (event) => { var _a2; try { const json = JSON.parse(event.data); if ("event" in json && json.event === "reconnect") { this.close(CLOSE_CODE_NORMAL_CLOSURE); await this.retryConnection(json.retryAfter * 1e3); return; } this.onMessageReceived(json); } catch (error) { (_a2 = this.options.logger) == null ? void 0 : _a2.error("Error parsing socket message", error); } }); this.webSocket.addEventListener("close", (event) => { if (event.code !== CLOSE_CODE_NORMAL_CLOSURE && !this.closeRequested) { const courierCloseEvent = _CourierSocket.parseCloseEvent(event); if (courierCloseEvent.retryAfterSeconds) { this.retryConnection(courierCloseEvent.retryAfterSeconds * 1e3); } else { this.retryConnection(); } } this.onClose(event); }); this.webSocket.addEventListener("error", (event) => { if (!this.closeRequested) { this.retryConnection(); } this.onError(event); reject(event); }); }); } /** * Closes the WebSocket connection. * * See {@link https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close} for more details. * * @param code The WebSocket close code. Defaults to {@link CLOSE_CODE_NORMAL_CLOSURE}. * @param reason The WebSocket close reason. */ close(code = CLOSE_CODE_NORMAL_CLOSURE, reason) { if (this.webSocket === null) { return; } this.closeRequested = true; this.clearRetryTimeout(); this.retryAttempt = 0; this.webSocket.close(code, reason); } /** * Sends a message to the Courier WebSocket server. * * @param message The message to send. The message will be serialized to a JSON string. */ send(message) { var _a; if (this.webSocket === null || this.isConnecting) { (_a = this.options.logger) == null ? void 0 : _a.info("Attempted to send a message, but the WebSocket is not yet open."); return; } const json = JSON.stringify(message); this.webSocket.send(json); } get userId() { return this.options.userId; } /** The sub-tenant ID, if specified by the user. */ get subTenantId() { return this.options.tenantId; } get logger() { return this.options.logger; } /** * Whether the WebSocket connection is currently being established. */ get isConnecting() { return this.webSocket !== null && this.webSocket.readyState === WebSocket.CONNECTING; } /** * Whether the WebSocket connection is currently open. */ get isOpen() { return this.webSocket !== null && this.webSocket.readyState === WebSocket.OPEN; } /** * Constructs the WebSocket URL for the Courier WebSocket server using context * from the {@link CourierClientOptions} passed to the constructor. * * @returns The WebSocket URL */ getWebSocketUrl() { const accessToken = this.options.accessToken; const connectionId = this.options.connectionId; const userId = this.userId; return `${this.url}?auth=${accessToken}&cid=${connectionId}&iwpv=${INBOX_WIRE_PROTOCOL_VERSION}&userId=${userId}`; } /** * Parses the Retry-After time from the WebSocket close event reason, * and returns a new {@link CourierCloseEvent} with the retry after time in seconds * if present. * * The Courier WebSocket server may send the close event reason in the following format: * * ```json * { * "Retry-After": "10" // The retry after time in seconds * } * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/reason * * @param closeEvent The WebSocket close event. * @returns The WebSocket close event with the retry after time in seconds. */ static parseCloseEvent(closeEvent) { if (closeEvent.reason === null || closeEvent.reason === "") { return closeEvent; } try { const jsonReason = JSON.parse(closeEvent.reason); if (!jsonReason[_CourierSocket.RETRY_AFTER_KEY]) { return closeEvent; } const retryAfterSeconds = parseInt(jsonReason[_CourierSocket.RETRY_AFTER_KEY]); if (Number.isNaN(retryAfterSeconds) || retryAfterSeconds < 0) { return closeEvent; } return { ...closeEvent, retryAfterSeconds }; } catch (error) { return closeEvent; } } /** * Calculates the retry backoff time in milliseconds based on the current retry attempt. */ getBackoffTimeInMillis() { const backoffIntervalInMillis = _CourierSocket.BACKOFF_INTERVALS_IN_MILLIS[this.retryAttempt]; const lowerBound = backoffIntervalInMillis - backoffIntervalInMillis * _CourierSocket.BACKOFF_JITTER_FACTOR; const upperBound = backoffIntervalInMillis + backoffIntervalInMillis * _CourierSocket.BACKOFF_JITTER_FACTOR; return Math.floor(Math.random() * (upperBound - lowerBound) + lowerBound); } /** * Retries the connection to the Courier WebSocket server after * either {@param suggestedBackoffTimeInMillis} or a random backoff time * calculated using {@link getBackoffTimeInMillis}. * * @param suggestedBackoffTimeInMillis The suggested backoff time in milliseconds. * @returns A promise that resolves when the connection is established or rejects if the connection could not be established. */ async retryConnection(suggestedBackoffTimeInMillis) { var _a, _b, _c; if (this.retryTimeoutId !== null) { (_a = this.logger) == null ? void 0 : _a.debug("Skipping retry attempt because a previous retry is already scheduled."); return; } if (this.retryAttempt >= _CourierSocket.MAX_RETRY_ATTEMPTS) { (_b = this.logger) == null ? void 0 : _b.error(`Max retry attempts (${_CourierSocket.MAX_RETRY_ATTEMPTS}) reached.`); return; } const backoffTimeInMillis = suggestedBackoffTimeInMillis ?? this.getBackoffTimeInMillis(); this.retryTimeoutId = window.setTimeout(async () => { try { await this.connect(); } catch (error) { } }, backoffTimeInMillis); (_c = this.logger) == null ? void 0 : _c.debug(`Retrying connection in ${Math.floor(backoffTimeInMillis / 1e3)}s. Retry attempt ${this.retryAttempt + 1} of ${_CourierSocket.MAX_RETRY_ATTEMPTS}.`); this.retryAttempt++; } /** * Clears the retry timeout if it exists. */ clearRetryTimeout() { if (this.retryTimeoutId !== null) { window.clearTimeout(this.retryTimeoutId); this.retryTimeoutId = null; } } }; /** * The jitter factor for the backoff intervals. * * Backoff with jitter is calculated as a random value in the range: * [BACKOFF_INTERVAL - BACKOFF_JITTER_FACTOR * BACKOFF_INTERVAL, * BACKOFF_INTERVAL + BACKOFF_JITTER_FACTOR * BACKOFF_INTERVAL). */ __publicField(_CourierSocket, "BACKOFF_JITTER_FACTOR", 0.5); /** * The maximum number of retry attempts. */ __publicField(_CourierSocket, "MAX_RETRY_ATTEMPTS", 5); /** * Backoff intervals in milliseconds. * * Each represents an offset from the previous interval, rather than a * absolute offset from the initial request time. */ __publicField(_CourierSocket, "BACKOFF_INTERVALS_IN_MILLIS", [ 3e4, // 30 seconds 6e4, // 1 minute 12e4, // 2 minutes 24e4, // 4 minutes 48e4 // 8 minutes ]); /** * The key of the retry after time in the WebSocket close event reason. * * The Courier WebSocket server may send the close event reason in the following format: * * ```json * { * "Retry-After": "10" // The retry after time in seconds * } * ``` * * @see https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/reason */ __publicField(_CourierSocket, "RETRY_AFTER_KEY", "Retry-After"); let CourierSocket = _CourierSocket; class TransactionManager { constructor(completedTransactionsToKeep = 10) { /** * The map of <transactionId, Transaction> representing outstanding requests. */ __publicField(this, "outstandingRequestsMap", /* @__PURE__ */ new Map()); /** * The queue of completed requests. This is a FIFO queue of the last N completed requests, * where N is {@link completedTransactionsToKeep}. */ __publicField(this, "completedTransactionsQueue", []); /** * Number of completed requests to keep in memory. */ __publicField(this, "completedTransactionsToKeep"); this.completedTransactionsToKeep = completedTransactionsToKeep; } addOutstandingRequest(transactionId, request) { const isOutstanding = this.outstandingRequestsMap.has(transactionId); if (isOutstanding) { throw new Error(`Transaction [${transactionId}] already has an outstanding request`); } const transaction = { transactionId, request, response: null, start: /* @__PURE__ */ new Date(), end: null }; this.outstandingRequestsMap.set(transactionId, transaction); } addResponse(transactionId, response) { const transaction = this.outstandingRequestsMap.get(transactionId); if (transaction === void 0) { throw new Error(`Transaction [${transactionId}] does not have an outstanding request`); } transaction.response = response; transaction.end = /* @__PURE__ */ new Date(); this.outstandingRequestsMap.delete(transactionId); this.addCompletedTransaction(transaction); } get outstandingRequests() { return Array.from(this.outstandingRequestsMap.values()); } get completedTransactions() { return this.completedTransactionsQueue; } clearOutstandingRequests() { this.outstandingRequestsMap.clear(); } /** * Adds a completed request to the queue. * * If the number of completed requests exceeds the maximum number of completed requests to keep, * remove the oldest completed request. */ addCompletedTransaction(transaction) { this.completedTransactionsQueue.push(transaction); if (this.completedTransactionsQueue.length > this.completedTransactionsToKeep) { this.completedTransactionsQueue.shift(); } } } function ensureCreatedTime(envelope) { if (envelope.event === InboxMessageEvent.NewMessage) { const message = envelope.data; if (!message.created) { message.created = (/* @__PURE__ */ new Date()).toISOString(); } return { ...envelope, data: message }; } return envelope; } function fixMessageEventEnvelope(envelope) { return ensureCreatedTime(envelope); } const _CourierInboxSocket = class _CourierInboxSocket extends CourierSocket { constructor(options) { super(options); /** * The interval ID for the ping interval. * * @see https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval */ __publicField(this, "pingIntervalId", null); /** * The list of message event listeners, called when a message event is received * from the Courier WebSocket server. */ __publicField(this, "messageEventListeners", []); /** Server-provided configuration for the client. */ __publicField(this, "config", null); /** * The transaction manager, used to track outstanding requests and responses. */ __publicField(this, "pingTransactionManager", new TransactionManager()); } onOpen(_) { this.pingTransactionManager.clearOutstandingRequests(); this.restartPingInterval(); this.sendGetConfig(); this.sendSubscribe(); return Promise.resolve(); } onMessageReceived(data) { if ("action" in data && data.action === ServerAction.Ping) { const envelope = data; this.sendPong(envelope); } if ("response" in data && data.response === "pong") { const envelope = data; this.pingTransactionManager.addResponse(envelope.tid, envelope); this.pingTransactionManager.clearOutstandingRequests(); } if ("response" in data && data.response === "config") { const envelope = data; this.setConfig(envelope.data); } if ("event" in data && _CourierInboxSocket.isInboxMessageEvent(data.event)) { const envelope = data; const fixedEnvelope = fixMessageEventEnvelope(envelope); for (const listener of this.messageEventListeners) { listener(fixedEnvelope); } } this.restartPingInterval(); return Promise.resolve(); } onClose(_) { this.clearPingInterval(); this.clearMessageEventListeners(); this.pingTransactionManager.clearOutstandingRequests(); return Promise.resolve(); } onError(_) { return Promise.resolve(); } /** * Sends a subscribe message to the server. * * Subscribes to all events for the user. */ sendSubscribe() { const data = { channel: this.userId, event: "*" }; if (this.subTenantId) { data.accountId = this.subTenantId; } const envelope = { tid: UUID.nanoid(), action: ClientAction.Subscribe, data }; this.send(envelope); } /** * Sends an unsubscribe message to the server. * * Unsubscribes from all events for the user. */ sendUnsubscribe() { const envelope = { tid: UUID.nanoid(), action: ClientAction.Unsubscribe, data: { channel: this.userId } }; this.send(envelope); } /** * Adds a message event listener, called when a message event is received * from the Courier WebSocket server. * * @param listener The listener function */ addMessageEventListener(listener) { this.messageEventListeners.push(listener); } /** * Send a ping message to the server. * * ping/pong is implemented at the application layer since the browser's * WebSocket implementation does not support control-level ping/pong. */ sendPing() { var _a; if (this.pingTransactionManager.outstandingRequests.length >= this.maxOutstandingPings) { (_a = this.logger) == null ? void 0 : _a.debug("Max outstanding pings reached, retrying connection."); this.close(CLOSE_CODE_NORMAL_CLOSURE, "Max outstanding pings reached, retrying connection."); this.retryConnection(); return; } const envelope = { tid: UUID.nanoid(), action: ClientAction.Ping }; this.send(envelope); this.pingTransactionManager.addOutstandingRequest(envelope.tid, envelope); } /** * Send a pong response to the server. * * ping/pong is implemented at the application layer since the browser's * WebSocket implementation does not support control-level ping/pong. */ sendPong(incomingMessage) { const response = { tid: incomingMessage.tid, action: ClientAction.Pong }; this.send(response); } /** * Send a request for the client's configuration. */ sendGetConfig() { const envelope = { tid: UUID.nanoid(), action: ClientAction.GetConfig }; this.send(envelope); } /** * Restart the ping interval, clearing the previous interval if it exists. */ restartPingInterval() { this.clearPingInterval(); this.pingIntervalId = window.setInterval(() => { this.sendPing(); }, this.pingInterval); } clearPingInterval() { if (this.pingIntervalId) { window.clearInterval(this.pingIntervalId); } } get pingInterval() { if (this.config) { return this.config.pingInterval * 1e3; } return _CourierInboxSocket.DEFAULT_PING_INTERVAL_MILLIS; } get maxOutstandingPings() { if (this.config) { return this.config.maxOutstandingPings; } return _CourierInboxSocket.DEFAULT_MAX_OUTSTANDING_PINGS; } setConfig(config) { this.config = config; } /** * Removes all message event listeners. */ clearMessageEventListeners() { this.messageEventListeners = []; } static isInboxMessageEvent(event) { return Object.values(InboxMessageEvent).includes(event); } }; /** * The default interval in milliseconds at which to send a ping message to the server * if no other message has been received from the server. * * Fallback when the server does not provide a config. */ __publicField(_CourierInboxSocket, "DEFAULT_PING_INTERVAL_MILLIS", 6e4); // 1 minute /** * The default maximum number of outstanding pings before the client should * close the connection and retry connecting. * * Fallback when the server does not provide a config. */ __publicField(_CourierInboxSocket, "DEFAULT_MAX_OUTSTANDING_PINGS", 3); let CourierInboxSocket = _CourierInboxSocket; class InboxClient extends Client { constructor(options) { super(options); __publicField(this, "socket"); this.socket = new CourierInboxSocket(options); } /** * Get paginated messages * @param paginationLimit - Number of messages to return per page (default: 24) * @param startCursor - Cursor for pagination * @returns Promise resolving to paginated messages response */ async getMessages(props) { const query = ` query GetInboxMessages( $params: FilterParamsInput = { ${this.options.tenantId ? `accountId: "${this.options.tenantId}"` : ""} } $limit: Int = ${(props == null ? void 0 : props.paginationLimit) ?? 24} $after: String ${(props == null ? void 0 : props.startCursor) ? `= "${props.startCursor}"` : ""} ) { count(params: $params) messages(params: $params, limit: $limit, after: $after) { totalCount pageInfo { startCursor hasNextPage } nodes { messageId read archived created opened title preview data tags trackingIds { clickTrackingId } actions { content data href } } } } `; return await graphql({ options: this.options, query, headers: { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }, url: this.options.apiUrls.inbox.graphql }); } /** * Get paginated archived messages * @param paginationLimit - Number of messages to return per page (default: 24) * @param startCursor - Cursor for pagination * @returns Promise resolving to paginated archived messages response */ async getArchivedMessages(props) { const query = ` query GetInboxMessages( $params: FilterParamsInput = { ${this.options.tenantId ? `accountId: "${this.options.tenantId}"` : ""}, archived: true } $limit: Int = ${(props == null ? void 0 : props.paginationLimit) ?? 24} $after: String ${(props == null ? void 0 : props.startCursor) ? `= "${props.startCursor}"` : ""} ) { count(params: $params) messages(params: $params, limit: $limit, after: $after) { totalCount pageInfo { startCursor hasNextPage } nodes { messageId read archived created opened title preview data tags trackingIds { clickTrackingId } actions { content data href } } } } `; return graphql({ options: this.options, query, headers: { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }, url: this.options.apiUrls.inbox.graphql }); } /** * Get unread message count * @returns Promise resolving to number of unread messages */ async getUnreadMessageCount() { var _a; const query = ` query GetMessages { count(params: { status: "unread" ${this.options.tenantId ? `, accountId: "${this.options.tenantId}"` : ""} }) } `; const response = await graphql({ options: this.options, query, headers: { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }, url: this.options.apiUrls.inbox.graphql }); return ((_a = response.data) == null ? void 0 : _a.count) ?? 0; } /** * Track a click event * @param messageId - ID of the message * @param trackingId - ID for tracking the click * @returns Promise resolving when click is tracked */ async click(props) { const query = ` mutation TrackEvent { clicked(messageId: "${props.messageId}", trackingId: "${props.trackingId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Mark a message as read * @param messageId - ID of the message to mark as read * @returns Promise resolving when message is marked as read */ async read(props) { const query = ` mutation TrackEvent { read(messageId: "${props.messageId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Mark a message as unread * @param messageId - ID of the message to mark as unread * @returns Promise resolving when message is marked as unread */ async unread(props) { const query = ` mutation TrackEvent { unread(messageId: "${props.messageId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Mark a message as opened * @param messageId - ID of the message to mark as opened * @returns Promise resolving when message is marked as opened */ async open(props) { const query = ` mutation TrackEvent { opened(messageId: "${props.messageId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Archive a message * @param messageId - ID of the message to archive * @returns Promise resolving when message is archived */ async archive(props) { const query = ` mutation TrackEvent { archive(messageId: "${props.messageId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Unarchive a message * @param messageId - ID of the message to unarchive * @returns Promise resolving when message is unarchived */ async unarchive(props) { const query = ` mutation TrackEvent { unarchive(messageId: "${props.messageId}") } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Mark all messages as read * @returns Promise resolving when all messages are marked as read */ async readAll() { const query = ` mutation TrackEvent { markAllRead } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Archive all read messages. */ async archiveRead() { const query = ` mutation TrackEvent { archiveRead } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } /** * Archive all read messages. */ async archiveAll() { const query = ` mutation TrackEvent { archiveAll } `; const headers = { "x-courier-user-id": this.options.userId, "Authorization": `Bearer ${this.options.accessToken}` }; if (this.options.connectionId) { headers["x-courier-client-source-id"] = this.options.connectionId; } await graphql({ options: this.options, query, headers, url: this.options.apiUrls.inbox.graphql }); } } class PreferenceTransformer { /** * Transforms a single API response item to the CourierUserPreferencesTopic type * @param item - The API response item * @returns A CourierUserPreferencesTopic object */ transformItem(item) { return { topicId: item.topic_id, topicName: item.topic_name, sectionId: item.section_id, sectionName: item.section_name, status: item.status, defaultStatus: item.default_status, hasCustomRouting: item.has_custom_routing, customRouting: item.custom_routing || [] }; } /** * Transforms an array of API response items to CourierUserPreferencesTopic objects * @param items - The API response items * @returns A generator of CourierUserPreferencesTopic objects */ *transform(items) { for (const item of items) { yield this.transformItem(item); } } } function decode(clientKey) { const binaryString = atob(clientKey); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return String.fromCharCode(...bytes); } function encode(key) { const bytes = new Uint8Array(key.length); for (let i = 0; i < key.length; i++) { bytes[i] = key.charCodeAt(i); } return btoa(String.fromCharCode(...bytes)); } class PreferenceClient extends Client { constructor() { super(...arguments); __publicField(this, "transformer", new PreferenceTransformer()); } /** * Get all preferences for a user * @param paginationCursor - Optional cursor for pagination * @returns Promise resolving to user preferences * @see https://www.courier.com/docs/reference/user-preferences/list-all-user-preferences */ async getUserPreferences(props) { let url = `${this.options.apiUrls.courier.rest}/users/${this.options.userId}/preferences`; if (props == null ? void 0 : props.paginationCursor) { url += `?cursor=${props.paginationCursor}`; } const json = await http({ options: this.options, url, method: "GET", headers: { "Authorization": `Bearer ${this.options.accessToken}` } }); const data = json; return { items: [...this.transformer.transform(data.items)], paging: data.paging }; } /** * Get preferences for a specific topic * @param topicId - The ID of the topic to get preferences for * @returns Promise resolving to topic preferences * @see https://www.courier.com/docs/reference/user-preferences/get-subscription-topic-preferences */ async getUserPreferenceTopic(props) { const json = await http({ options: this.options, url: `${this.options.apiUrls.courier.rest}/users/${this.options.userId}/preferences/${props.topicId}`, method: "GET", headers: { "Authorization": `Bearer ${this.options.accessToken}` } }); const res = json; return this.transformer.transformItem(res.topic); } /** * Update preferences for a specific topic * @param topicId - The ID of the topic to update preferences for * @param status - The new status for the topic * @param hasCustomRouting - Whether the topic has custom routing * @param customRouting - The custom routing channels for the topic * @returns Promise resolving when update is complete * @see https://www.courier.com/docs/reference/user-preferences/update-subscription-topic-preferences */ async putUserPreferenceTopic(props) { const payload = { topic: { status: props.status, has_custom_routing: props.hasCustomRouting, custom_routing: props.customRouting } }; await http({ options: this.options, url: `${this.options.apiUrls.courier.rest}/users/${this.options.userId}/preferences/${props.topicId}`, method: "PUT", headers: { "Authorization": `Bearer ${this.options.accessToken}` }, body: payload }); } /** * Get the notification center URL * @param clientKey - The client key to use for the URL * @returns The notification center URL */ getNotificationCenterUrl(props) { const rootTenantId = decode(props.clientKey); const url = encode(`${rootTenantId}#${this.options.userId}${this.options.tenantId ? `#${this.options.tenantId}` : ""}#${false}`); return `https://view.notificationcenter.app/p/${url}`; } } class TokenClient extends Client { /** * Store a push notification token for a user * @param token - The push notification token * @param provider - The provider of the token * @param device - The device information * @see https://www.courier.com/docs/reference/token-management/put-token */ async putUserToken(props) { const payload = { provider_key: props.provider, ...props.device && { device: { app_id: props.device.appId, ad_id: props.device.adId, device_id: props.device.deviceId, platform: props.device.platform, manufacturer: props.device.manufacturer, model: props.device.model } } }; await http({ options: this.options, url: `${this.options.apiUrls.courier.rest}/users/${this.options.userId}/tokens/${props.token}`, method: "PUT", headers: { "Authorization": `Bearer ${this.options.accessToken}` }, body: payload, validCodes: [200, 204] }); } /** * Delete a push notification token for a user * @param token - The push notification token * @returns Promise resolving when token is deleted */ async deleteUserToken(props) { await http({ options: this.options, url: `${this.options.apiUrls.courier.rest}/users/${this.options.userId}/tokens/${props.token}`, method: "DELETE", headers: { "Authorization": `Bearer ${this.options.accessToken}` }, validCodes: [200, 204] }); } } class ListClient extends Client { /** * Subscribe a user to a list * @param listId - The ID of the list to subscribe to * @returns Promise resolving when subscription is complete * @see https://www.courier.com/docs/reference/lists/recipient-subscribe */ async putSubscription(props) { return await http({ url: `${this.options.apiUrls.courier.rest}/lists/${props.listId}/subscriptions/${this.options.userId}`, options: this.options, method: "PUT", headers: { Authorization: `Bearer ${this.options.accessToken}` } }); } /** * Unsubscribe a user from a list * @param listId - The ID of the list to unsubscribe from * @returns Promise resolving when unsubscription is complete * @see https://www.courier.com/docs/reference/lists/delete-subscription */ async deleteSubscription(props) { return await http({ url: `${this.options.apiUrls.courier.rest}/lists/${props.listId}/subscriptions/${this.options.userId}`, options: this.options, method: "DELETE", headers: { Authorization: `Bearer ${this.options.accessToken}` } }); } } class TrackingClient extends Client { /** * Post an inbound courier event * @param event - The event type: Example: "New Order Placed" * @param messageId - The message ID * @param type - The type of event: Available options: "track" * @param properties - The properties of the event * @returns Promise resolving to the message ID * @see https://www.courier.com/docs/reference/inbound/courier-track-event */ async postInboundCourier(props) { return await http({ url: `${this.options.apiUrls.courier.rest}/inbound/courier`, options: this.options, method: "POST", headers: { Authorization: `Bearer ${this.options.accessToken}` }, body: { ...props, userId: this.options.userId }, validCodes: [200, 202] }); } /** * Post a tracking URL event * These urls are found in messages sent from Courier * @param url - The URL to post the event to * @param event - The event type: Available options: "click", "open", "unsubscribe" * @returns Promise resolving when the event is posted */ async postTrackingUrl(props) { return await http({ url: props.url, options: this.options, method: "POST", body: { event: props.event } }); } } class CourierClient extends Client { constructor(props) { var _a, _b; const showLogs = props.showLogs !== void 0 ? props.showLogs : process.env.NODE_ENV === "development"; const baseOptions = { ...props, showLogs, apiUrls: props.apiUrls || getCourierApiUrls(), accessToken: props.jwt ?? props.publicApiKey }; super({ ...baseOptions, logger: new Logger(baseOptions.showLogs), apiUrls: getCourierApiUrls(baseOptions.apiUrls) }); __publicField(this, "tokens"); __publicField(this, "brands"); __publicField(this, "preferences"); __publicField(this, "inbox"); __publicField(this, "lists"); __publicField(this, "tracking"); this.tokens = new TokenClient(this.options); this.brands = new BrandClient(this.options); this.preferences = new PreferenceClient(this.options); this.inbox = new InboxClient(this.options); this.lists = new ListClient(this.options); this.tracking = new TrackingClient(this.options); if (!this.options.jwt && !this.options.publicApiKey) { this.options.logger.warn("Courier Client initialized with no authentication method. Please provide a JWT or public API key."); } if (this.options.publicApiKey) { (_a = this.options.logger) == null ? void 0 : _a.warn( "Courier Warning: Public API Keys are for testing only. Please use JWTs for production.\nYou can generate a JWT with this endpoint: https://www.courier.com/docs/reference/auth/issue-token\nThis endpoint should be called from your backend server, not the SDK." ); } if (this.options.jwt && this.options.publicApiKey) { (_b = this.options.logger) == null ? void 0 : _b.warn( "Courier Warning: Both a JWT and a Public API Key were provided. The Public API Key will be ignored." ); } } } class AuthenticationListener { constructor(callback) { __publicField(this, "callback"); this.callback = callback; } remove() { Courier.shared.removeAuthenticationListener(this); } } const _Courier = class _Courier { constructor() { /** * The unique identifier for the Courier instance */ __publicField(this, "id", UUID.nanoid()); /** * The Courier client instance */ __publicField(this, "instanceClient"); /** * The pagination limit (min: 1, max: 100) */ __publicField(this, "_paginationLimit", 24); /** * The authentication listeners */ __publicField(this, "authenticationListeners", []); } get paginationLimit() { return this._paginationLimit; } set paginationLimit(value) { this._paginationLimit = Math.min(Math.max(value, 1), 100); } /** * Get the Courier client instance * @returns The Courier client instance or undefined if not signed in */ get client() { return this.instanceClient; } /** * Get the shared Courier instance * @returns The shared Courier instance */ static get shared() { if (!_Courier.instance) { _Courier.instance = new _Courier(); } return _Courier.instance; } /** * Sign in to Courier * @param options - The options for the Courier client */ signIn(props) { if (this.instanceClient) { this.instanceClient.options.logger.warn("Sign in called but there is already a user signed in. Signing out the current user."); this.signOut(); } const connectionId = props.connectionId ?? UUID.nanoid(); this.instanceClient = new CourierClient({ ...props, connectionId }); this.notifyAuthenticationListeners({ userId: props.userId }); } /** * Sign out of Courier */ signOut() { var _a, _b; (_b = (_a = this.instanceClient) == null ? void 0 : _a.inbox.socket) == null ? void 0 : _b.close(); this.instanceClient = void 0; this.notifyAuthenticationListeners({ userId: void 0 }); } /** * Register a callback to be notified of authentication state changes * @param callback - Function to be called when authentication state changes * @returns AuthenticationListener instance that can be used to remove the listener */ addAuthenticationListener(callback) { var _a; (_a = this.instanceClient) == null ? void 0 : _a.options.logger.info("Adding authentication listener"); const listener = new AuthenticationListener(callback); this.authenticationListeners.push(listener); return listener; } /** * Unregister an authentication state change listener * @param listener - The AuthenticationListener instance to remove */ removeAuthenticationListener(listener) { var _a; (_a = this.instanceClient) == null ? void 0 : _a.options.logger.info("Removing authentication listener"); this.authenticationListeners = this.authenticationListeners.filter((l) => l !== listener); } /** * Notify all authentication listeners * @param props - The props to notify the listeners with */ notifyAuthenticationListeners(props) { this.authenticationListeners.forEach((listener) => listener.callback(props)); } }; /** * The shared Courier instance */ __publicField(_Courier, "instance"); let Courier = _Courier; export { BrandClient, Courier, CourierClient, InboxClient, InboxMessageEvent, ListClient, PreferenceClient, TokenClient }; //# sourceMappingURL=index.mjs.map