diff --git a/.gitignore b/.gitignore index 6bb00e0..df97d6c 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ yarn-error.log* /playwright-report/ /blob-report/ /playwright/.cache/ +/dev.db diff --git a/dev/app/(app)/demo/page.tsx b/dev/app/(app)/demo/page.tsx index c053d4e..d0dfb9f 100644 --- a/dev/app/(app)/demo/page.tsx +++ b/dev/app/(app)/demo/page.tsx @@ -75,27 +75,8 @@ export default function DemoPage() { setLoading(true) try { await pushManager.unsubscribe() - - // Remove the subscription from Payload's database - 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') - } + setIsSubscribed(false) + alert('Successfully unsubscribed from push notifications') } catch (error) { console.error('Failed to unsubscribe:', error) alert('Failed to unsubscribe from push notifications: ' + (error as Error).message) diff --git a/dev/helpers/testEmailAdapter.ts b/dev/helpers/testEmailAdapter.ts deleted file mode 100644 index 693cf97..0000000 --- a/dev/helpers/testEmailAdapter.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { EmailAdapter, SendEmailOptions } from 'payload' - -/** - * Logs all emails to stdout - */ -export const testEmailAdapter: EmailAdapter = ({ 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 -} diff --git a/dev/payload.config.ts b/dev/payload.config.ts index 55ecf7b..7c44b06 100644 --- a/dev/payload.config.ts +++ b/dev/payload.config.ts @@ -1,13 +1,13 @@ -import { mongooseAdapter } from '@payloadcms/db-mongodb' + import { lexicalEditor } from '@payloadcms/richtext-lexical' import path from 'path' import { buildConfig } from 'payload' import sharp from 'sharp' import { fileURLToPath } from 'url' -import {testEmailAdapter} from "./helpers/testEmailAdapter" import {seed} from "./seed" import { notificationsPlugin } from '@xtr-dev/payload-notifications' +import {sqliteAdapter} from "@payloadcms/db-sqlite" const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) @@ -16,144 +16,128 @@ if (!process.env.ROOT_DIR) { process.env.ROOT_DIR = dirname } -const buildConfigWithMemoryDB = async () => { - if (!process.env.DATABASE_URI) { - // Use a simple memory server instead of replica set for better stability - const { MongoMemoryServer } = await import('mongodb-memory-server') - const memoryDB = await MongoMemoryServer.create({ - instance: { - dbName: 'payloadmemory', - }, - }) - - process.env.DATABASE_URI = memoryDB.getUri() - } - - return buildConfig({ - admin: { - importMap: { - baseDir: path.resolve(dirname), - }, +export default buildConfig({ + admin: { + importMap: { + baseDir: path.resolve(dirname), }, - collections: [ - // Users collection with roles for authentication - { - slug: 'users', - auth: true, - admin: { - useAsTitle: 'email', + }, + collections: [ + // Users collection with roles for authentication + { + slug: 'users', + auth: true, + admin: { + useAsTitle: 'email', + }, + fields: [ + { + name: 'role', + type: 'select', + options: [ + { label: 'Admin', value: 'admin' }, + { label: 'Customer', value: 'customer' }, + ], + defaultValue: 'customer', + required: true, }, - fields: [ - { - name: 'role', - type: 'select', - options: [ - { label: 'Admin', value: 'admin' }, - { label: 'Customer', value: 'customer' }, - ], - defaultValue: 'customer', - 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) + { + name: 'firstName', + type: 'text', + label: 'First Name', + }, + { + name: 'lastName', + type: 'text', + label: 'Last Name', + }, + ], }, - plugins: [ - // Demo of the notifications plugin with relationships and channels - notificationsPlugin({ - channels: [ - { - id: 'general', - name: 'General Notifications', - description: 'General updates and announcements', - }, - { - id: 'orders', - name: 'Order Updates', - description: 'Order status changes and shipping notifications', - }, - { - id: 'products', - name: 'Product Updates', - description: 'New products, restocks, and price changes', - }, - { - id: 'marketing', - name: 'Marketing & Promotions', - description: 'Special offers, sales, and promotional content', - } - ], - 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' + ], + db: sqliteAdapter({ + client: { + url: process.env.DATABASE_URI || 'file:./dev.db', + }, + }), + editor: lexicalEditor(), + onInit: async (payload) => { + await seed(payload) + }, + plugins: [ + // Demo of the notifications plugin with relationships and channels + notificationsPlugin({ + channels: [ + { + id: 'general', + name: 'General Notifications', + description: 'General updates and announcements', + }, + { + id: 'orders', + name: 'Order Updates', + description: 'Order status changes and shipping notifications', + }, + { + id: 'products', + name: 'Product Updates', + description: 'New products, restocks, and price changes', + }, + { + id: 'marketing', + name: 'Marketing & Promotions', + description: 'Special offers, sales, and promotional content', + } + ], + 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 - let body = 'You have a new notification' - if (notification.message && Array.isArray(notification.message)) { - const textParts: string[] = [] - notification.message.forEach((block: any) => { - if (block.children && Array.isArray(block.children)) { - block.children.forEach((child: any) => { - if (child.text) textParts.push(child.text) - }) - } - }) - if (textParts.length > 0) { - body = textParts.join(' ').substring(0, 120) + (textParts.join(' ').length > 120 ? '...' : '') + // Extract text from rich text message + let body = 'You have a new notification' + if (notification.message && Array.isArray(notification.message)) { + const textParts: string[] = [] + notification.message.forEach((block: any) => { + if (block.children && Array.isArray(block.children)) { + block.children.forEach((child: any) => { + if (child.text) textParts.push(child.text) + }) } + }) + if (textParts.length > 0) { + body = textParts.join(' ').substring(0, 120) + (textParts.join(' ').length > 120 ? '...' : '') } + } - return { - title: `🔔 ${title}`, - body, - icon: '/icons/notification-icon.png', - badge: '/icons/notification-badge.png', - data: { - notificationId: notification.id, - url: `/admin/collections/notifications/${notification.id}`, - createdAt: notification.createdAt, - }, - actions: [ - { action: 'view', title: 'View in Admin', icon: '/icons/view.png' }, - { action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' } - ], - tag: `notification-${notification.id}`, - requireInteraction: false, - } + return { + title: `🔔 ${title}`, + body, + icon: '/icons/notification-icon.png', + badge: '/icons/notification-badge.png', + data: { + notificationId: notification.id, + url: `/admin/collections/notifications/${notification.id}`, + createdAt: notification.createdAt, + }, + actions: [ + { action: 'view', title: 'View in Admin', icon: '/icons/view.png' }, + { action: 'dismiss', title: 'Dismiss', icon: '/icons/dismiss.png' } + ], + tag: `notification-${notification.id}`, + requireInteraction: false, } } - }), - ], - secret: process.env.PAYLOAD_SECRET || 'test-secret_key', - sharp, - typescript: { - outputFile: path.resolve(dirname, 'payload-types.ts'), - }, - }) -} - -export default buildConfigWithMemoryDB() + } + }), + ], + secret: process.env.PAYLOAD_SECRET || 'test-secret_key', + sharp, + typescript: { + outputFile: path.resolve(dirname, 'payload-types.ts'), + }, +}) diff --git a/dev/tsconfig.json b/dev/tsconfig.json index 2732635..c057da5 100644 --- a/dev/tsconfig.json +++ b/dev/tsconfig.json @@ -13,6 +13,7 @@ ], "compilerOptions": { "baseUrl": "./", + "rootDir": "../", "paths": { "@payload-config": [ "./payload.config.ts" diff --git a/package.json b/package.json index 3f21629..2676e79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", "license": "MIT", "type": "module", diff --git a/src/collections/notifications.ts b/src/collections/notifications.ts index 953c298..640f6e6 100644 --- a/src/collections/notifications.ts +++ b/src/collections/notifications.ts @@ -188,9 +188,9 @@ export function createNotificationsCollection(options: NotificationsPluginOption 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 } else if (doc.recipient?.id) { recipientId = doc.recipient.id diff --git a/src/endpoints/push-notifications.ts b/src/endpoints/push-notifications.ts index 4093d38..22b845a 100644 --- a/src/endpoints/push-notifications.ts +++ b/src/endpoints/push-notifications.ts @@ -36,7 +36,7 @@ export function createPushNotificationEndpoints(options: NotificationsPluginOpti const pushManager = new WebPushManager(webPushConfig, req.payload) await pushManager.subscribe( - String(req.user.id), + req.user.id, subscription, userAgent, channels diff --git a/src/utils/webPush.ts b/src/utils/webPush.ts index c0a8f79..fba864a 100644 --- a/src/utils/webPush.ts +++ b/src/utils/webPush.ts @@ -57,7 +57,7 @@ export class WebPushManager { * Send push notification to all active subscriptions for a recipient */ public async sendToRecipient( - recipientId: string, + recipientId: string|number, title: string, body: string, options?: { @@ -186,7 +186,7 @@ export class WebPushManager { * Subscribe a user to push notifications */ public async subscribe( - userId: string, + userId: string | number, subscription: PushSubscription, userAgent?: string, channels?: string[] @@ -216,15 +216,6 @@ export class WebPushManager { }, }) } else { - console.info({ - user: userId, - endpoint: subscription.endpoint, - p256dh: subscription.keys.p256dh, - auth: subscription.keys.auth, - userAgent, - channels, - isActive: true, - }) // Create new subscription await this.payload.create({ collection: 'push-subscriptions',