Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support for sending transactional push messages #141

Merged
merged 2 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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