const ONESIGNAL_APP_ID = process.env.ONESIGNAL_APP_ID;
const ONESIGNAL_REST_API_KEY = process.env.ONESIGNAL_REST_API_KEY;
// POST /api/webhooks/revenuecat
async function handleRevenueCatWebhook(req, res) {
const { event } = req.body;
const eventId = event.id;
const eventType = event.type;
const appUserId = event.app_user_id;
// Idempotency check: skip if already processed
const existingEvent = await db.query(
'SELECT 1 FROM processed_webhook_events WHERE event_id = $1',
[eventId]
);
if (existingEvent.rows.length > 0) {
return res.json({ success: true, message: 'Event already processed' });
}
// Safety net: create user if missing
await db.query(`
INSERT INTO app_users (app_user_id, source, created_at, last_seen)
VALUES ($1, 'webhook_safety_net', NOW(), NOW())
ON CONFLICT (app_user_id) DO NOTHING
`, [appUserId]);
// Update subscription status
const isActive = ['INITIAL_PURCHASE', 'RENEWAL', 'PRODUCT_CHANGE', 'UNCANCELLATION']
.includes(eventType);
await db.query(`
INSERT INTO subscriptions (
app_user_id, status, product_id, expires_at, billing_issue, updated_at
) VALUES ($1, $2, $3, $4, $5, NOW())
ON CONFLICT (app_user_id) DO UPDATE SET
status = EXCLUDED.status,
product_id = EXCLUDED.product_id,
expires_at = EXCLUDED.expires_at,
billing_issue = EXCLUDED.billing_issue,
updated_at = NOW()
`, [
appUserId,
eventType.toLowerCase(),
event.product_id,
event.expiration_at_ms ? new Date(event.expiration_at_ms) : null,
eventType === 'BILLING_ISSUE'
]);
// Send push notification
await sendSubscriptionNotification(appUserId, eventType);
// Mark event as processed
await db.query(`
INSERT INTO processed_webhook_events (event_id, event_type, app_user_id, processed_at)
VALUES ($1, $2, $3, NOW())
`, [eventId, eventType, appUserId]);
return res.json({ success: true });
}
async function sendSubscriptionNotification(appUserId, eventType) {
const notifications = {
'INITIAL_PURCHASE': {
title: 'Welcome to Premium',
message: 'Thank you for subscribing. Enjoy all premium features.'
},
'RENEWAL': {
title: 'Subscription Renewed',
message: 'Your subscription has been renewed successfully.'
},
'CANCELLATION': {
title: 'Subscription Cancelled',
message: 'Your subscription will remain active until the end of the billing period.'
},
'UNCANCELLATION': {
title: 'Subscription Reactivated',
message: 'Your subscription has been reactivated.'
},
'BILLING_ISSUE': {
title: 'Payment Issue',
message: 'There was a problem processing your payment. Please update your payment method.'
},
'EXPIRATION': {
title: 'Subscription Expired',
message: 'Your premium access has ended. Resubscribe to continue enjoying all features.'
}
};
const notification = notifications[eventType];
if (!notification) return;
await fetch('https://onesignal.com/api/v1/notifications', {
method: 'POST',
headers: {
'Authorization': `Basic ${ONESIGNAL_REST_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
app_id: ONESIGNAL_APP_ID,
include_aliases: { external_id: [appUserId] },
target_channel: 'push',
headings: { en: notification.title },
contents: { en: notification.message },
data: { type: eventType, screen: 'subscription' }
})
});
}