@kfiross44/payload-push 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -0
- package/dist/adapters/push-firebase.d.ts +33 -0
- package/dist/adapters/push-firebase.js +185 -0
- package/dist/adapters/push-firebase.js.map +1 -0
- package/dist/endpoints/fcmPushEndpointHandler.d.ts +4 -0
- package/dist/endpoints/fcmPushEndpointHandler.js +60 -0
- package/dist/endpoints/fcmPushEndpointHandler.js.map +1 -0
- package/dist/index.d.js +3 -0
- package/dist/index.d.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +48 -0
- package/dist/index.js.map +1 -0
- package/dist/payloadPush.d.js +3 -0
- package/dist/payloadPush.d.js.map +1 -0
- package/dist/payloadPush.d.ts +15 -0
- package/dist/payloadPush.js +32 -0
- package/dist/payloadPush.js.map +1 -0
- package/dist/types/global.d.js +3 -0
- package/dist/types/global.d.js.map +1 -0
- package/dist/types/index.d.ts +26 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +97 -0
package/README.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# PayloadCMS Push Plugin
|
|
2
|
+
|
|
3
|
+
The **PayloadCMS Push Plugin** provides a unified interface for sending
|
|
4
|
+
push notifications from within Payload applications.
|
|
5
|
+
|
|
6
|
+
It is designed as an extensible, adapter-based system that supports
|
|
7
|
+
multiple push providers. Currently, the plugin includes a **Firebase
|
|
8
|
+
adapter** powered by the Firebase Admin SDK and Firebase Cloud Messaging
|
|
9
|
+
(FCM), with additional providers planned for future releases (such as
|
|
10
|
+
OneSignal).
|
|
11
|
+
|
|
12
|
+
------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
## Available Adapters
|
|
15
|
+
|
|
16
|
+
### Firebase (FCM)
|
|
17
|
+
|
|
18
|
+
The Firebase Push Adapter integrates **PayloadCMS** with **Firebase
|
|
19
|
+
Cloud Messaging (FCM)** using the official Firebase Admin SDK. It
|
|
20
|
+
enables transactional and broadcast push notification workflows across
|
|
21
|
+
web, iOS, and Android devices.
|
|
22
|
+
|
|
23
|
+
Built on top of Firebase and Firebase Cloud Messaging (FCM), this
|
|
24
|
+
adapter allows teams to send:
|
|
25
|
+
|
|
26
|
+
- Single-device notifications
|
|
27
|
+
- Multicast push notifications
|
|
28
|
+
- Topic-based broadcast notifications
|
|
29
|
+
- Platform-specific (Android / APNs) configurations
|
|
30
|
+
|
|
31
|
+
By leveraging the Firebase Admin SDK, organisations retain full control
|
|
32
|
+
over credentials, infrastructure, and deployment topology while
|
|
33
|
+
simplifying push delivery from within Payload.
|
|
34
|
+
|
|
35
|
+
------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
## Roadmap
|
|
38
|
+
|
|
39
|
+
The plugin is designed to support multiple providers through adapters.
|
|
40
|
+
Planned integrations may include:
|
|
41
|
+
|
|
42
|
+
- OneSignal
|
|
43
|
+
- Additional self-hosted or API-first push services
|
|
44
|
+
- Requested by community
|
|
45
|
+
|
|
46
|
+
------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
``` sh
|
|
51
|
+
pnpm add firebase-admin
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
## Prerequisites
|
|
57
|
+
|
|
58
|
+
1. Create a Firebase project\
|
|
59
|
+
2. Enable Firebase Cloud Messaging (FCM)\
|
|
60
|
+
3. Generate a **Service Account Key (JSON)**\
|
|
61
|
+
4. Store the service account JSON securely (environment variable or
|
|
62
|
+
secret manager)
|
|
63
|
+
|
|
64
|
+
------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### Firebase Adapter
|
|
69
|
+
``` ts
|
|
70
|
+
// payload.config.ts
|
|
71
|
+
import { buildConfig } from 'payload'
|
|
72
|
+
import { firebaseAdapter } from '@kfiross/payload-push'
|
|
73
|
+
|
|
74
|
+
export default buildConfig({
|
|
75
|
+
push: firebaseAdapter({
|
|
76
|
+
serviceAccountJSON: JSON.parse(
|
|
77
|
+
process.env.FIREBASE_SERVICE_ACCOUNT_JSON!
|
|
78
|
+
),
|
|
79
|
+
}),
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Firebase Adapter
|
|
88
|
+
* Sending a Push Notification
|
|
89
|
+
``` ts
|
|
90
|
+
await payload.push.send({
|
|
91
|
+
title: 'New Booking Confirmed',
|
|
92
|
+
body: 'Your booking has been successfully confirmed.',
|
|
93
|
+
data: {
|
|
94
|
+
bookingId: '12345',
|
|
95
|
+
},
|
|
96
|
+
options: {
|
|
97
|
+
token: '<device-fcm-token>',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
* Sending to Multiple Devices
|
|
103
|
+
|
|
104
|
+
``` ts
|
|
105
|
+
await payload.push.send({
|
|
106
|
+
title: 'System Update',
|
|
107
|
+
body: 'We have updated our terms of service.',
|
|
108
|
+
options: {
|
|
109
|
+
tokens: ['token-1', 'token-2'],
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
* Sending to a Topic
|
|
115
|
+
|
|
116
|
+
``` ts
|
|
117
|
+
await payload.push.send({
|
|
118
|
+
title: 'Weekly Newsletter',
|
|
119
|
+
body: 'Check out what’s new this week!',
|
|
120
|
+
options: {
|
|
121
|
+
topic: 'weekly-updates',
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
## Configuration
|
|
129
|
+
|
|
130
|
+
### Firebase Adapter
|
|
131
|
+
|
|
132
|
+
| Option | Type | Required | Default | Description |
|
|
133
|
+
|--------------------|--------|----------|---------|-------------------------------------------|
|
|
134
|
+
| serviceAccountJSON | string | Yes | - | Firebase service account credentials JSON |
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Inside `options`, you may provide:
|
|
139
|
+
|
|
140
|
+
| Option | Type | Description |
|
|
141
|
+
|----------------------|----------|------------------------|
|
|
142
|
+
| token | string | Single device token |
|
|
143
|
+
| tokens | string[] | Multiple device tokens |
|
|
144
|
+
| topic | string | Subscribed topic name |
|
|
145
|
+
| android | object | Android-specific FCM config |
|
|
146
|
+
| apns | object | iOS/APNs-specific config |
|
|
147
|
+
|
|
148
|
+
------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
## Example Environment Variable Setup
|
|
151
|
+
|
|
152
|
+
You may define (for FCM for example):
|
|
153
|
+
``` bash
|
|
154
|
+
FIREBASE_SERVICE_ACCOUNT_JSON='{"type":"service_account", ...}'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
------------------------------------------------------------------------
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BatchResponse } from 'firebase-admin/messaging';
|
|
2
|
+
import type { PushAdapter } from '../types/index.js';
|
|
3
|
+
type ServiceAccountJSONData = {
|
|
4
|
+
auth_provider_x509_cert_url: string;
|
|
5
|
+
auth_uri: string;
|
|
6
|
+
client_email: string;
|
|
7
|
+
client_id: string;
|
|
8
|
+
client_x509_cert_url: string;
|
|
9
|
+
private_key: string;
|
|
10
|
+
private_key_id: string;
|
|
11
|
+
project_id: string;
|
|
12
|
+
token_uri: string;
|
|
13
|
+
type: string;
|
|
14
|
+
universe_domain: string;
|
|
15
|
+
};
|
|
16
|
+
export type FirebaseAdapterArgs = {
|
|
17
|
+
serviceAccountJSON: ServiceAccountJSONData;
|
|
18
|
+
};
|
|
19
|
+
type FirebasePushAdapter = PushAdapter<firebaseResponse>;
|
|
20
|
+
type firebaseError = {
|
|
21
|
+
error: {
|
|
22
|
+
code: string;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
type firebaseResponse = {
|
|
27
|
+
messageId: string;
|
|
28
|
+
} | BatchResponse | firebaseError;
|
|
29
|
+
/**
|
|
30
|
+
* Push adapter for [firebase](https://firebase.com) Admin API
|
|
31
|
+
*/
|
|
32
|
+
export declare const firebaseAdapter: ({ serviceAccountJSON }: FirebaseAdapterArgs) => FirebasePushAdapter;
|
|
33
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// import { APIError } from "payload"
|
|
2
|
+
import admin from 'firebase-admin';
|
|
3
|
+
// export const firebaseAdapter = (args: firebaseAdapterArgs): FirebasePushAdapter => {
|
|
4
|
+
// const {
|
|
5
|
+
// apiKey,
|
|
6
|
+
// scheduledAt,
|
|
7
|
+
// firebaseUrl,
|
|
8
|
+
// variables,
|
|
9
|
+
// } = args
|
|
10
|
+
// const adapter: FirebasePushAdapter = () => ({
|
|
11
|
+
// name: "firebase",
|
|
12
|
+
// // defaultFromName,
|
|
13
|
+
// // defaultFromAddress,
|
|
14
|
+
// sendPush: async (message) => {
|
|
15
|
+
// const sendPushOptions = mapPayloadToFirebasePush(
|
|
16
|
+
// // defaultFromName,
|
|
17
|
+
// // defaultFromAddress,
|
|
18
|
+
// message
|
|
19
|
+
// )
|
|
20
|
+
// const payload = {
|
|
21
|
+
// ...sendPushOptions,
|
|
22
|
+
// ...(scheduledAt ? { scheduledAt } : {}),
|
|
23
|
+
// // ...(templateId ? { templateId } : {}),
|
|
24
|
+
// ...(variables ? { variables } : {}),
|
|
25
|
+
// }
|
|
26
|
+
// const res = await fetch(`${firebaseUrl}/api/v1/emails`, {
|
|
27
|
+
// body: JSON.stringify(payload),
|
|
28
|
+
// headers: {
|
|
29
|
+
// Authorization: `Bearer ${apiKey}`,
|
|
30
|
+
// "Content-Type": "application/json",
|
|
31
|
+
// },
|
|
32
|
+
// method: "POST",
|
|
33
|
+
// })
|
|
34
|
+
// const data = (await res.json()) as firebaseResponse
|
|
35
|
+
// if ("emailId" in data) {
|
|
36
|
+
// return data
|
|
37
|
+
// }
|
|
38
|
+
// else {
|
|
39
|
+
// const statusCode = res.status
|
|
40
|
+
// let formattedError = `Error sending email: ${statusCode}`
|
|
41
|
+
// if ("error" in data) {
|
|
42
|
+
// formattedError += ` ${data.error.code} - ${data.error.message}`
|
|
43
|
+
// }
|
|
44
|
+
// throw new APIError(formattedError, statusCode)
|
|
45
|
+
// }
|
|
46
|
+
// },
|
|
47
|
+
// })
|
|
48
|
+
/**
|
|
49
|
+
* Push adapter for [firebase](https://firebase.com) Admin API
|
|
50
|
+
*/ export const firebaseAdapter = ({ serviceAccountJSON })=>{
|
|
51
|
+
// Initialize firebase-admin if not already done
|
|
52
|
+
if (!admin.apps.length) {
|
|
53
|
+
if (!serviceAccountJSON) {
|
|
54
|
+
throw new Error('Missing service account json data');
|
|
55
|
+
}
|
|
56
|
+
const creds = serviceAccountJSON || undefined;
|
|
57
|
+
try {
|
|
58
|
+
admin.initializeApp({
|
|
59
|
+
credential: creds ? admin.credential.cert(creds) : admin.credential.applicationDefault()
|
|
60
|
+
});
|
|
61
|
+
console.log('✅ Firebase admin app initialized', admin.app().name);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(e);
|
|
64
|
+
console.log('❌ Firebase admin app initialized failed', e);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const fcm = admin.messaging();
|
|
68
|
+
const adapter = ()=>({
|
|
69
|
+
name: 'firebase-admin',
|
|
70
|
+
/**
|
|
71
|
+
* Sends push notifications using Firebase Admin SDK
|
|
72
|
+
*/ async sendPush (message) {
|
|
73
|
+
const sendPushOptions = mapPayloadToFirebasePush(message);
|
|
74
|
+
const notification = {
|
|
75
|
+
body: message.body,
|
|
76
|
+
title: message.title
|
|
77
|
+
};
|
|
78
|
+
console.log({
|
|
79
|
+
sendPushOptions
|
|
80
|
+
});
|
|
81
|
+
try {
|
|
82
|
+
if (sendPushOptions.topic) {
|
|
83
|
+
const payload = {
|
|
84
|
+
data: message.data,
|
|
85
|
+
notification,
|
|
86
|
+
topic: sendPushOptions.topic
|
|
87
|
+
};
|
|
88
|
+
const res = await fcm.send(payload);
|
|
89
|
+
console.log('✅ Push to single token:', res);
|
|
90
|
+
if (res) {
|
|
91
|
+
return {
|
|
92
|
+
messageId: res
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (sendPushOptions.tokens && sendPushOptions.tokens.length) {
|
|
97
|
+
const res = await fcm.sendEachForMulticast({
|
|
98
|
+
data: message.data,
|
|
99
|
+
notification,
|
|
100
|
+
tokens: sendPushOptions.tokens
|
|
101
|
+
});
|
|
102
|
+
console.log('✅ Push multicast result:', res);
|
|
103
|
+
return res;
|
|
104
|
+
}
|
|
105
|
+
if (sendPushOptions.token) {
|
|
106
|
+
const res = await fcm.send({
|
|
107
|
+
data: message.data,
|
|
108
|
+
notification,
|
|
109
|
+
token: sendPushOptions.token
|
|
110
|
+
});
|
|
111
|
+
console.log('✅ Push to token:', res);
|
|
112
|
+
return {
|
|
113
|
+
messageId: res
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw new Error('No token(s) or topic provided');
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error('❌ Firebase push error:', err);
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return adapter;
|
|
124
|
+
};
|
|
125
|
+
function mapPayloadToFirebasePush(message) {
|
|
126
|
+
const pushOptions = {
|
|
127
|
+
body: message.body,
|
|
128
|
+
title: message.title
|
|
129
|
+
};
|
|
130
|
+
if (!message.options) {
|
|
131
|
+
return pushOptions;
|
|
132
|
+
}
|
|
133
|
+
if (message.options['token']) {
|
|
134
|
+
const token = message.options['token'];
|
|
135
|
+
pushOptions.token = token;
|
|
136
|
+
}
|
|
137
|
+
if (message.options['tokens']) {
|
|
138
|
+
const tokens = message.options['tokens'];
|
|
139
|
+
pushOptions.tokens = tokens;
|
|
140
|
+
}
|
|
141
|
+
if (message.options['topic']) {
|
|
142
|
+
const topic = message.options['topic'];
|
|
143
|
+
pushOptions.topic = topic;
|
|
144
|
+
}
|
|
145
|
+
if (message.options['android']) {
|
|
146
|
+
const android = message.options['android'];
|
|
147
|
+
pushOptions.android = android;
|
|
148
|
+
}
|
|
149
|
+
if (message.options['apns']) {
|
|
150
|
+
const apns = message.options['apns'];
|
|
151
|
+
pushOptions.apns = apns;
|
|
152
|
+
}
|
|
153
|
+
// if (message.text?.toString().trim().length > 0) {
|
|
154
|
+
// pushOptions.text = message.text
|
|
155
|
+
// } else {
|
|
156
|
+
// pushOptions.text = "Please view this email in an HTML-compatible client."
|
|
157
|
+
// }
|
|
158
|
+
// if (message.html?.toString().trim()) {
|
|
159
|
+
// pushOptions.html = message.html.toString()
|
|
160
|
+
// }
|
|
161
|
+
// if (message.attachments?.length) {
|
|
162
|
+
// if (message.attachments.length > 10) {
|
|
163
|
+
// throw new APIError("Maximum of 10 attachments allowed", 400)
|
|
164
|
+
// }
|
|
165
|
+
// pushOptions.attachments = mapAttachments(message.attachments)
|
|
166
|
+
// }
|
|
167
|
+
// if (message.replyTo) {
|
|
168
|
+
// pushOptions.replyTo = mapAddresses(message.replyTo)
|
|
169
|
+
// }
|
|
170
|
+
// if (message.cc) {
|
|
171
|
+
// pushOptions.cc = mapAddresses(message.cc)
|
|
172
|
+
// }
|
|
173
|
+
// if (message.bcc) {
|
|
174
|
+
// pushOptions.bcc = mapAddresses(message.bcc)
|
|
175
|
+
// }
|
|
176
|
+
return pushOptions;
|
|
177
|
+
}
|
|
178
|
+
// type Attachment = {
|
|
179
|
+
// /** Content of an attached file. */
|
|
180
|
+
// content: string
|
|
181
|
+
// /** Name of attached file. */
|
|
182
|
+
// filename: string
|
|
183
|
+
// }
|
|
184
|
+
|
|
185
|
+
//# sourceMappingURL=push-firebase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/push-firebase.ts"],"sourcesContent":["import type { BatchResponse, TopicMessage } from 'firebase-admin/messaging'\n\n// import { APIError } from \"payload\"\n\nimport admin from 'firebase-admin'\n\nimport type { PushAdapter, SendPushOptions } from '../types/index.js'\n\ntype ServiceAccountJSONData = {\n auth_provider_x509_cert_url: string\n auth_uri: string\n client_email: string\n client_id: string\n client_x509_cert_url: string\n private_key: string\n private_key_id: string\n project_id: string\n token_uri: string\n type: string\n universe_domain: string\n}\n\nexport type FirebaseAdapterArgs = {\n serviceAccountJSON: ServiceAccountJSONData\n}\n\ntype FirebasePushAdapter = PushAdapter<firebaseResponse>\n\ntype firebaseError = {\n error: {\n code: string\n message: string\n }\n}\n\ntype firebaseResponse = { messageId: string } | BatchResponse | firebaseError\n\n// export const firebaseAdapter = (args: firebaseAdapterArgs): FirebasePushAdapter => {\n// \tconst {\n// \t\tapiKey,\n// \t\tscheduledAt,\n// \t\tfirebaseUrl,\n// \t\tvariables,\n// \t} = args\n\n// \tconst adapter: FirebasePushAdapter = () => ({\n// \t\tname: \"firebase\",\n// \t\t// defaultFromName,\n// \t\t// defaultFromAddress,\n// \t\tsendPush: async (message) => {\n// \t\t\tconst sendPushOptions = mapPayloadToFirebasePush(\n// \t\t\t\t// defaultFromName,\n// \t\t\t\t// defaultFromAddress,\n// \t\t\t\tmessage\n// \t\t\t)\n\n// \t\t\tconst payload = {\n// \t\t\t\t...sendPushOptions,\n// \t\t\t\t...(scheduledAt ? { scheduledAt } : {}),\n// \t\t\t\t// ...(templateId ? { templateId } : {}),\n// \t\t\t\t...(variables ? { variables } : {}),\n// \t\t\t}\n\n// \t\t\tconst res = await fetch(`${firebaseUrl}/api/v1/emails`, {\n// \t\t\t\tbody: JSON.stringify(payload),\n// \t\t\t\theaders: {\n// \t\t\t\t\tAuthorization: `Bearer ${apiKey}`,\n// \t\t\t\t\t\"Content-Type\": \"application/json\",\n// \t\t\t\t},\n// \t\t\t\tmethod: \"POST\",\n// \t\t\t})\n\n// \t\t\tconst data = (await res.json()) as firebaseResponse\n\n// \t\t\tif (\"emailId\" in data) {\n// \t\t\t\treturn data\n// \t\t\t}\n// else {\n// \t\t\t\tconst statusCode = res.status\n// \t\t\t\tlet formattedError = `Error sending email: ${statusCode}`\n// \t\t\t\tif (\"error\" in data) {\n// \t\t\t\t\tformattedError += ` ${data.error.code} - ${data.error.message}`\n// \t\t\t\t}\n\n// \t\t\t\tthrow new APIError(formattedError, statusCode)\n// \t\t\t}\n// \t\t},\n// \t})\n\n/**\n * Push adapter for [firebase](https://firebase.com) Admin API\n */\nexport const firebaseAdapter = ({ serviceAccountJSON }: FirebaseAdapterArgs) => {\n // Initialize firebase-admin if not already done\n if (!admin.apps.length) {\n if (!serviceAccountJSON) {\n throw new Error('Missing service account json data')\n }\n\n const creds = serviceAccountJSON || undefined\n\n try {\n admin.initializeApp({\n credential: creds\n ? admin.credential.cert(creds as admin.ServiceAccount)\n : admin.credential.applicationDefault(),\n })\n console.log('✅ Firebase admin app initialized', admin.app().name)\n } catch (e) {\n console.error(e)\n console.log('❌ Firebase admin app initialized failed', e)\n }\n }\n\n const fcm = admin.messaging()\n\n const adapter: FirebasePushAdapter = () => ({\n name: 'firebase-admin',\n /**\n * Sends push notifications using Firebase Admin SDK\n */\n async sendPush(message: {\n body: string\n data?: Record<string, string>\n options?: Record<string, string>\n title: string\n }) {\n const sendPushOptions = mapPayloadToFirebasePush(message)\n\n const notification = {\n body: message.body,\n title: message.title,\n }\n\n console.log({ sendPushOptions })\n\n try {\n if (sendPushOptions.topic) {\n const payload: TopicMessage = {\n data: message.data,\n notification,\n topic: sendPushOptions.topic,\n }\n\n const res = await fcm.send(payload)\n console.log('✅ Push to single token:', res)\n if (res) {\n return { messageId: res }\n }\n }\n if (sendPushOptions.tokens && sendPushOptions.tokens.length) {\n const res = await fcm.sendEachForMulticast({\n data: message.data,\n notification,\n tokens: sendPushOptions.tokens,\n })\n console.log('✅ Push multicast result:', res)\n return res\n }\n\n if (sendPushOptions.token) {\n const res = await fcm.send({\n data: message.data,\n notification,\n token: sendPushOptions.token,\n })\n console.log('✅ Push to token:', res)\n\n return { messageId: res }\n }\n\n throw new Error('No token(s) or topic provided')\n } catch (err) {\n console.error('❌ Firebase push error:', err)\n throw err\n }\n },\n })\n return adapter\n}\n\nfunction mapPayloadToFirebasePush(message: SendPushOptions): firebasePushOptions {\n const pushOptions: Partial<firebasePushOptions> = {\n body: message.body,\n title: message.title,\n }\n\n if (!message.options) {\n return pushOptions as firebasePushOptions\n }\n\n if (message.options['token']) {\n const token = message.options['token']\n pushOptions.token = token\n }\n\n if (message.options['tokens']) {\n const tokens = message.options['tokens']\n pushOptions.tokens = tokens\n }\n\n if (message.options['topic']) {\n const topic = message.options['topic']\n pushOptions.topic = topic\n }\n\n if (message.options['android']) {\n const android = message.options['android']\n pushOptions.android = android\n }\n\n if (message.options['apns']) {\n const apns = message.options['apns']\n pushOptions.apns = apns\n }\n\n // if (message.text?.toString().trim().length > 0) {\n // \tpushOptions.text = message.text\n // } else {\n // \tpushOptions.text = \"Please view this email in an HTML-compatible client.\"\n // }\n\n // if (message.html?.toString().trim()) {\n // \tpushOptions.html = message.html.toString()\n // }\n\n // if (message.attachments?.length) {\n // \tif (message.attachments.length > 10) {\n // \t\tthrow new APIError(\"Maximum of 10 attachments allowed\", 400)\n // \t}\n // \tpushOptions.attachments = mapAttachments(message.attachments)\n // }\n\n // if (message.replyTo) {\n // \tpushOptions.replyTo = mapAddresses(message.replyTo)\n // }\n\n // if (message.cc) {\n // \tpushOptions.cc = mapAddresses(message.cc)\n // }\n\n // if (message.bcc) {\n // \tpushOptions.bcc = mapAddresses(message.bcc)\n // }\n\n return pushOptions as firebasePushOptions\n}\n\n// function mapFromAddress(\n// \taddress: SendPushOptions[\"from\"],\n// \tdefaultFromName: string,\n// \tdefaultFromAddress: string\n// ): firebasePushOptions[\"from\"] {\n// \tif (!address) {\n// \t\treturn `${defaultFromName} <${defaultFromAddress}>`\n// \t}\n\n// \tif (typeof address === \"string\") {\n// \t\treturn address\n// \t}\n\n// \treturn `${address.name} <${address.address}>`\n// }\n\n// function mapAddresses(\n// \taddresses: SendPushOptions[\"to\"]\n// ): firebasePushOptions[\"to\"] {\n// \tif (!addresses) {\n// \t\treturn \"\"\n// \t}\n\n// \tif (typeof addresses === \"string\") {\n// \t\treturn addresses\n// \t}\n\n// \tif (Array.isArray(addresses)) {\n// \t\treturn addresses.map((address) =>\n// \t\t\ttypeof address === \"string\" ? address : address.address\n// \t\t)\n// \t}\n\n// \treturn [addresses.address]\n// }\n\n// function mapAttachments(\n// \tattachments: SendPushOptions[\"attachments\"]\n// ): firebasePushOptions[\"attachments\"] {\n// \tif (!attachments) {\n// \t\treturn []\n// \t}\n\n// \tif (attachments.length > 10) {\n// \t\tthrow new APIError(\"Maximum of 10 attachments allowed\", 400)\n// \t}\n\n// \treturn attachments.map((attachment) => {\n// \t\tif (!attachment.filename || !attachment.content) {\n// \t\t\tthrow new APIError(\"Attachment is missing filename or content\", 400)\n// \t\t}\n\n// \t\tif (typeof attachment.content === \"string\") {\n// \t\t\treturn {\n// \t\t\t\tcontent: Buffer.from(attachment.content).toString(\"base64\"),\n// \t\t\t\tfilename: attachment.filename,\n// \t\t\t}\n// \t\t}\n\n// \t\tif (attachment.content instanceof Buffer) {\n// \t\t\treturn {\n// \t\t\t\tcontent: attachment.content.toString(\"base64\"),\n// \t\t\t\tfilename: attachment.filename,\n// \t\t\t}\n// \t\t}\n\n// \t\tthrow new APIError(\"Attachment content must be a string or a buffer\", 400)\n// \t})\n// }\n\ntype firebasePushOptions = {\n android?: admin.messaging.AndroidConfig\n apns?: admin.messaging.ApnsConfig\n body: string\n /**\n * The date and time to send the email. If not provided, the email will be sent immediately.\n */\n scheduledAt?: string\n title: string\n token?: string\n tokens?: string[]\n\n topic?: string\n}\n\n// type Attachment = {\n// \t/** Content of an attached file. */\n// \tcontent: string\n// \t/** Name of attached file. */\n// \tfilename: string\n// }\n"],"names":["admin","firebaseAdapter","serviceAccountJSON","apps","length","Error","creds","undefined","initializeApp","credential","cert","applicationDefault","console","log","app","name","e","error","fcm","messaging","adapter","sendPush","message","sendPushOptions","mapPayloadToFirebasePush","notification","body","title","topic","payload","data","res","send","messageId","tokens","sendEachForMulticast","token","err","pushOptions","options","android","apns"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { payloadPush } from '../payloadPush.js';
|
|
2
|
+
const fcmPushEndpointHandler = (pluginOptions)=>{
|
|
3
|
+
const handler = async (req)=>{
|
|
4
|
+
const adapterName = pluginOptions.pushAdapter.name;
|
|
5
|
+
try {
|
|
6
|
+
if (!req) {
|
|
7
|
+
return Response.json({});
|
|
8
|
+
}
|
|
9
|
+
const { payload } = req;
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
const data = await req.json();
|
|
12
|
+
payload.logger.info({
|
|
13
|
+
data
|
|
14
|
+
});
|
|
15
|
+
const { body, title, topic } = data;
|
|
16
|
+
if (!req.user) {
|
|
17
|
+
return Response.json({
|
|
18
|
+
error: 'Unauthorized',
|
|
19
|
+
success: false
|
|
20
|
+
}, {
|
|
21
|
+
status: 401
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// 2️⃣ Optional: restrict to admin users
|
|
25
|
+
// if (!req.user.roles?.some(r => r === 'admin')) {
|
|
26
|
+
// return Response.json({ success: false, error: 'Forbidden' }, { status: 403 })
|
|
27
|
+
// }
|
|
28
|
+
payloadPush.init(payload, pluginOptions.pushAdapter);
|
|
29
|
+
payload.logger.info(`Sending Push sent via ${adapterName}...`);
|
|
30
|
+
payload.logger.info('title=' + title);
|
|
31
|
+
payload.logger.info('body=' + body);
|
|
32
|
+
payload.logger.info('topic=' + topic);
|
|
33
|
+
await payloadPush.sendPush({
|
|
34
|
+
body,
|
|
35
|
+
title,
|
|
36
|
+
// data,
|
|
37
|
+
options: {
|
|
38
|
+
topic
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
payload.logger.info(`📱 Push sent via ${adapterName}`);
|
|
42
|
+
return Response.json({
|
|
43
|
+
success: true
|
|
44
|
+
}, {
|
|
45
|
+
status: 200
|
|
46
|
+
});
|
|
47
|
+
} catch (err) {
|
|
48
|
+
return Response.json({
|
|
49
|
+
error: 'Push failed',
|
|
50
|
+
success: false
|
|
51
|
+
}, {
|
|
52
|
+
status: 500
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return handler;
|
|
57
|
+
};
|
|
58
|
+
export default fcmPushEndpointHandler;
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=fcmPushEndpointHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/fcmPushEndpointHandler.ts"],"sourcesContent":["import type { PayloadHandler, PayloadRequest } from 'payload'\n\nimport type { PayloadPushPluginConfig } from '../index.js'\n\nimport { payloadPush } from '../payloadPush.js'\n\nconst fcmPushEndpointHandler = (pluginOptions: PayloadPushPluginConfig) => {\n const handler: PayloadHandler = async (req: PayloadRequest) => {\n const adapterName = pluginOptions.pushAdapter.name\n try {\n if (!req) {\n return Response.json({})\n }\n const { payload } = req\n\n // @ts-ignore\n const data = await req.json()\n\n payload.logger.info({ data })\n\n const { body, title, topic } = data as Record<string, string>\n\n if (!req.user) {\n return Response.json({ error: 'Unauthorized', success: false }, { status: 401 })\n }\n\n // 2️⃣ Optional: restrict to admin users\n // if (!req.user.roles?.some(r => r === 'admin')) {\n // return Response.json({ success: false, error: 'Forbidden' }, { status: 403 })\n // }\n\n payloadPush.init(payload, pluginOptions.pushAdapter)\n\n payload.logger.info(`Sending Push sent via ${adapterName}...`)\n payload.logger.info('title=' + title)\n payload.logger.info('body=' + body)\n payload.logger.info('topic=' + topic)\n\n await payloadPush.sendPush({\n body,\n title,\n // data,\n options: { topic },\n })\n\n payload.logger.info(`📱 Push sent via ${adapterName}`)\n\n return Response.json({ success: true }, { status: 200 })\n } catch (err) {\n return Response.json({ error: 'Push failed', success: false }, { status: 500 })\n }\n }\n\n return handler\n}\n\nexport default fcmPushEndpointHandler\n"],"names":["payloadPush","fcmPushEndpointHandler","pluginOptions","handler","req","adapterName","pushAdapter","name","Response","json","payload","data","logger","info","body","title","topic","user","error","success","status","init","sendPush","options","err"],"mappings":"AAIA,SAASA,WAAW,QAAQ,oBAAmB;AAE/C,MAAMC,yBAAyB,CAACC;IAC9B,MAAMC,UAA0B,OAAOC;QACrC,MAAMC,cAAcH,cAAcI,WAAW,CAACC,IAAI;QAClD,IAAI;YACF,IAAI,CAACH,KAAK;gBACR,OAAOI,SAASC,IAAI,CAAC,CAAC;YACxB;YACA,MAAM,EAAEC,OAAO,EAAE,GAAGN;YAEpB,aAAa;YACb,MAAMO,OAAO,MAAMP,IAAIK,IAAI;YAE3BC,QAAQE,MAAM,CAACC,IAAI,CAAC;gBAAEF;YAAK;YAE3B,MAAM,EAAEG,IAAI,EAAEC,KAAK,EAAEC,KAAK,EAAE,GAAGL;YAE/B,IAAI,CAACP,IAAIa,IAAI,EAAE;gBACb,OAAOT,SAASC,IAAI,CAAC;oBAAES,OAAO;oBAAgBC,SAAS;gBAAM,GAAG;oBAAEC,QAAQ;gBAAI;YAChF;YAEA,wCAAwC;YACxC,mDAAmD;YACnD,mFAAmF;YACnF,IAAI;YAEJpB,YAAYqB,IAAI,CAACX,SAASR,cAAcI,WAAW;YAEnDI,QAAQE,MAAM,CAACC,IAAI,CAAC,CAAC,sBAAsB,EAAER,YAAY,GAAG,CAAC;YAC7DK,QAAQE,MAAM,CAACC,IAAI,CAAC,WAAWE;YAC/BL,QAAQE,MAAM,CAACC,IAAI,CAAC,UAAUC;YAC9BJ,QAAQE,MAAM,CAACC,IAAI,CAAC,WAAWG;YAE/B,MAAMhB,YAAYsB,QAAQ,CAAC;gBACzBR;gBACAC;gBACA,QAAQ;gBACRQ,SAAS;oBAAEP;gBAAM;YACnB;YAEAN,QAAQE,MAAM,CAACC,IAAI,CAAC,CAAC,iBAAiB,EAAER,aAAa;YAErD,OAAOG,SAASC,IAAI,CAAC;gBAAEU,SAAS;YAAK,GAAG;gBAAEC,QAAQ;YAAI;QACxD,EAAE,OAAOI,KAAK;YACZ,OAAOhB,SAASC,IAAI,CAAC;gBAAES,OAAO;gBAAeC,SAAS;YAAM,GAAG;gBAAEC,QAAQ;YAAI;QAC/E;IACF;IAEA,OAAOjB;AACT;AAEA,eAAeF,uBAAsB"}
|
package/dist/index.d.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["index.d.ts"],"sourcesContent":["import type { Config } from 'payload';\nimport type { PushAdapter } from './types/index.js';\nexport type PayloadPushPluginConfig = {\n disabled?: boolean;\n pushAdapter: PushAdapter;\n};\nexport declare const payloadPushPlugin: (pluginOptions?: PayloadPushPluginConfig) => (config: Config) => Config;\n"],"names":[],"mappings":"AAMA,WAAgH"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Config } from 'payload';
|
|
2
|
+
import type { PushAdapter } from './types/index.js';
|
|
3
|
+
export type PayloadPushPluginConfig = {
|
|
4
|
+
disabled?: boolean;
|
|
5
|
+
pushAdapter: PushAdapter;
|
|
6
|
+
};
|
|
7
|
+
export declare const payloadPushPlugin: (pluginOptions?: PayloadPushPluginConfig) => (config: Config) => Config;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fcmPushEndpointHandler from './endpoints/fcmPushEndpointHandler.js';
|
|
2
|
+
import { payloadPush } from './payloadPush.js';
|
|
3
|
+
export const payloadPushPlugin = (pluginOptions)=>(config)=>{
|
|
4
|
+
if (!config.collections) {
|
|
5
|
+
config.collections = [];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.
|
|
9
|
+
* If your plugin heavily modifies the database schema, you may want to remove this property.
|
|
10
|
+
*/ if (pluginOptions?.disabled) {
|
|
11
|
+
return config;
|
|
12
|
+
}
|
|
13
|
+
if (!pluginOptions?.pushAdapter) {
|
|
14
|
+
throw new Error('pushAdapter is missing');
|
|
15
|
+
}
|
|
16
|
+
if (!config.endpoints) {
|
|
17
|
+
config.endpoints = [];
|
|
18
|
+
}
|
|
19
|
+
if (!config.endpoints) {
|
|
20
|
+
config.endpoints = [];
|
|
21
|
+
}
|
|
22
|
+
if (!config.admin) {
|
|
23
|
+
config.admin = {};
|
|
24
|
+
}
|
|
25
|
+
if (!config.admin.components) {
|
|
26
|
+
config.admin.components = {};
|
|
27
|
+
}
|
|
28
|
+
config.endpoints.push({
|
|
29
|
+
handler: fcmPushEndpointHandler(pluginOptions),
|
|
30
|
+
method: 'post',
|
|
31
|
+
path: '/test-push/firebase'
|
|
32
|
+
});
|
|
33
|
+
const incomingOnInit = config.onInit;
|
|
34
|
+
config.onInit = async (payload)=>{
|
|
35
|
+
// Ensure we are executing any existing onInit functions before running our own.
|
|
36
|
+
if (incomingOnInit) {
|
|
37
|
+
await incomingOnInit(payload);
|
|
38
|
+
}
|
|
39
|
+
payloadPush.init(payload, pluginOptions.pushAdapter);
|
|
40
|
+
// Optionally inject into payload so user can use: payload.push.sendPush()
|
|
41
|
+
// @ts-ignore
|
|
42
|
+
payload.push = payloadPush;
|
|
43
|
+
payload.logger.info('📱 Payload Push initialized with custom adapter');
|
|
44
|
+
};
|
|
45
|
+
return config;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { PushAdapter } from './types/index.js'\n\nimport fcmPushEndpointHandler from './endpoints/fcmPushEndpointHandler.js'\nimport { payloadPush } from './payloadPush.js'\n\nexport type PayloadPushPluginConfig = {\n disabled?: boolean\n pushAdapter: PushAdapter\n}\n\nexport const payloadPushPlugin =\n (pluginOptions?: PayloadPushPluginConfig) =>\n (config: Config): Config => {\n if (!config.collections) {\n config.collections = []\n }\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields so the database schema is consistent which is important for migrations.\n * If your plugin heavily modifies the database schema, you may want to remove this property.\n */\n if (pluginOptions?.disabled) {\n return config\n }\n\n if (!pluginOptions?.pushAdapter) {\n throw new Error('pushAdapter is missing')\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n if (!config.admin) {\n config.admin = {}\n }\n\n if (!config.admin.components) {\n config.admin.components = {}\n }\n\n config.endpoints.push({\n handler: fcmPushEndpointHandler(pluginOptions),\n method: 'post',\n path: '/test-push/firebase',\n })\n\n const incomingOnInit = config.onInit\n\n config.onInit = async (payload) => {\n // Ensure we are executing any existing onInit functions before running our own.\n if (incomingOnInit) {\n await incomingOnInit(payload)\n }\n\n payloadPush.init(payload, pluginOptions.pushAdapter)\n\n // Optionally inject into payload so user can use: payload.push.sendPush()\n // @ts-ignore\n payload.push = payloadPush\n\n payload.logger.info('📱 Payload Push initialized with custom adapter')\n }\n\n return config\n }\n"],"names":["fcmPushEndpointHandler","payloadPush","payloadPushPlugin","pluginOptions","config","collections","disabled","pushAdapter","Error","endpoints","admin","components","push","handler","method","path","incomingOnInit","onInit","payload","init","logger","info"],"mappings":"AAIA,OAAOA,4BAA4B,wCAAuC;AAC1E,SAASC,WAAW,QAAQ,mBAAkB;AAO9C,OAAO,MAAMC,oBACX,CAACC,gBACD,CAACC;QACC,IAAI,CAACA,OAAOC,WAAW,EAAE;YACvBD,OAAOC,WAAW,GAAG,EAAE;QACzB;QAEA;;;KAGC,GACD,IAAIF,eAAeG,UAAU;YAC3B,OAAOF;QACT;QAEA,IAAI,CAACD,eAAeI,aAAa;YAC/B,MAAM,IAAIC,MAAM;QAClB;QAEA,IAAI,CAACJ,OAAOK,SAAS,EAAE;YACrBL,OAAOK,SAAS,GAAG,EAAE;QACvB;QAEA,IAAI,CAACL,OAAOK,SAAS,EAAE;YACrBL,OAAOK,SAAS,GAAG,EAAE;QACvB;QAEA,IAAI,CAACL,OAAOM,KAAK,EAAE;YACjBN,OAAOM,KAAK,GAAG,CAAC;QAClB;QAEA,IAAI,CAACN,OAAOM,KAAK,CAACC,UAAU,EAAE;YAC5BP,OAAOM,KAAK,CAACC,UAAU,GAAG,CAAC;QAC7B;QAEAP,OAAOK,SAAS,CAACG,IAAI,CAAC;YACpBC,SAASb,uBAAuBG;YAChCW,QAAQ;YACRC,MAAM;QACR;QAEA,MAAMC,iBAAiBZ,OAAOa,MAAM;QAEpCb,OAAOa,MAAM,GAAG,OAAOC;YACrB,gFAAgF;YAChF,IAAIF,gBAAgB;gBAClB,MAAMA,eAAeE;YACvB;YAEAjB,YAAYkB,IAAI,CAACD,SAASf,cAAcI,WAAW;YAEnD,0EAA0E;YAC1E,aAAa;YACbW,QAAQN,IAAI,GAAGX;YAEfiB,QAAQE,MAAM,CAACC,IAAI,CAAC;QACtB;QAEA,OAAOjB;IACT,EAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["payloadPush.d.ts"],"sourcesContent":["import type { Payload } from 'payload';\nimport type { PushAdapter } from './types/index.js';\ndeclare class PayloadPush {\n private adapter?;\n private payload?;\n init(payload: Payload, adapterFactory: PushAdapter): void;\n sendPush(message: {\n body: string;\n data?: Record<string, any>;\n options?: Record<string, any>;\n title: string;\n }): Promise<unknown>;\n}\nexport declare const payloadPush: PayloadPush;\nexport {};\n"],"names":[],"mappings":"AAcA,WAAU"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { PushAdapter } from './types/index.js';
|
|
3
|
+
declare class PayloadPush {
|
|
4
|
+
private adapter?;
|
|
5
|
+
private payload?;
|
|
6
|
+
init(payload: Payload, adapterFactory: PushAdapter): void;
|
|
7
|
+
sendPush(message: {
|
|
8
|
+
body: string;
|
|
9
|
+
data?: Record<string, any>;
|
|
10
|
+
options?: Record<string, any>;
|
|
11
|
+
title: string;
|
|
12
|
+
}): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
export declare const payloadPush: PayloadPush;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
class PayloadPush {
|
|
2
|
+
adapter;
|
|
3
|
+
payload;
|
|
4
|
+
init(payload, adapterFactory) {
|
|
5
|
+
this.payload = payload;
|
|
6
|
+
this.adapter = adapterFactory({
|
|
7
|
+
payload
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
async sendPush(message) {
|
|
11
|
+
if (!this.payload) {
|
|
12
|
+
throw new Error('PayloadPush not initialized');
|
|
13
|
+
}
|
|
14
|
+
if (!this.adapter) {
|
|
15
|
+
throw new Error('Push adapter not initialized');
|
|
16
|
+
}
|
|
17
|
+
if (typeof this.adapter.sendPush !== 'function') {
|
|
18
|
+
throw new Error('Push adapter missing sendPush() method');
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const result = await this.adapter.sendPush(message);
|
|
22
|
+
this.payload.logger.info(`📤 Push sent via ${this.adapter.name}`);
|
|
23
|
+
return result;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
this.payload.logger.error('❌ Push send failed', err);
|
|
26
|
+
throw err;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export const payloadPush = new PayloadPush();
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=payloadPush.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/payloadPush.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport type { PushAdapter } from './types/index.js'\n\nclass PayloadPush {\n private adapter?: ReturnType<PushAdapter>\n private payload?: Payload\n\n init(payload: Payload, adapterFactory: PushAdapter) {\n this.payload = payload\n this.adapter = adapterFactory({ payload })\n }\n\n async sendPush(message: {\n body: string\n data?: Record<string, any>\n options?: Record<string, any>\n title: string\n }) {\n if (!this.payload) {\n throw new Error('PayloadPush not initialized')\n }\n if (!this.adapter) {\n throw new Error('Push adapter not initialized')\n }\n\n if (typeof this.adapter.sendPush !== 'function') {\n throw new Error('Push adapter missing sendPush() method')\n }\n\n try {\n const result = await this.adapter.sendPush(message)\n this.payload.logger.info(`📤 Push sent via ${this.adapter.name}`)\n return result\n } catch (err: any) {\n this.payload.logger.error('❌ Push send failed', err)\n throw err\n }\n }\n}\n\nexport const payloadPush = new PayloadPush()\n"],"names":["PayloadPush","adapter","payload","init","adapterFactory","sendPush","message","Error","result","logger","info","name","err","error","payloadPush"],"mappings":"AAIA,MAAMA;IACIC,QAAiC;IACjCC,QAAiB;IAEzBC,KAAKD,OAAgB,EAAEE,cAA2B,EAAE;QAClD,IAAI,CAACF,OAAO,GAAGA;QACf,IAAI,CAACD,OAAO,GAAGG,eAAe;YAAEF;QAAQ;IAC1C;IAEA,MAAMG,SAASC,OAKd,EAAE;QACD,IAAI,CAAC,IAAI,CAACJ,OAAO,EAAE;YACjB,MAAM,IAAIK,MAAM;QAClB;QACA,IAAI,CAAC,IAAI,CAACN,OAAO,EAAE;YACjB,MAAM,IAAIM,MAAM;QAClB;QAEA,IAAI,OAAO,IAAI,CAACN,OAAO,CAACI,QAAQ,KAAK,YAAY;YAC/C,MAAM,IAAIE,MAAM;QAClB;QAEA,IAAI;YACF,MAAMC,SAAS,MAAM,IAAI,CAACP,OAAO,CAACI,QAAQ,CAACC;YAC3C,IAAI,CAACJ,OAAO,CAACO,MAAM,CAACC,IAAI,CAAC,CAAC,iBAAiB,EAAE,IAAI,CAACT,OAAO,CAACU,IAAI,EAAE;YAChE,OAAOH;QACT,EAAE,OAAOI,KAAU;YACjB,IAAI,CAACV,OAAO,CAACO,MAAM,CAACI,KAAK,CAAC,sBAAsBD;YAChD,MAAMA;QACR;IACF;AACF;AAEA,OAAO,MAAME,cAAc,IAAId,cAAa"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/global.d.ts"],"sourcesContent":["import type { payloadPush } from '../payloadPush.js'\nimport { BasePayload } from 'payload'\n\ndeclare module 'payload' {\n interface BasePayload {\n /**\n * Custom push method added to the Payload instance\n */\n push: typeof payloadPush.sendPush\n }\n}\n"],"names":[],"mappings":"AACA,WAAqC"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Options for sending an push notification. Allows access to the PayloadRequest object
|
|
4
|
+
*/
|
|
5
|
+
export type SendPushOptions = {
|
|
6
|
+
title: string;
|
|
7
|
+
body: string;
|
|
8
|
+
data?: Record<string, any>;
|
|
9
|
+
options?: Record<string, any>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Email adapter after it has been initialized. This is used internally by Payload.
|
|
13
|
+
*/
|
|
14
|
+
export type InitializedPushAdapter<TSendPushResponse = unknown> = ReturnType<PushAdapter<TSendPushResponse>>;
|
|
15
|
+
/**
|
|
16
|
+
* Push adapter interface. Allows a generic type for the response of the sendEmail method.
|
|
17
|
+
*
|
|
18
|
+
* This is the interface to use if you are creating a new push notification adapter.
|
|
19
|
+
*/
|
|
20
|
+
export type PushAdapter<TSendPushResponse = unknown> = ({ payload }: {
|
|
21
|
+
payload: Payload;
|
|
22
|
+
}) => {
|
|
23
|
+
name: string;
|
|
24
|
+
sendPush: (message: SendPushOptions) => Promise<TSendPushResponse>;
|
|
25
|
+
};
|
|
26
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/index.ts"],"sourcesContent":["import type { Payload } from 'payload'\n// type Prettify<T> = {\n// [K in keyof T]: T[K];\n// } & NonNullable<unknown>;\n\n/**\n * Options for sending an push notification. Allows access to the PayloadRequest object\n */\nexport type SendPushOptions = {\n title: string\n body: string\n data?: Record<string, any>\n options?: Record<string, any>\n}\n//Prettify<Nodpush notificationerSendMailOptions>;\n/**\n * Email adapter after it has been initialized. This is used internally by Payload.\n */\nexport type InitializedPushAdapter<TSendPushResponse = unknown> = ReturnType<\n PushAdapter<TSendPushResponse>\n>\n/**\n * Push adapter interface. Allows a generic type for the response of the sendEmail method.\n *\n * This is the interface to use if you are creating a new push notification adapter.\n */\nexport type PushAdapter<TSendPushResponse = unknown> = ({ payload }: { payload: Payload }) => {\n // defaultFromAddress: string;\n // defaultFromName: string;\n name: string\n sendPush: (message: SendPushOptions) => Promise<TSendPushResponse>\n}\nexport {}\n"],"names":[],"mappings":"AAgCA,WAAS"}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kfiross44/payload-push",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "A blank template to get started with Payload 3.0",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"payload",
|
|
9
|
+
"cms",
|
|
10
|
+
"push",
|
|
11
|
+
"fcm",
|
|
12
|
+
"push-message",
|
|
13
|
+
"typescript",
|
|
14
|
+
"react",
|
|
15
|
+
"nextjs"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"import": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@changesets/cli": "^2.30.0",
|
|
31
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
32
|
+
"@payloadcms/db-mongodb": "3.37.0",
|
|
33
|
+
"@payloadcms/db-postgres": "3.37.0",
|
|
34
|
+
"@payloadcms/db-sqlite": "3.37.0",
|
|
35
|
+
"@payloadcms/eslint-config": "3.9.0",
|
|
36
|
+
"@payloadcms/next": "3.37.0",
|
|
37
|
+
"@payloadcms/richtext-lexical": "3.37.0",
|
|
38
|
+
"@payloadcms/ui": "3.37.0",
|
|
39
|
+
"@playwright/test": "1.58.2",
|
|
40
|
+
"@swc-node/register": "1.10.9",
|
|
41
|
+
"@swc/cli": "0.6.0",
|
|
42
|
+
"@types/node": "22.19.9",
|
|
43
|
+
"@types/react": "19.2.9",
|
|
44
|
+
"@types/react-dom": "19.2.3",
|
|
45
|
+
"copyfiles": "2.4.1",
|
|
46
|
+
"cross-env": "^7.0.3",
|
|
47
|
+
"eslint": "^9.23.0",
|
|
48
|
+
"eslint-config-next": "15.4.11",
|
|
49
|
+
"graphql": "^16.8.1",
|
|
50
|
+
"mongodb-memory-server": "10.1.4",
|
|
51
|
+
"next": "15.4.11",
|
|
52
|
+
"open": "^10.1.0",
|
|
53
|
+
"payload": "3.37.0",
|
|
54
|
+
"prettier": "^3.4.2",
|
|
55
|
+
"qs-esm": "7.0.2",
|
|
56
|
+
"react": "19.2.1",
|
|
57
|
+
"react-dom": "19.2.1",
|
|
58
|
+
"rimraf": "3.0.2",
|
|
59
|
+
"sharp": "0.34.2",
|
|
60
|
+
"sort-package-json": "^2.10.0",
|
|
61
|
+
"typescript": "5.7.3",
|
|
62
|
+
"vite-tsconfig-paths": "6.0.5",
|
|
63
|
+
"vitest": "4.0.18"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"firebase-admin": "^13.5.0",
|
|
67
|
+
"payload": "^3.37.0"
|
|
68
|
+
},
|
|
69
|
+
"engines": {
|
|
70
|
+
"node": "^18.20.2 || >=20.9.0",
|
|
71
|
+
"pnpm": "^9 || ^10"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
},
|
|
76
|
+
"registry": "https://registry.npmjs.org/",
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
79
|
+
"format": "bun x prettier --write '**/*.{js,jsx,ts,tsx,json,md,yaml,yml}'",
|
|
80
|
+
"release": "bun run build && changeset publish",
|
|
81
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths --ignore **/*.d.ts",
|
|
82
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
83
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
84
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
85
|
+
"dev": "next dev dev --turbo",
|
|
86
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
87
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
88
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
89
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
90
|
+
"generate:types": "pnpm dev:generate-types",
|
|
91
|
+
"lint": "eslint",
|
|
92
|
+
"lint:fix": "eslint ./src --fix",
|
|
93
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
94
|
+
"test:e2e": "playwright test",
|
|
95
|
+
"test:int": "vitest"
|
|
96
|
+
}
|
|
97
|
+
}
|