Skip to content
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
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"amannn",
"analagous",
"Angularjs",
"sinch",
"evalsha",
"antd",
"anthonybaxter",
Expand Down
8 changes: 8 additions & 0 deletions apps/dashboard/public/images/providers/light/square/sinch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/static/images/providers/dark/sinch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions apps/web/public/static/images/providers/dark/square/sinch.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/web/public/static/images/providers/light/sinch.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const providers: Record<ChannelTypeEnum, ProvidersIdEnum[]> = {
SmsProviderIdEnum.BrevoSms,
SmsProviderIdEnum.ISendSms,
SmsProviderIdEnum.EazySms,
SmsProviderIdEnum.Sinch,
].sort(),
],
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export * from './plivo.handler';
export * from './ring-central.handler';
export * from './sendchamp.handler';
export * from './simpletexting.handler';
export * from './sinch.handler';
export * from './sms-central.handler';
export * from './sms77.handler';
export * from './sns.handler';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SinchSmsProvider } from '@novu/providers';
import { ChannelTypeEnum, ICredentials, SmsProviderIdEnum } from '@novu/shared';
import { BaseSmsHandler } from './base.handler';

export class SinchHandler extends BaseSmsHandler {
constructor() {
super(SmsProviderIdEnum.Sinch, ChannelTypeEnum.SMS);
}

buildProvider(credentials: ICredentials) {
const config = credentials as Record<string, string>;
this.provider = new SinchSmsProvider({
servicePlanId: config.servicePlanId,
apiToken: config.apiToken,
from: config.from,
region: config.region,
});
}
}
2 changes: 2 additions & 0 deletions libs/application-generic/src/factories/sms/sms.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
RingCentralHandler,
SendchampSmsHandler,
SimpletextingSmsHandler,
SinchHandler,
Sms77Handler,
SmsCentralHandler,
SnsHandler,
Expand Down Expand Up @@ -60,6 +61,7 @@ export class SmsFactory implements ISmsFactory {
new SendchampSmsHandler(),
new ClicksendSmsHandler(),
new SimpletextingSmsHandler(),
new SinchHandler(),
new BandwidthHandler(),
new GenericSmsHandler(),
new MessageBirdHandler(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const integrationSchema = new Schema<IntegrationDBModel>(
accessKey: Schema.Types.String,
appSid: Schema.Types.String,
senderId: Schema.Types.String,
servicePlanId: Schema.Types.String,
},
configurations: {
inboundWebhookEnabled: Schema.Types.Boolean,
Expand Down
1 change: 1 addition & 0 deletions packages/framework/src/schemas/providers/sms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,5 @@ export const smsProviderSchemas = {
'afro-message': genericProviderSchemas,
unifonic: genericProviderSchemas,
imedia: genericProviderSchemas,
sinch: genericProviderSchemas,
} as const satisfies Record<SmsProviderIdEnum, { output: JsonSchema }>;
1 change: 1 addition & 0 deletions packages/framework/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export enum SmsProviderIdEnum {
Unifonic = 'unifonic',
Smsmode = 'smsmode',
IMedia = 'imedia',
Sinch = 'sinch',
}

export enum ChatProviderIdEnum {
Expand Down
1 change: 1 addition & 0 deletions packages/providers/src/lib/sms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export * from './plivo/plivo.provider';
export * from './ring-central/ring-central.provider';
export * from './sendchamp/sendchamp.provider';
export * from './simpletexting/simpletexting.provider';
export * from './sinch/sinch.provider';
export * from './sms-central/sms-central.provider';
export * from './sms77/sms77.provider';
export * from './smsmode/smsmode.provider';
Expand Down
111 changes: 111 additions & 0 deletions packages/providers/src/lib/sms/sinch/sinch.provider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import axios from 'axios';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SinchSmsProvider } from './sinch.provider';

vi.mock('axios');

describe('SinchSmsProvider', () => {
const mockConfig = {
servicePlanId: 'test-service-plan-id',
apiToken: 'test-api-token',
from: '+1234567890',
region: 'eu',
};

let provider: SinchSmsProvider;

beforeEach(() => {
provider = new SinchSmsProvider(mockConfig);
vi.clearAllMocks();
});

describe('sendMessage', () => {
it('should send an SMS message successfully', async () => {
const mockResponse = {
data: {
id: 'batch-123',
created_at: '2023-01-01T00:00:00Z',
},
};

vi.mocked(axios.post).mockResolvedValue(mockResponse);

const result = await provider.sendMessage({
to: '+9876543210',
content: 'Test message',
});

expect(axios.post).toHaveBeenCalledWith(
'https://eu.sms.api.sinch.com/xms/v1/test-service-plan-id/batches',
{
from: '+1234567890',
to: ['+9876543210'],
body: 'Test message',
},
{
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer test-api-token',
},
}
);

expect(result).toEqual({
id: 'batch-123',
date: '2023-01-01T00:00:00Z',
});
});

it('should use custom from number if provided', async () => {
const mockResponse = {
data: {
id: 'batch-456',
created_at: '2023-01-02T00:00:00Z',
},
};

vi.mocked(axios.post).mockResolvedValue(mockResponse);

await provider.sendMessage({
to: '+9876543210',
content: 'Test message',
from: '+1111111111',
});

expect(axios.post).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
from: '+1111111111',
}),
expect.any(Object)
);
});

it('should use different region if configured', async () => {
const caProvider = new SinchSmsProvider({
...mockConfig,
region: 'ca',
});

const mockResponse = {
data: {
id: 'batch-789',
created_at: '2023-01-03T00:00:00Z',
},
};

vi.mocked(axios.post).mockResolvedValue(mockResponse);

await caProvider.sendMessage({
to: '+9876543210',
content: 'Test message',
});

expect(axios.post).toHaveBeenCalledWith(
'https://ca.sms.api.sinch.com/xms/v1/test-service-plan-id/batches',
expect.any(Object),
expect.any(Object)
);
});
});
});
49 changes: 49 additions & 0 deletions packages/providers/src/lib/sms/sinch/sinch.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { SmsProviderIdEnum } from '@novu/shared';
import { ChannelTypeEnum, ISendMessageSuccessResponse, ISmsOptions, ISmsProvider } from '@novu/stateless';

import axios from 'axios';
import { BaseProvider, CasingEnum } from '../../../base.provider';
import { WithPassthrough } from '../../../utils/types';

export class SinchSmsProvider extends BaseProvider implements ISmsProvider {
id = SmsProviderIdEnum.Sinch;
protected casing = CasingEnum.CAMEL_CASE;
channelType = ChannelTypeEnum.SMS as ChannelTypeEnum.SMS;

constructor(
private config: {
servicePlanId?: string;
apiToken?: string;
from?: string;
region?: string;
}
) {
super();
}

async sendMessage(
options: ISmsOptions,
bridgeProviderData: WithPassthrough<Record<string, unknown>> = {}
): Promise<ISendMessageSuccessResponse> {
const region = this.config.region || 'eu';
const url = `https://${region}.sms.api.sinch.com/xms/v1/${this.config.servicePlanId}/batches`;

const payload = this.transform<Record<string, unknown>>(bridgeProviderData, {
from: options.from || this.config.from,
to: [options.to],
body: options.content,
}).body;

const response = await axios.post(url, payload, {
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.config.apiToken}`,
},
});

return {
id: response.data.id,
date: response.data.created_at || new Date().toISOString(),
};
}
}
11 changes: 10 additions & 1 deletion packages/shared/src/consts/providers/channels/sms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
ringCentralConfig,
sendchampConfig,
simpleTextingConfig,
sinchConfig,
sms77Config,
smsCentralConfig,
smsmodeProviderConfig,
Expand Down Expand Up @@ -291,6 +292,14 @@ export const smsProviders: IProviderConfig[] = [
docReference: 'https://telkosh.com/mobishastra/',
logoFileName: { light: 'mobishastra.png', dark: 'mobishastra.png' },
},
{
id: SmsProviderIdEnum.Sinch,
displayName: 'Sinch',
channel: ChannelTypeEnum.SMS,
credentials: sinchConfig,
docReference: `https://docs.novu.co/integrations/providers/sms/sinch${UTM_CAMPAIGN_QUERY_PARAM}`,
logoFileName: { light: 'sinch.png', dark: 'sinch.png' },
},
{
id: SmsProviderIdEnum.AfroSms,
displayName: 'Afro Message',
Expand All @@ -307,7 +316,7 @@ export const smsProviders: IProviderConfig[] = [
docReference: 'https://docs.unifonic.com/articles/#!products-documentation/getting-started-with-unifonic',
logoFileName: { light: 'unifonic.svg', dark: 'unifonic.svg' },
},
{
{
id: SmsProviderIdEnum.Smsmode,
displayName: 'smsmode',
channel: ChannelTypeEnum.SMS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1274,3 +1274,35 @@ export const smsmodeProviderConfig: IConfigCredential[] = [
},
...smsConfigBase,
];

export const sinchConfig: IConfigCredential[] = [
{
key: CredentialsKeyEnum.ServicePlanId,
displayName: 'Service Plan ID',
description: 'Your Sinch Service Plan ID',
type: 'string',
required: true,
},
{
key: CredentialsKeyEnum.ApiToken,
displayName: 'API Token',
type: 'string',
required: true,
},
{
key: CredentialsKeyEnum.Region,
displayName: 'Region',
description: 'Select your Sinch region',
type: 'dropdown',
required: true,
value: 'eu',
dropdown: [
{ name: 'EU (Ireland, Sweden)', value: 'eu' },
{ name: 'US', value: 'us' },
{ name: 'Australia', value: 'au' },
{ name: 'Brazil', value: 'br' },
{ name: 'Canada', value: 'ca' },
],
},
...smsConfigBase,
];
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,5 @@ export interface ICredentials {
apiVersion?: string;
appSid?: string;
senderId?: string;
servicePlanId?: string;
}
2 changes: 2 additions & 0 deletions packages/shared/src/types/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export enum CredentialsKeyEnum {
ApiVersion = 'apiVersion',
AppSid = 'appSid',
SenderId = 'senderId',
ServicePlanId = 'servicePlanId',
}

export type ConfigurationKey = keyof IConfigurations;
Expand Down Expand Up @@ -115,6 +116,7 @@ export enum SmsProviderIdEnum {
// cspell:disable-next-line
Smsmode = 'smsmode',
IMedia = 'imedia',
Sinch = 'sinch',
}

export enum ChatProviderIdEnum {
Expand Down
Loading