diff --git a/README.md b/README.md index cae61b3..2420e03 100644 --- a/README.md +++ b/README.md @@ -295,7 +295,9 @@ return cio.track(customerId, { ### Transactional API -To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), import our API client and initialize it with an [app key](https://customer.io/docs/managing-credentials#app-api-keys). +To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), import our API client and initialize it with an [app key](https://customer.io/docs/managing-credentials#app-api-keys) and create a request object of your message type. + +#### Email Create a new `SendEmailRequest` object containing: @@ -337,6 +339,40 @@ api .catch((err) => console.log(err.statusCode, err.message)); ``` +#### Push + +Create a new `SendPushRequest` object containing: + +- `transactional_message_id`: the ID or trigger name of the transactional message you want to send. +- an `identifiers` object containing the `id` or `email` of your recipient. If the profile does not exist, Customer.io will create it. + +Use `sendPush` referencing your request to send a transactional message. [Learn more about transactional messages and `sendPushRequest` properties](https://customer.io/docs/transactional-api). + +```javascript +const { APIClient, SendPushRequest, RegionUS, RegionEU } = require("customerio-node"); +const api = new APIClient("app-key", { region: RegionUS }); + +const request = new SendPushRequest({ + transactional_message_id: "3", + message_data: { + name: "Person", + items: { + name: "shoes", + price: "59.99", + }, + products: [], + }, + identifiers: { + id: "2", + }, +}); + +api + .sendPush(request) + .then((res) => console.log(res)) + .catch((err) => console.log(err.statusCode, err.message)); +``` + ### api.triggerBroadcast(campaign_id, data, recipients) Trigger an email broadcast using the email campaign's id. You can also optionally pass along custom data that will be merged with the liquid template, and additional conditions to filter recipients. diff --git a/lib/api.ts b/lib/api.ts index 25759f7..1c6e8bf 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,7 +1,7 @@ import type { RequestOptions } from 'https'; import Request, { BearerAuth, RequestData } from './request'; import { Region, RegionUS } from './regions'; -import { SendEmailRequest } from './api/requests'; +import { SendEmailRequest, SendPushRequest } from './api/requests'; import { cleanEmail, isEmpty, isIdentifierType, MissingParamError } from './utils'; import { Filter, IdentifierType } from './types'; @@ -77,6 +77,14 @@ export class APIClient { return this.request.post(`${this.apiRoot}/send/email`, req.message); } + sendPush(req: SendPushRequest) { + if (!(req instanceof SendPushRequest)) { + throw new Error('"request" must be an instance of SendPushRequest'); + } + + return this.request.post(`${this.apiRoot}/send/push`, req.message); + } + getCustomersByEmail(email: string) { if (typeof email !== 'string' || isEmpty(email)) { throw new Error('"email" must be a string'); @@ -152,4 +160,4 @@ export class APIClient { } } -export { SendEmailRequest } from './api/requests'; +export { SendEmailRequest, SendPushRequest } from './api/requests'; diff --git a/lib/api/requests.ts b/lib/api/requests.ts index 2ff92f6..9623e49 100644 --- a/lib/api/requests.ts +++ b/lib/api/requests.ts @@ -37,11 +37,12 @@ export type SendEmailRequestWithoutTemplate = SendEmailRequestRequiredOptions & export type SendEmailRequestOptions = SendEmailRequestWithTemplate | SendEmailRequestWithoutTemplate; -export type Message = Partial & { +export type EmailMessage = Partial & { attachments: Record; }; + export class SendEmailRequest { - message: Message; + message: EmailMessage; constructor(opts: SendEmailRequestOptions) { this.message = { @@ -96,3 +97,73 @@ export class SendEmailRequest { } } } + +export type SendPushCustomPayload = { + ios: Record; + android: Record; +}; + +export type SendPushRequestRequiredOptions = { + identifiers: Identifiers; + transactional_message_id: string | number; +}; + +export type SendPushRequestOptionalOptions = Partial<{ + to: string; + title: string; + message: string; + disable_message_retention: boolean; + send_to_unsubscribed: boolean; + queue_draft: boolean; + message_data: Record; + send_at: number; + language: string; + image_url: string; + link: string; + sound: string; + custom_data: Record; + device: Record; + custom_device: Record; +}>; + +export type SendPushRequestWithoutCustomPayload = SendPushRequestRequiredOptions & SendPushRequestOptionalOptions & {}; + +export type SendPushRequestWithCustomPayload = SendPushRequestRequiredOptions & + SendPushRequestOptionalOptions & { + custom_payload: SendPushCustomPayload; + }; + +export type SendPushRequestOptions = SendPushRequestWithoutCustomPayload | SendPushRequestWithCustomPayload; + +export type PushMessage = Partial< + Omit & Omit +>; + +export class SendPushRequest { + message: PushMessage; + + constructor(opts: SendPushRequestOptions) { + this.message = { + identifiers: opts.identifiers, + to: opts.to, + transactional_message_id: opts.transactional_message_id, + title: opts.title, + message: opts.message, + disable_message_retention: opts.disable_message_retention, + send_to_unsubscribed: opts.send_to_unsubscribed, + queue_draft: opts.queue_draft, + message_data: opts.message_data, + send_at: opts.send_at, + language: opts.language, + image_url: opts.image_url, + link: opts.link, + sound: opts.sound, + custom_data: opts.custom_data, + custom_device: opts.device, + }; + + if ('custom_payload' in opts) { + this.message.custom_payload = opts.custom_payload; + } + } +} diff --git a/test/api.ts b/test/api.ts index c79533f..787201d 100644 --- a/test/api.ts +++ b/test/api.ts @@ -1,6 +1,12 @@ import avaTest, { TestFn } from 'ava'; import sinon, { SinonStub } from 'sinon'; -import { APIClient, DeliveryExportMetric, DeliveryExportRequestOptions, SendEmailRequest } from '../lib/api'; +import { + APIClient, + DeliveryExportMetric, + DeliveryExportRequestOptions, + SendEmailRequest, + SendPushRequest, +} from '../lib/api'; import { RegionUS, RegionEU } from '../lib/regions'; import { Filter, IdentifierType } from '../lib/types'; @@ -134,6 +140,49 @@ test('#sendEmail: override body: success', (t) => { t.is(req.message.transactional_message_id, 1); }); +test('sendPush: passing in a plain object throws an error', (t) => { + sinon.stub(t.context.client.request, 'post'); + + let req = { identifiers: { id: '2' }, transactional_message_id: 1 }; + + t.throws(() => t.context.client.sendPush(req as any), { + message: /"request" must be an instance of SendPushRequest/, + }); + t.falsy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/push`)); +}); + +test('#sendPush: with custom payload: success', (t) => { + sinon.stub(t.context.client.request, 'post'); + let req = new SendPushRequest({ + identifiers: { id: '2' }, + transactional_message_id: 1, + custom_payload: { ios: { foo: 'bar' }, android: { foo: 'bar' } }, + }); + t.context.client.sendPush(req); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/push`, req.message)); + t.is(req.message.transactional_message_id, 1); + t.deepEqual(req.message.custom_payload, { ios: { foo: 'bar' }, android: { foo: 'bar' } }); +}); + +test('#sendPush: without custom payload: success', (t) => { + sinon.stub(t.context.client.request, 'post'); + let req = new SendPushRequest({ + identifiers: { id: '2' }, + transactional_message_id: 1, + title: 'This is a test', + message: 'Hi there!', + message_data: { foo: 'bar' }, + }); + + t.context.client.sendPush(req); + t.truthy((t.context.client.request.post as SinonStub).calledWith(`${RegionUS.apiUrl}/send/push`, req.message)); + t.is(req.message.transactional_message_id, 1); + t.is(req.message.title, 'This is a test'); + t.is(req.message.message, 'Hi there!'); + t.deepEqual(req.message.message_data, { foo: 'bar' }); + t.falsy(req.message.custom_payload); +}); + test('#getCustomersByEmail: searching for a customer email (default)', (t) => { sinon.stub(t.context.client.request, 'get');