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

feat: 🚀 Feature — Add Web PushAPI support as Push Provider #3385

Open
wants to merge 20 commits into
base: next
Choose a base branch
from
Open
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ Novu provides a single API to manage providers across multiple channels with a s
- [x] [Expo](https://github.com/novuhq/novu/tree/main/providers/expo)
- [x] [APNS](https://github.com/novuhq/novu/tree/main/providers/apns)
- [x] [OneSignal](https://github.com/novuhq/novu/tree/main/providers/one-signal)
- [x] [PushAPI](https://github.com/novuhq/novu/tree/main/providers/push-api)
- [ ] Pushwoosh

#### 👇 Chat
Expand Down
6 changes: 6 additions & 0 deletions apps/api/src/app/integrations/dtos/credentials.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ export class CredentialsDto implements ICredentials {
@IsOptional()
webhookUrl?: string;

@ApiPropertyOptional()
privateKey?: string;

@ApiPropertyOptional()
publicKey?: string;

@ApiPropertyOptional()
@IsString()
@IsOptional()
Expand Down
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.
58 changes: 58 additions & 0 deletions docs/docs/channels/push/push-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
sidebar_label: PushAPI
sidebar_position: 1
---

# Web APIs - PushAPI

[MDN PushAPI](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)

To enable the PushAPI integration, you need to generate VAPID Private/Public keys to be used to encrypt/decrypt your push payload. See the help documentation on [w3.org](https://www.w3.org/TR/push-api/#push-subscription) to learn how to use a push subscription.

## Generate a VAPID key pair

To generate a key pair using the ECDH signature algorithm, run the following commands:

1. `openssl ecparam -out ecprivate.pem -name prime256v1`
1. `openssl genpkey -paramfile ecprivate.pem -out ecdhkey.pem`

After that, paste the contents of each output to the Private Key and Public Key fields for the Push API integration page.

## Set Device Token

Before triggering the notification to a subscriber(user) with push as a step in the workflow, make sure you have added the subscriber's device token as follows:

```ts
import { Novu, PushProviderIdEnum } from '@novu/node';

const novu = new Novu(process.env.NOVU_API_KEY);

// request access to notifications API and get subscription
const subscriptionJSON = JSON.stringify(subscription);
const subscriptionBase64 = btoa(subscriptionJSON) // browser
// const subscriptionBase64 = Buffer.from(subscriptionJSON, 'ascii').toString('base64') // nodejs

await novu.subscribers.setCredentials('subscriberId', PushProviderIdEnum.PushAPI, {
deviceTokens: [subscriptionBase64], // for multiple devices store each new subscription in your own API
});
```

:::info

Note that `subscriptionBase64` is a base64 encoded JSON object containing the push subscription received from the browser by requesting permission for receiving notifications. It will look something like:

```json
{
"endpoint": "<some_endpoint_string>",
"keys": {
"p256dh": "<encryption_public_key>",
"auth": "<encrypted_auth>"
}
}
```

:::

See [MDN Push API](https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe) for more information and examples regarding this process.

Device/notification identifiers can be set by using [setCredentials](#set-device-token) or by using the `deviceIdentifiers` field in overrides.
2 changes: 2 additions & 0 deletions libs/dal/src/repositories/integration/integration.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const integrationSchema = new Schema<IntegrationDBModel>(
ignoreTls: Schema.Types.Boolean,
tlsOptions: Schema.Types.Mixed,
redirectUrl: Schema.Types.String,
privateKey: Schema.Types.String,
publicKey: Schema.Types.String,
hmac: Schema.Types.Boolean,
ipPoolName: Schema.Types.String,
},
Expand Down
11 changes: 10 additions & 1 deletion libs/shared/src/consts/providers/channels/push.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { apnsConfig, fcmConfig, expoConfig, oneSignalConfig, pushWebhookConfig } from '../credentials';
import { apnsConfig, fcmConfig, expoConfig, oneSignalConfig, pushWebhookConfig, pushApiConfig } from '../credentials';

import { PushProviderIdEnum } from '../provider.enum';
import { IProviderConfig } from '../provider.interface';
Expand Down Expand Up @@ -39,6 +39,15 @@ export const pushProviders: IProviderConfig[] = [
logoFileName: { light: 'apns.png', dark: 'apns.png' },
betaVersion: true,
},
{
id: PushProviderIdEnum.PushAPI,
displayName: 'PushAPI',
channel: ChannelTypeEnum.PUSH,
credentials: pushApiConfig,
docReference: 'https://docs.novu.co/channels/push/push-api',
logoFileName: { light: 'push-api.png', dark: 'push-api.png' },
betaVersion: true,
},
{
id: PushProviderIdEnum.PushWebhook,
displayName: 'Push Webhook',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,24 @@ export const expoConfig: IConfigCredentials[] = [
...pushConfigBase,
];

export const pushApiConfig: IConfigCredentials[] = [
{
key: CredentialsKeyEnum.PrivateKey,
displayName: 'VAPID Private Key',
type: 'string',
description: 'The private key used to encrypt the push payload.',
required: true,
},
{
key: CredentialsKeyEnum.PublicKey,
displayName: 'VAPID Public Key',
type: 'string',
description: 'The public key used to encrypt and decrypt the push payload.',
required: true,
},
...pushConfigBase,
];

export const pushWebhookConfig: IConfigCredentials[] = [
{
key: CredentialsKeyEnum.WebhookUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export const secureCredentials: CredentialsKeyEnum[] = [
CredentialsKeyEnum.Token,
CredentialsKeyEnum.Password,
CredentialsKeyEnum.ServiceAccount,
CredentialsKeyEnum.PrivateKey,
];
3 changes: 3 additions & 0 deletions libs/shared/src/consts/providers/provider.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export enum CredentialsKeyEnum {
IgnoreTls = 'ignoreTls',
TlsOptions = 'tlsOptions',
RedirectUrl = 'redirectUrl',
PublicKey = 'publicKey',
PrivateKey = 'privateKey',
Hmac = 'hmac',
IpPoolName = 'ipPoolName',
}
Expand Down Expand Up @@ -84,6 +86,7 @@ export enum PushProviderIdEnum {
EXPO = 'expo',
OneSignal = 'one-signal',
PushWebhook = 'push-webhook',
PushAPI = 'push-api',
}

export enum InAppProviderIdEnum {
Expand Down
2 changes: 2 additions & 0 deletions libs/shared/src/entities/integration/credential.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface ICredentials {
ignoreTls?: boolean;
tlsOptions?: Record<string, unknown>;
redirectUrl?: string;
privateKey?: string;
publicKey?: string;
hmac?: boolean;
ipPoolName?: string;
}
1 change: 1 addition & 0 deletions packages/application-generic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"@novu/outlook365": "^0.16.3",
"@novu/plivo": "^0.16.3",
"@novu/postmark": "^0.16.3",
"@novu/push-api": "^0.16.3",
"@novu/push-webhook": "^0.16.3",
"@novu/resend": "^0.16.3",
"@novu/sendgrid": "^0.16.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ChannelTypeEnum } from '@novu/shared';
import { PushApiPushProvider } from '@novu/push-api';
import { BasePushHandler } from './base.handler';
import { ICredentials } from '@novu/dal';

export class PushAPIHandler extends BasePushHandler {
constructor() {
super('push-api', ChannelTypeEnum.PUSH);
}

buildProvider(credentials: ICredentials) {
if (!credentials.privateKey || !credentials.publicKey) {
throw new Error('Config is not valid for Push API');
}
this.provider = new PushApiPushProvider({
vapidPrivateKey: credentials.privateKey,
vapidPublicKey: credentials.publicKey,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {
OneSignalHandler,
PushWebhookHandler,
} from './handlers';
import { PushAPIHandler } from './handlers/push-api.handler';

export class PushFactory implements IPushFactory {
handlers: IPushHandler[] = [
new FCMHandler(),
new ExpoHandler(),
new APNSHandler(),
new OneSignalHandler(),
new PushAPIHandler(),
new PushWebhookHandler(),
];

Expand Down
78 changes: 78 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions providers/push-api/.czrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"path": "cz-conventional-changelog"
}
3 changes: 3 additions & 0 deletions providers/push-api/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../.eslintrc.js"
}
9 changes: 9 additions & 0 deletions providers/push-api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.idea/*
.nyc_output
build
node_modules
test
src/**.js
coverage
*.log
package-lock.json
9 changes: 9 additions & 0 deletions providers/push-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Novu PushApi Provider

A PushApi push provider library for [@novu/node](https://github.com/novuhq/novu)

## Usage

```javascript
FILL IN THE INITIALIZATION USAGE
```
5 changes: 5 additions & 0 deletions providers/push-api/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
Loading