mirror of
https://github.com/xtr-dev/payload-notifications.git
synced 2025-12-10 02:43:23 +00:00
Refactor: Simplify notifications plugin
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -47,3 +47,4 @@ yarn-error.log*
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
/dev.db
|
||||||
|
|||||||
@@ -75,27 +75,8 @@ export default function DemoPage() {
|
|||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
await pushManager.unsubscribe()
|
await pushManager.unsubscribe()
|
||||||
|
setIsSubscribed(false)
|
||||||
// Remove the subscription from Payload's database
|
alert('Successfully unsubscribed from push notifications')
|
||||||
const response = await fetch('/api/push-notifications/unsubscribe', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
user: 'customer@example.com', // Associate with the demo customer user
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
setIsSubscribed(false)
|
|
||||||
alert('Successfully unsubscribed from push notifications')
|
|
||||||
} else {
|
|
||||||
const error = await response.text()
|
|
||||||
console.warn('Failed to remove subscription from database:', error)
|
|
||||||
setIsSubscribed(false)
|
|
||||||
alert('Unsubscribed from browser, but may still be in database')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to unsubscribe:', error)
|
console.error('Failed to unsubscribe:', error)
|
||||||
alert('Failed to unsubscribe from push notifications: ' + (error as Error).message)
|
alert('Failed to unsubscribe from push notifications: ' + (error as Error).message)
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import type { EmailAdapter, SendEmailOptions } from 'payload'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs all emails to stdout
|
|
||||||
*/
|
|
||||||
export const testEmailAdapter: EmailAdapter<void> = ({ payload }) => ({
|
|
||||||
name: 'test-email-adapter',
|
|
||||||
defaultFromAddress: 'dev@payloadcms.com',
|
|
||||||
defaultFromName: 'Payload Test',
|
|
||||||
sendEmail: async (message) => {
|
|
||||||
const stringifiedTo = getStringifiedToAddress(message)
|
|
||||||
const res = `Test email to: '${stringifiedTo}', Subject: '${message.subject}'`
|
|
||||||
payload.logger.info({ content: message, msg: res })
|
|
||||||
return Promise.resolve()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function getStringifiedToAddress(message: SendEmailOptions): string | undefined {
|
|
||||||
let stringifiedTo: string | undefined
|
|
||||||
|
|
||||||
if (typeof message.to === 'string') {
|
|
||||||
stringifiedTo = message.to
|
|
||||||
} else if (Array.isArray(message.to)) {
|
|
||||||
stringifiedTo = message.to
|
|
||||||
.map((to: { address: string } | string) => {
|
|
||||||
if (typeof to === 'string') {
|
|
||||||
return to
|
|
||||||
} else if (to.address) {
|
|
||||||
return to.address
|
|
||||||
}
|
|
||||||
return ''
|
|
||||||
})
|
|
||||||
.join(', ')
|
|
||||||
} else if (message.to?.address) {
|
|
||||||
stringifiedTo = message.to.address
|
|
||||||
}
|
|
||||||
return stringifiedTo
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
|
||||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { buildConfig } from 'payload'
|
import { buildConfig } from 'payload'
|
||||||
import sharp from 'sharp'
|
import sharp from 'sharp'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
|
||||||
import {testEmailAdapter} from "./helpers/testEmailAdapter"
|
|
||||||
import {seed} from "./seed"
|
import {seed} from "./seed"
|
||||||
import { notificationsPlugin } from '@xtr-dev/payload-notifications'
|
import { notificationsPlugin } from '@xtr-dev/payload-notifications'
|
||||||
|
import {sqliteAdapter} from "@payloadcms/db-sqlite"
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -16,144 +16,128 @@ if (!process.env.ROOT_DIR) {
|
|||||||
process.env.ROOT_DIR = dirname
|
process.env.ROOT_DIR = dirname
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildConfigWithMemoryDB = async () => {
|
export default buildConfig({
|
||||||
if (!process.env.DATABASE_URI) {
|
admin: {
|
||||||
// Use a simple memory server instead of replica set for better stability
|
importMap: {
|
||||||
const { MongoMemoryServer } = await import('mongodb-memory-server')
|
baseDir: path.resolve(dirname),
|
||||||
const memoryDB = await MongoMemoryServer.create({
|
|
||||||
instance: {
|
|
||||||
dbName: 'payloadmemory',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
process.env.DATABASE_URI = memoryDB.getUri()
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildConfig({
|
|
||||||
admin: {
|
|
||||||
importMap: {
|
|
||||||
baseDir: path.resolve(dirname),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
collections: [
|
},
|
||||||
// Users collection with roles for authentication
|
collections: [
|
||||||
{
|
// Users collection with roles for authentication
|
||||||
slug: 'users',
|
{
|
||||||
auth: true,
|
slug: 'users',
|
||||||
admin: {
|
auth: true,
|
||||||
useAsTitle: 'email',
|
admin: {
|
||||||
|
useAsTitle: 'email',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'role',
|
||||||
|
type: 'select',
|
||||||
|
options: [
|
||||||
|
{ label: 'Admin', value: 'admin' },
|
||||||
|
{ label: 'Customer', value: 'customer' },
|
||||||
|
],
|
||||||
|
defaultValue: 'customer',
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
fields: [
|
{
|
||||||
{
|
name: 'firstName',
|
||||||
name: 'role',
|
type: 'text',
|
||||||
type: 'select',
|
label: 'First Name',
|
||||||
options: [
|
},
|
||||||
{ label: 'Admin', value: 'admin' },
|
{
|
||||||
{ label: 'Customer', value: 'customer' },
|
name: 'lastName',
|
||||||
],
|
type: 'text',
|
||||||
defaultValue: 'customer',
|
label: 'Last Name',
|
||||||
required: true,
|
},
|
||||||
},
|
],
|
||||||
{
|
|
||||||
name: 'firstName',
|
|
||||||
type: 'text',
|
|
||||||
label: 'First Name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'lastName',
|
|
||||||
type: 'text',
|
|
||||||
label: 'Last Name',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
db: mongooseAdapter({
|
|
||||||
ensureIndexes: true,
|
|
||||||
url: process.env.DATABASE_URI || '',
|
|
||||||
}),
|
|
||||||
editor: lexicalEditor(),
|
|
||||||
email: testEmailAdapter,
|
|
||||||
onInit: async (payload) => {
|
|
||||||
await seed(payload)
|
|
||||||
},
|
},
|
||||||
plugins: [
|
],
|
||||||
// Demo of the notifications plugin with relationships and channels
|
db: sqliteAdapter({
|
||||||
notificationsPlugin({
|
client: {
|
||||||
channels: [
|
url: process.env.DATABASE_URI || 'file:./dev.db',
|
||||||
{
|
},
|
||||||
id: 'general',
|
}),
|
||||||
name: 'General Notifications',
|
editor: lexicalEditor(),
|
||||||
description: 'General updates and announcements',
|
onInit: async (payload) => {
|
||||||
},
|
await seed(payload)
|
||||||
{
|
},
|
||||||
id: 'orders',
|
plugins: [
|
||||||
name: 'Order Updates',
|
// Demo of the notifications plugin with relationships and channels
|
||||||
description: 'Order status changes and shipping notifications',
|
notificationsPlugin({
|
||||||
},
|
channels: [
|
||||||
{
|
{
|
||||||
id: 'products',
|
id: 'general',
|
||||||
name: 'Product Updates',
|
name: 'General Notifications',
|
||||||
description: 'New products, restocks, and price changes',
|
description: 'General updates and announcements',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'marketing',
|
id: 'orders',
|
||||||
name: 'Marketing & Promotions',
|
name: 'Order Updates',
|
||||||
description: 'Special offers, sales, and promotional content',
|
description: 'Order status changes and shipping notifications',
|
||||||
}
|
},
|
||||||
],
|
{
|
||||||
webPush: {
|
id: 'products',
|
||||||
enabled: true,
|
name: 'Product Updates',
|
||||||
autoPush: true, // Enable automatic push notifications
|
description: 'New products, restocks, and price changes',
|
||||||
vapidPublicKey: process.env.VAPID_PUBLIC_KEY || 'BMrF5MbHcaEo6w4lPjG9m3BvONvFPfz7jLJ9t0F9yJGzSI3ZUHQj9fNUP7w2D8h1kI4x3YzJ1a4f0nS5g6t2F9L',
|
},
|
||||||
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY || 'your-private-key-here',
|
{
|
||||||
vapidSubject: 'mailto:test@example.com',
|
id: 'marketing',
|
||||||
// Custom notification transformer for demo
|
name: 'Marketing & Promotions',
|
||||||
transformNotification: (notification: any) => {
|
description: 'Special offers, sales, and promotional content',
|
||||||
const title = notification.title || 'New Notification'
|
}
|
||||||
|
],
|
||||||
|
webPush: {
|
||||||
|
enabled: true,
|
||||||
|
autoPush: true, // Enable automatic push notifications
|
||||||
|
vapidPublicKey: process.env.VAPID_PUBLIC_KEY || 'BMrF5MbHcaEo6w4lPjG9m3BvONvFPfz7jLJ9t0F9yJGzSI3ZUHQj9fNUP7w2D8h1kI4x3YzJ1a4f0nS5g6t2F9L',
|
||||||
|
vapidPrivateKey: process.env.VAPID_PRIVATE_KEY || 'your-private-key-here',
|
||||||
|
vapidSubject: 'mailto:test@example.com',
|
||||||
|
// Custom notification transformer for demo
|
||||||
|
transformNotification: (notification: any) => {
|
||||||
|
const title = notification.title || 'New Notification'
|
||||||
|
|
||||||
// Extract text from rich text message
|
// Extract text from rich text message
|
||||||
let body = 'You have a new notification'
|
let body = 'You have a new notification'
|
||||||
if (notification.message && Array.isArray(notification.message)) {
|
if (notification.message && Array.isArray(notification.message)) {
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
notification.message.forEach((block: any) => {
|
notification.message.forEach((block: any) => {
|
||||||
if (block.children && Array.isArray(block.children)) {
|
if (block.children && Array.isArray(block.children)) {
|
||||||
block.children.forEach((child: any) => {
|
block.children.forEach((child: any) => {
|
||||||
if (child.text) textParts.push(child.text)
|
if (child.text) textParts.push(child.text)
|
||||||
})
|
})
|
||||||
}
|
|
||||||
})
|
|
||||||
if (textParts.length > 0) {
|
|
||||||
body = textParts.join(' ').substring(0, 120) + (textParts.join(' ').length > 120 ? '...' : '')
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
if (textParts.length > 0) {
|
||||||
|
body = textParts.join(' ').substring(0, 120) + (textParts.join(' ').length > 120 ? '...' : '')
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `🔔 ${title}`,
|
title: `🔔 ${title}`,
|
||||||
body,
|
body,
|
||||||
icon: '/icons/notification-icon.png',
|
icon: '/icons/notification-icon.png',
|
||||||
badge: '/icons/notification-badge.png',
|
badge: '/icons/notification-badge.png',
|
||||||
data: {
|
data: {
|
||||||
notificationId: notification.id,
|
notificationId: notification.id,
|
||||||
url: `/admin/collections/notifications/${notification.id}`,
|
url: `/admin/collections/notifications/${notification.id}`,
|
||||||
createdAt: notification.createdAt,
|
createdAt: notification.createdAt,
|
||||||
},
|
},
|
||||||
actions: [
|
actions: [
|
||||||
{ action: 'view', title: 'View in Admin', icon: '/icons/view.png' },
|
{ action: 'view', title: 'View in Admin', icon: '/icons/view.png' },
|
||||||
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' }
|
{ action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' }
|
||||||
],
|
],
|
||||||
tag: `notification-${notification.id}`,
|
tag: `notification-${notification.id}`,
|
||||||
requireInteraction: false,
|
requireInteraction: false,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
],
|
}),
|
||||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
],
|
||||||
sharp,
|
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||||
typescript: {
|
sharp,
|
||||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
typescript: {
|
||||||
},
|
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||||
})
|
},
|
||||||
}
|
})
|
||||||
|
|
||||||
export default buildConfigWithMemoryDB()
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
|
"rootDir": "../",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@payload-config": [
|
"@payload-config": [
|
||||||
"./payload.config.ts"
|
"./payload.config.ts"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-notifications",
|
"name": "@xtr-dev/payload-notifications",
|
||||||
"version": "0.0.1",
|
"version": "0.0.2",
|
||||||
"description": "A PayloadCMS plugin that adds a configurable notifications collection for sending messages with titles, content, and attachable relationship items",
|
"description": "A PayloadCMS plugin that adds a configurable notifications collection for sending messages with titles, content, and attachable relationship items",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -188,9 +188,9 @@ export function createNotificationsCollection(options: NotificationsPluginOption
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let recipientId: string
|
let recipientId: string|number
|
||||||
|
|
||||||
if (typeof doc.recipient === 'string') {
|
if (typeof doc.recipient === 'string' || typeof doc.recipient === 'number') {
|
||||||
recipientId = doc.recipient
|
recipientId = doc.recipient
|
||||||
} else if (doc.recipient?.id) {
|
} else if (doc.recipient?.id) {
|
||||||
recipientId = doc.recipient.id
|
recipientId = doc.recipient.id
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function createPushNotificationEndpoints(options: NotificationsPluginOpti
|
|||||||
|
|
||||||
const pushManager = new WebPushManager(webPushConfig, req.payload)
|
const pushManager = new WebPushManager(webPushConfig, req.payload)
|
||||||
await pushManager.subscribe(
|
await pushManager.subscribe(
|
||||||
String(req.user.id),
|
req.user.id,
|
||||||
subscription,
|
subscription,
|
||||||
userAgent,
|
userAgent,
|
||||||
channels
|
channels
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class WebPushManager {
|
|||||||
* Send push notification to all active subscriptions for a recipient
|
* Send push notification to all active subscriptions for a recipient
|
||||||
*/
|
*/
|
||||||
public async sendToRecipient(
|
public async sendToRecipient(
|
||||||
recipientId: string,
|
recipientId: string|number,
|
||||||
title: string,
|
title: string,
|
||||||
body: string,
|
body: string,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -186,7 +186,7 @@ export class WebPushManager {
|
|||||||
* Subscribe a user to push notifications
|
* Subscribe a user to push notifications
|
||||||
*/
|
*/
|
||||||
public async subscribe(
|
public async subscribe(
|
||||||
userId: string,
|
userId: string | number,
|
||||||
subscription: PushSubscription,
|
subscription: PushSubscription,
|
||||||
userAgent?: string,
|
userAgent?: string,
|
||||||
channels?: string[]
|
channels?: string[]
|
||||||
@@ -216,15 +216,6 @@ export class WebPushManager {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.info({
|
|
||||||
user: userId,
|
|
||||||
endpoint: subscription.endpoint,
|
|
||||||
p256dh: subscription.keys.p256dh,
|
|
||||||
auth: subscription.keys.auth,
|
|
||||||
userAgent,
|
|
||||||
channels,
|
|
||||||
isActive: true,
|
|
||||||
})
|
|
||||||
// Create new subscription
|
// Create new subscription
|
||||||
await this.payload.create({
|
await this.payload.create({
|
||||||
collection: 'push-subscriptions',
|
collection: 'push-subscriptions',
|
||||||
|
|||||||
Reference in New Issue
Block a user