Skip to content

Commit

Permalink
support for sending transactional push messages (#141)
Browse files Browse the repository at this point in the history
  • Loading branch information
karngyan authored May 8, 2023
1 parent 43c088d commit 1e007a8
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 6 deletions.
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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.
Expand Down
12 changes: 10 additions & 2 deletions lib/api.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -152,4 +160,4 @@ export class APIClient {
}
}

export { SendEmailRequest } from './api/requests';
export { SendEmailRequest, SendPushRequest } from './api/requests';
75 changes: 73 additions & 2 deletions lib/api/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,12 @@ export type SendEmailRequestWithoutTemplate = SendEmailRequestRequiredOptions &

export type SendEmailRequestOptions = SendEmailRequestWithTemplate | SendEmailRequestWithoutTemplate;

export type Message = Partial<SendEmailRequestWithTemplate & SendEmailRequestWithoutTemplate> & {
export type EmailMessage = Partial<SendEmailRequestWithTemplate & SendEmailRequestWithoutTemplate> & {
attachments: Record<string, string>;
};

export class SendEmailRequest {
message: Message;
message: EmailMessage;

constructor(opts: SendEmailRequestOptions) {
this.message = {
Expand Down Expand Up @@ -96,3 +97,73 @@ export class SendEmailRequest {
}
}
}

export type SendPushCustomPayload = {
ios: Record<string, any>;
android: Record<string, any>;
};

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<string, any>;
send_at: number;
language: string;
image_url: string;
link: string;
sound: string;
custom_data: Record<string, string>;
device: Record<string, any>;
custom_device: Record<string, any>;
}>;

export type SendPushRequestWithoutCustomPayload = SendPushRequestRequiredOptions & SendPushRequestOptionalOptions & {};

export type SendPushRequestWithCustomPayload = SendPushRequestRequiredOptions &
SendPushRequestOptionalOptions & {
custom_payload: SendPushCustomPayload;
};

export type SendPushRequestOptions = SendPushRequestWithoutCustomPayload | SendPushRequestWithCustomPayload;

export type PushMessage = Partial<
Omit<SendPushRequestWithoutCustomPayload, 'device'> & Omit<SendPushRequestWithCustomPayload, 'device'>
>;

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;
}
}
}
51 changes: 50 additions & 1 deletion test/api.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');

Expand Down

0 comments on commit 1e007a8

Please sign in to comment.