← Volver al listado de tecnologías

Push Notifications y Notificaciones Locales

Por: Artiko
defoldnotificationspushmobileengagementretention

Push Notifications y Notificaciones Locales

Las notificaciones son fundamentales para mantener el engagement y la retención de usuarios. Esta guía te enseñará a implementar un sistema completo de notificaciones para móviles.

Configuración Base de Notificaciones

1. Extensions y Dependencies

game.project Setup

[project]
dependencies = https://github.com/defold/extension-push/archive/main.zip

[android]
gcm_sender_id = 123456789012
package = com.miestudio.mijuego

[ios]
bundle_identifier = com.miestudio.mijuego

Permisos Android (AndroidManifest.xml)

<!-- Push Notifications -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />

<permission
    android:name="com.miestudio.mijuego.permission.C2D_MESSAGE"
    android:protectionLevel="signature" />
<uses-permission android:name="com.miestudio.mijuego.permission.C2D_MESSAGE" />

<application>
    <!-- Firebase Cloud Messaging -->
    <service android:name="com.defold.push.PushService">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>

    <!-- Default notification icon -->
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_icon"
        android:resource="@drawable/notification_icon" />

    <!-- Default notification color -->
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_color"
        android:resource="@color/notification_color" />

    <!-- Notification channels -->
    <meta-data
        android:name="com.google.firebase.messaging.default_notification_channel_id"
        android:value="@string/default_notification_channel_id" />
</application>

iOS Configuration (Info.plist)

<!-- Push Notifications -->
<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>

<!-- Firebase Configuration -->
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

2. Notification Manager Base

Sistema de Gestión de Notificaciones

-- notification_manager.script
local M = {}

function init(self)
    self.push_token = nil
    self.notification_enabled = false
    self.local_notifications = {}
    self.scheduled_notifications = {}

    -- Configuración
    self.config = {
        request_permission_on_start = true,
        default_badge_count = 0,
        max_local_notifications = 64,
        notification_channels = {
            default = {id = "default", name = "General", importance = "high"},
            gameplay = {id = "gameplay", name = "Gameplay", importance = "default"},
            social = {id = "social", name = "Social", importance = "high"},
            promotions = {id = "promotions", name = "Promotions", importance = "low"}
        }
    }

    -- Estado
    self.stats = {
        notifications_sent = 0,
        notifications_opened = 0,
        push_registered = false,
        permission_granted = false
    }

    print("Notification Manager initialized")
end

function M.initialize(self, callback)
    -- Configurar callback de push
    push.set_callback(function(self, payload, origin, activated)
        M.handle_push_message(self, payload, origin, activated)
    end)

    -- Solicitar permisos de notificación
    if self.config.request_permission_on_start then
        self:request_notification_permission(callback)
    end

    -- Configurar canales de notificación (Android)
    self:setup_notification_channels()

    -- Registrar para push notifications
    self:register_for_push_notifications()
end

function M.request_notification_permission(self, callback)
    print("Requesting notification permission...")

    push.request_permission(function(self, result)
        self.stats.permission_granted = result

        if result then
            print("Notification permission granted")
            self.notification_enabled = true
        else
            print("Notification permission denied")
        end

        if callback then callback(result) end
    end)
end

function M.register_for_push_notifications(self)
    push.register({}, function(self, token, error)
        if token then
            self.push_token = token
            self.stats.push_registered = true

            print("Push token received: " .. token)

            -- Enviar token al servidor
            self:send_token_to_server(token)
        else
            print("Push registration failed: " .. (error or "Unknown error"))
        end
    end)
end

function M.handle_push_message(self, payload, origin, activated)
    print("Push notification received:")
    print("Origin: " .. tostring(origin))
    print("Activated: " .. tostring(activated))

    self.stats.notifications_opened = self.stats.notifications_opened + 1

    -- Procesar payload
    if payload then
        local notification_data = self:parse_notification_payload(payload)
        self:process_notification_action(notification_data, activated)
    end
end

function M.parse_notification_payload(self, payload)
    -- Estructura esperada del payload:
    -- {
    --   "title": "Título",
    --   "body": "Mensaje",
    --   "action": "open_store",
    --   "data": {...}
    -- }

    return {
        title = payload.title or "",
        body = payload.body or payload.alert or "",
        action = payload.action or "default",
        data = payload.data or payload.custom or {},
        badge = tonumber(payload.badge) or 0
    }
end

function M.process_notification_action(self, notification_data, activated)
    if not activated then
        -- Solo registrar si no se activó la app
        return
    end

    local action = notification_data.action

    if action == "open_store" then
        msg.post("main:/ui", "open_store")

    elseif action == "claim_reward" then
        msg.post("main:/rewards", "claim_daily_reward")

    elseif action == "join_event" then
        msg.post("main:/events", "open_event", notification_data.data)

    elseif action == "continue_level" then
        msg.post("main:/game", "continue_level", notification_data.data)

    elseif action == "social_update" then
        msg.post("main:/social", "open_friends")

    else
        -- Acción por defecto: abrir pantalla principal
        msg.post("main:/ui", "show_main_menu")
    end

    -- Trackear apertura
    self:track_notification_opened(notification_data)
end

return M

Notificaciones Locales

1. Local Notification System

Notificaciones Programadas

-- local_notifications.lua
local M = {}

function init(self)
    self.scheduled = {}
    self.templates = {}
    self.next_id = 1

    -- Configurar templates de notificaciones
    self:setup_notification_templates()
end

function M.setup_notification_templates(self)
    self.templates = {
        daily_login = {
            title = "¡Tu recompensa diaria te espera!",
            body = "Entra y reclama tus monedas gratis",
            action = "claim_reward",
            icon = "coin_icon",
            channel = "gameplay"
        },

        energy_full = {
            title = "¡Energía completamente recargada!",
            body = "Tu energía está al máximo. ¡Es hora de jugar!",
            action = "continue_level",
            icon = "energy_icon",
            channel = "gameplay"
        },

        limited_offer = {
            title = "¡Oferta por tiempo limitado!",
            body = "50% de descuento en monedas. ¡Solo por hoy!",
            action = "open_store",
            icon = "offer_icon",
            channel = "promotions"
        },

        friend_request = {
            title = "Nueva solicitud de amistad",
            body = "{{friend_name}} quiere ser tu amigo",
            action = "social_update",
            icon = "friend_icon",
            channel = "social"
        },

        level_complete_reminder = {
            title = "¡Casi terminas el nivel!",
            body = "Regresa y completa el nivel {{level_number}}",
            action = "continue_level",
            icon = "level_icon",
            channel = "gameplay"
        },

        comeback_3days = {
            title = "¡Te extrañamos!",
            body = "Han pasado 3 días. Regresa por tu regalo especial",
            action = "claim_reward",
            icon = "gift_icon",
            channel = "default"
        },

        comeback_7days = {
            title = "¡Recompensa especial!",
            body = "7 días sin jugar = recompensa extra grande",
            action = "claim_reward",
            icon = "big_gift_icon",
            channel = "default"
        }
    }
end

function M.schedule_notification(self, template_id, delay_seconds, data)
    local template = self.templates[template_id]
    if not template then
        print("Unknown notification template: " .. template_id)
        return nil
    end

    local notification_id = self.next_id
    self.next_id = self.next_id + 1

    -- Procesar template con datos
    local notification = self:build_notification(template, data)
    notification.id = notification_id
    notification.scheduled_time = os.time() + delay_seconds

    -- Programar notificación
    push.schedule(notification_id, notification.title, notification.body, delay_seconds, {
        action = notification.action,
        icon = notification.icon,
        badge = 1,
        channel = notification.channel,
        data = data or {}
    })

    -- Guardar referencia
    self.scheduled[notification_id] = {
        template_id = template_id,
        notification = notification,
        data = data
    }

    print(string.format("Scheduled notification %d: %s (in %d seconds)",
          notification_id, template_id, delay_seconds))

    return notification_id
end

function M.build_notification(self, template, data)
    local notification = {
        title = template.title,
        body = template.body,
        action = template.action,
        icon = template.icon,
        channel = template.channel
    }

    -- Reemplazar placeholders con datos
    if data then
        for key, value in pairs(data) do
            local placeholder = "{{" .. key .. "}}"
            notification.title = string.gsub(notification.title, placeholder, tostring(value))
            notification.body = string.gsub(notification.body, placeholder, tostring(value))
        end
    end

    return notification
end

function M.cancel_notification(self, notification_id)
    if self.scheduled[notification_id] then
        push.cancel(notification_id)
        self.scheduled[notification_id] = nil
        print("Cancelled notification: " .. notification_id)
        return true
    end
    return false
end

function M.cancel_all_notifications(self)
    for notification_id, _ in pairs(self.scheduled) do
        push.cancel(notification_id)
    end
    self.scheduled = {}
    print("Cancelled all scheduled notifications")
end

function M.schedule_daily_login_reminder(self)
    -- Cancelar notificación diaria anterior
    self:cancel_notifications_by_template("daily_login")

    -- Programar para mañana a las 10:00 AM
    local tomorrow_10am = self:get_next_10am()
    local delay = tomorrow_10am - os.time()

    return self:schedule_notification("daily_login", delay)
end

function M.schedule_energy_notification(self, current_energy, max_energy, recharge_time)
    if current_energy >= max_energy then
        return nil  -- Energía ya llena
    end

    local time_to_full = (max_energy - current_energy) * recharge_time
    return self:schedule_notification("energy_full", time_to_full)
end

function M.schedule_comeback_notifications(self)
    -- Cancelar notificaciones de comeback anteriores
    self:cancel_notifications_by_template("comeback_3days")
    self:cancel_notifications_by_template("comeback_7days")

    -- Programar para 3 y 7 días
    local three_days = 3 * 24 * 60 * 60
    local seven_days = 7 * 24 * 60 * 60

    local id_3days = self:schedule_notification("comeback_3days", three_days)
    local id_7days = self:schedule_notification("comeback_7days", seven_days)

    return {id_3days, id_7days}
end

function M.cancel_notifications_by_template(self, template_id)
    local to_cancel = {}

    for notification_id, scheduled in pairs(self.scheduled) do
        if scheduled.template_id == template_id then
            table.insert(to_cancel, notification_id)
        end
    end

    for _, notification_id in ipairs(to_cancel) do
        self:cancel_notification(notification_id)
    end
end

function M.get_next_10am(self)
    local current_time = os.time()
    local current_date = os.date("*t", current_time)

    -- Configurar para las 10:00 AM del día siguiente
    local target_date = {
        year = current_date.year,
        month = current_date.month,
        day = current_date.day + 1,
        hour = 10,
        min = 0,
        sec = 0
    }

    return os.time(target_date)
end

return M

2. Smart Scheduling System

Sistema de Programación Inteligente

-- smart_scheduler.lua
local M = {}

function init(self)
    self.user_patterns = {
        typical_play_times = {},  -- Horarios frecuentes de juego
        session_lengths = {},     -- Duración típica de sesiones
        last_activity = 0,        -- Última actividad
        timezone_offset = 0,      -- Offset de zona horaria
        preferred_language = "en" -- Idioma preferido
    }

    self.scheduling_rules = {
        respect_sleep_hours = true,
        sleep_start = 22,  -- 10 PM
        sleep_end = 8,     -- 8 AM
        max_daily_notifications = 3,
        min_interval_hours = 4
    }
end

function M.analyze_user_patterns(self, session_data)
    -- Analizar horarios de juego
    local play_hour = tonumber(os.date("%H", session_data.start_time))
    table.insert(self.user_patterns.typical_play_times, play_hour)

    -- Mantener solo los últimos 30 registros
    if #self.user_patterns.typical_play_times > 30 then
        table.remove(self.user_patterns.typical_play_times, 1)
    end

    -- Analizar duración de sesiones
    local session_length = session_data.end_time - session_data.start_time
    table.insert(self.user_patterns.session_lengths, session_length)

    if #self.user_patterns.session_lengths > 20 then
        table.remove(self.user_patterns.session_lengths, 1)
    end

    self.user_patterns.last_activity = session_data.end_time
end

function M.get_optimal_notification_time(self, base_delay)
    local current_time = os.time()
    local target_time = current_time + base_delay

    -- Ajustar basado en patrones del usuario
    target_time = self:adjust_for_user_patterns(target_time)

    -- Respetar horas de sueño
    target_time = self:adjust_for_sleep_hours(target_time)

    -- Evitar sobrecargar al usuario
    target_time = self:adjust_for_notification_limits(target_time)

    return target_time - current_time
end

function adjust_for_user_patterns(self, target_time)
    if #self.user_patterns.typical_play_times == 0 then
        return target_time  -- No hay datos suficientes
    end

    -- Calcular hora más frecuente de juego
    local hour_counts = {}
    for _, hour in ipairs(self.user_patterns.typical_play_times) do
        hour_counts[hour] = (hour_counts[hour] or 0) + 1
    end

    local most_frequent_hour = 0
    local max_count = 0
    for hour, count in pairs(hour_counts) do
        if count > max_count then
            max_count = count
            most_frequent_hour = hour
        end
    end

    -- Ajustar hacia la hora más frecuente si es razonable
    local target_date = os.date("*t", target_time)
    local current_hour = target_date.hour

    if math.abs(current_hour - most_frequent_hour) > 3 then
        target_date.hour = most_frequent_hour
        target_date.min = math.random(0, 59)  -- Añadir algo de variación
        target_time = os.time(target_date)
    end

    return target_time
end

function adjust_for_sleep_hours(self, target_time)
    if not self.scheduling_rules.respect_sleep_hours then
        return target_time
    end

    local target_date = os.date("*t", target_time)
    local hour = target_date.hour

    -- Si cae en horas de sueño, mover a la mañana siguiente
    if hour >= self.scheduling_rules.sleep_start or hour < self.scheduling_rules.sleep_end then
        target_date.hour = self.scheduling_rules.sleep_end
        target_date.min = math.random(0, 30)  -- Entre 8:00 y 8:30 AM

        -- Si ya pasó las 8 AM hoy, mover a mañana
        if hour < self.scheduling_rules.sleep_end then
            -- Ya es mañana
        else
            -- Mover al día siguiente
            target_date.day = target_date.day + 1
        end

        target_time = os.time(target_date)
    end

    return target_time
end

function adjust_for_notification_limits(self, target_time)
    -- Verificar límite diario de notificaciones
    local notifications_today = self:count_notifications_for_date(target_time)

    if notifications_today >= self.scheduling_rules.max_daily_notifications then
        -- Mover al día siguiente
        local target_date = os.date("*t", target_time)
        target_date.day = target_date.day + 1
        target_date.hour = 9  -- 9 AM del día siguiente
        target_time = os.time(target_date)
    end

    return target_time
end

function count_notifications_for_date(self, timestamp)
    local target_date = os.date("*t", timestamp)
    local day_start = os.time({
        year = target_date.year,
        month = target_date.month,
        day = target_date.day,
        hour = 0, min = 0, sec = 0
    })
    local day_end = day_start + 24 * 60 * 60

    local count = 0
    local local_notifications = require "main.local_notifications"

    for _, scheduled in pairs(local_notifications.scheduled) do
        if scheduled.notification.scheduled_time >= day_start and
           scheduled.notification.scheduled_time < day_end then
            count = count + 1
        end
    end

    return count
end

function M.should_send_notification(self, notification_type)
    -- Verificar si el usuario ha estado inactivo lo suficiente
    local time_since_last_activity = os.time() - self.user_patterns.last_activity

    -- Reglas específicas por tipo de notificación
    if notification_type == "energy_full" then
        return time_since_last_activity > 30 * 60  -- 30 minutos de inactividad

    elseif notification_type == "daily_login" then
        return time_since_last_activity > 20 * 60 * 60  -- 20 horas

    elseif notification_type == "limited_offer" then
        return time_since_last_activity > 2 * 60 * 60  -- 2 horas

    elseif notification_type == "comeback_3days" then
        return time_since_last_activity > 3 * 24 * 60 * 60  -- 3 días

    end

    return true
end

return M

Push Notifications Server

1. Server-Side Implementation

Push Notification Server (Node.js)

// push_server.js
const admin = require('firebase-admin');
const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(express.json());

// Inicializar Firebase Admin
const serviceAccount = require('./firebase-service-account.json');
admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});

// Base de datos de tokens (en producción usar base de datos real)
const userTokens = new Map();
const notificationTemplates = new Map();

// Configurar templates de notificaciones
function setupNotificationTemplates() {
    notificationTemplates.set('daily_reward', {
        title: {
            en: 'Daily Reward Available!',
            es: '¡Recompensa Diaria Disponible!',
            fr: 'Récompense Quotidienne Disponible!'
        },
        body: {
            en: 'Come back and claim your free coins',
            es: 'Regresa y reclama tus monedas gratis',
            fr: 'Revenez et réclamez vos pièces gratuites'
        },
        action: 'claim_reward',
        icon: 'coin_icon'
    });

    notificationTemplates.set('friend_request', {
        title: {
            en: 'New Friend Request',
            es: 'Nueva Solicitud de Amistad',
            fr: 'Nouvelle Demande d\'Ami'
        },
        body: {
            en: '{{friend_name}} wants to be your friend',
            es: '{{friend_name}} quiere ser tu amigo',
            fr: '{{friend_name}} veut être votre ami'
        },
        action: 'social_update',
        icon: 'friend_icon'
    });

    notificationTemplates.set('limited_offer', {
        title: {
            en: 'Limited Time Offer!',
            es: '¡Oferta por Tiempo Limitado!',
            fr: 'Offre à Durée Limitée!'
        },
        body: {
            en: '50% off coins - today only!',
            es: '50% de descuento en monedas - ¡solo hoy!',
            fr: '50% de réduction sur les pièces - aujourd\'hui seulement!'
        },
        action: 'open_store',
        icon: 'offer_icon'
    });
}

// Registrar token de usuario
app.post('/register-token', (req, res) => {
    const { userId, token, platform, language } = req.body;

    if (!userId || !token) {
        return res.status(400).json({ error: 'Missing userId or token' });
    }

    userTokens.set(userId, {
        token: token,
        platform: platform,
        language: language || 'en',
        registered_at: new Date(),
        last_seen: new Date()
    });

    console.log(`Token registered for user ${userId}: ${token}`);
    res.json({ success: true });
});

// Enviar notificación a usuario específico
app.post('/send-notification', async (req, res) => {
    const { userId, templateId, data, customMessage } = req.body;

    try {
        const userToken = userTokens.get(userId);
        if (!userToken) {
            return res.status(404).json({ error: 'User token not found' });
        }

        let notification;

        if (customMessage) {
            notification = customMessage;
        } else {
            const template = notificationTemplates.get(templateId);
            if (!template) {
                return res.status(400).json({ error: 'Template not found' });
            }

            notification = buildNotificationFromTemplate(template, userToken.language, data);
        }

        const message = {
            token: userToken.token,
            notification: {
                title: notification.title,
                body: notification.body,
                image: notification.image
            },
            data: {
                action: notification.action || 'default',
                ...data
            },
            android: {
                notification: {
                    icon: notification.icon,
                    color: '#FF6B35',
                    sound: 'default',
                    channelId: 'default'
                }
            },
            apns: {
                payload: {
                    aps: {
                        badge: 1,
                        sound: 'default'
                    }
                }
            }
        };

        const response = await admin.messaging().send(message);
        console.log('Notification sent successfully:', response);

        res.json({ success: true, messageId: response });

    } catch (error) {
        console.error('Error sending notification:', error);
        res.status(500).json({ error: error.message });
    }
});

// Enviar notificación a múltiples usuarios
app.post('/send-bulk-notification', async (req, res) => {
    const { userIds, templateId, data } = req.body;

    try {
        const messages = [];

        for (const userId of userIds) {
            const userToken = userTokens.get(userId);
            if (!userToken) continue;

            const template = notificationTemplates.get(templateId);
            if (!template) continue;

            const notification = buildNotificationFromTemplate(template, userToken.language, data);

            messages.push({
                token: userToken.token,
                notification: {
                    title: notification.title,
                    body: notification.body
                },
                data: {
                    action: notification.action || 'default',
                    ...data
                }
            });
        }

        if (messages.length === 0) {
            return res.status(400).json({ error: 'No valid tokens found' });
        }

        const response = await admin.messaging().sendAll(messages);
        console.log(`Bulk notification sent to ${response.successCount} users`);

        res.json({
            success: true,
            successCount: response.successCount,
            failureCount: response.failureCount
        });

    } catch (error) {
        console.error('Error sending bulk notification:', error);
        res.status(500).json({ error: error.message });
    }
});

// Programar notificación
app.post('/schedule-notification', (req, res) => {
    const { userId, templateId, data, scheduledTime } = req.body;

    const delay = new Date(scheduledTime) - new Date();

    if (delay <= 0) {
        return res.status(400).json({ error: 'Scheduled time must be in the future' });
    }

    setTimeout(() => {
        // Enviar notificación después del delay
        sendScheduledNotification(userId, templateId, data);
    }, delay);

    res.json({ success: true, scheduledAt: scheduledTime });
});

function buildNotificationFromTemplate(template, language, data) {
    const notification = {
        title: template.title[language] || template.title.en,
        body: template.body[language] || template.body.en,
        action: template.action,
        icon: template.icon
    };

    // Reemplazar placeholders
    if (data) {
        for (const [key, value] of Object.entries(data)) {
            const placeholder = `{{${key}}}`;
            notification.title = notification.title.replace(placeholder, value);
            notification.body = notification.body.replace(placeholder, value);
        }
    }

    return notification;
}

async function sendScheduledNotification(userId, templateId, data) {
    try {
        const userToken = userTokens.get(userId);
        if (!userToken) return;

        const template = notificationTemplates.get(templateId);
        if (!template) return;

        const notification = buildNotificationFromTemplate(template, userToken.language, data);

        const message = {
            token: userToken.token,
            notification: {
                title: notification.title,
                body: notification.body
            },
            data: {
                action: notification.action,
                ...data
            }
        };

        await admin.messaging().send(message);
        console.log('Scheduled notification sent to user:', userId);

    } catch (error) {
        console.error('Error sending scheduled notification:', error);
    }
}

// Inicializar templates y servidor
setupNotificationTemplates();

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Push notification server running on port ${PORT}`);
});

2. Server Communication

Cliente para Comunicación con Servidor

-- push_server_client.lua
local M = {}

function init(self)
    self.server_url = sys.get_config("push.server_url", "")
    self.api_key = sys.get_config("push.api_key", "")
    self.user_id = nil
    self.registered = false
end

function M.register_token(self, user_id, token, callback)
    if not self.server_url or self.server_url == "" then
        print("No push server URL configured")
        if callback then callback(false, "no_server_url") end
        return
    end

    self.user_id = user_id

    local platform = sys.get_sys_info().system_name == "iPhone OS" and "ios" or "android"
    local language = sys.get_sys_info().language

    local data = {
        userId = user_id,
        token = token,
        platform = platform,
        language = language
    }

    local headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer " .. self.api_key
    }

    http.request(self.server_url .. "/register-token", "POST",
        function(self, id, response)
            local success = response.status == 200

            if success then
                self.registered = true
                print("Token registered successfully on server")
            else
                print("Failed to register token on server: " .. response.status)
            end

            if callback then callback(success, response) end
        end,
        headers, json.encode(data))
end

function M.send_user_activity(self, activity_data)
    if not self.registered then return end

    local data = {
        userId = self.user_id,
        activity = activity_data,
        timestamp = os.time()
    }

    local headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer " .. self.api_key
    }

    http.request(self.server_url .. "/user-activity", "POST",
        function(self, id, response)
            if response.status ~= 200 then
                print("Failed to send user activity: " .. response.status)
            end
        end,
        headers, json.encode(data))
end

function M.request_notification(self, template_id, data, callback)
    if not self.registered then
        if callback then callback(false, "not_registered") end
        return
    end

    local request_data = {
        userId = self.user_id,
        templateId = template_id,
        data = data or {}
    }

    local headers = {
        ["Content-Type"] = "application/json",
        ["Authorization"] = "Bearer " .. self.api_key
    }

    http.request(self.server_url .. "/send-notification", "POST",
        function(self, id, response)
            local success = response.status == 200

            if success then
                print("Notification request sent successfully")
            else
                print("Failed to request notification: " .. response.status)
            end

            if callback then callback(success, response) end
        end,
        headers, json.encode(request_data))
end

return M

Engagement Strategy

1. Notification Campaigns

Campaign Manager

-- notification_campaigns.lua
local M = {}

function init(self)
    self.campaigns = {}
    self.user_segments = {}
    self.campaign_stats = {}

    -- Configurar campañas
    self:setup_campaigns()
end

function M.setup_campaigns(self)
    self.campaigns = {
        onboarding = {
            name = "Onboarding Flow",
            target_segment = "new_users",
            notifications = {
                {template = "welcome", delay = 0},
                {template = "tutorial_reminder", delay = 24 * 60 * 60},  -- 1 día
                {template = "first_purchase_offer", delay = 3 * 24 * 60 * 60}  -- 3 días
            }
        },

        retention = {
            name = "Retention Campaign",
            target_segment = "at_risk_users",
            notifications = {
                {template = "comeback_reminder", delay = 24 * 60 * 60},  -- 1 día
                {template = "special_offer", delay = 3 * 24 * 60 * 60},  -- 3 días
                {template = "friend_invite", delay = 7 * 24 * 60 * 60}   -- 7 días
            }
        },

        engagement = {
            name = "Daily Engagement",
            target_segment = "active_users",
            notifications = {
                {template = "daily_reward", delay = 20 * 60 * 60},  -- 20 horas
                {template = "energy_reminder", delay = 6 * 60 * 60},   -- 6 horas
                {template = "limited_event", delay = 12 * 60 * 60}     -- 12 horas
            }
        },

        monetization = {
            name = "Monetization Push",
            target_segment = "potential_payers",
            notifications = {
                {template = "coin_offer", delay = 2 * 60 * 60},     -- 2 horas
                {template = "premium_benefits", delay = 24 * 60 * 60}, -- 1 día
                {template = "limited_bundle", delay = 48 * 60 * 60}    -- 2 días
            }
        }
    }
end

function M.segment_user(self, user_id, user_data)
    local segment = "active_users"  -- Default

    -- Segmentar basado en datos del usuario
    if user_data.days_since_install <= 7 then
        segment = "new_users"
    elseif user_data.days_since_last_play > 3 then
        segment = "at_risk_users"
    elseif user_data.total_purchases > 0 and user_data.days_since_last_purchase <= 30 then
        segment = "paying_users"
    elseif user_data.total_sessions > 10 and user_data.total_purchases == 0 then
        segment = "potential_payers"
    end

    self.user_segments[user_id] = segment
    return segment
end

function M.start_campaign_for_user(self, user_id, campaign_id, user_data)
    local campaign = self.campaigns[campaign_id]
    if not campaign then
        print("Campaign not found: " .. campaign_id)
        return false
    end

    -- Verificar si el usuario pertenece al segmento objetivo
    local user_segment = self:segment_user(user_id, user_data)
    if campaign.target_segment ~= "all" and user_segment ~= campaign.target_segment then
        print(string.format("User %s (segment: %s) doesn't match campaign target: %s",
              user_id, user_segment, campaign.target_segment))
        return false
    end

    -- Programar notificaciones de la campaña
    local scheduled_notifications = {}

    for i, notification in ipairs(campaign.notifications) do
        local notification_id = self:schedule_campaign_notification(
            user_id, campaign_id, notification.template, notification.delay, user_data
        )

        if notification_id then
            table.insert(scheduled_notifications, notification_id)
        end
    end

    -- Trackear inicio de campaña
    self:track_campaign_start(user_id, campaign_id, #scheduled_notifications)

    print(string.format("Started campaign '%s' for user %s with %d notifications",
          campaign.name, user_id, #scheduled_notifications))

    return true
end

function schedule_campaign_notification(self, user_id, campaign_id, template, delay, user_data)
    local smart_scheduler = require "main.smart_scheduler"
    local optimal_delay = smart_scheduler:get_optimal_notification_time(delay)

    local local_notifications = require "main.local_notifications"
    return local_notifications:schedule_notification(template, optimal_delay, user_data)
end

function track_campaign_start(self, user_id, campaign_id, notification_count)
    if not self.campaign_stats[campaign_id] then
        self.campaign_stats[campaign_id] = {
            started = 0,
            completed = 0,
            notifications_sent = 0,
            notifications_opened = 0
        }
    end

    local stats = self.campaign_stats[campaign_id]
    stats.started = stats.started + 1
    stats.notifications_sent = stats.notifications_sent + notification_count

    -- Enviar a analytics
    msg.post("main:/analytics", "track_event", {
        event = "campaign_started",
        properties = {
            campaign_id = campaign_id,
            user_id = user_id,
            notification_count = notification_count
        }
    })
end

function M.track_campaign_notification_opened(self, campaign_id, template_id)
    if self.campaign_stats[campaign_id] then
        self.campaign_stats[campaign_id].notifications_opened =
            self.campaign_stats[campaign_id].notifications_opened + 1
    end

    msg.post("main:/analytics", "track_event", {
        event = "campaign_notification_opened",
        properties = {
            campaign_id = campaign_id,
            template_id = template_id
        }
    })
end

function M.get_campaign_performance(self, campaign_id)
    local stats = self.campaign_stats[campaign_id]
    if not stats then return nil end

    return {
        open_rate = stats.notifications_opened / math.max(1, stats.notifications_sent),
        completion_rate = stats.completed / math.max(1, stats.started),
        total_started = stats.started,
        total_notifications = stats.notifications_sent
    }
end

return M

Analytics y Optimización

1. Notification Analytics

Sistema de Métricas

-- notification_analytics.lua
local M = {}

function init(self)
    self.metrics = {
        sent = 0,
        delivered = 0,
        opened = 0,
        clicked = 0,
        opt_outs = 0
    }

    self.template_metrics = {}
    self.time_metrics = {}
    self.segment_metrics = {}
end

function M.track_notification_sent(self, template_id, user_segment)
    self.metrics.sent = self.metrics.sent + 1

    -- Trackear por template
    if not self.template_metrics[template_id] then
        self.template_metrics[template_id] = {sent = 0, opened = 0, clicked = 0}
    end
    self.template_metrics[template_id].sent = self.template_metrics[template_id].sent + 1

    -- Trackear por segmento
    if not self.segment_metrics[user_segment] then
        self.segment_metrics[user_segment] = {sent = 0, opened = 0, clicked = 0}
    end
    self.segment_metrics[user_segment].sent = self.segment_metrics[user_segment].sent + 1

    -- Trackear por hora del día
    local hour = tonumber(os.date("%H"))
    if not self.time_metrics[hour] then
        self.time_metrics[hour] = {sent = 0, opened = 0}
    end
    self.time_metrics[hour].sent = self.time_metrics[hour].sent + 1

    -- Enviar a analytics externos
    self:send_analytics_event("notification_sent", {
        template_id = template_id,
        user_segment = user_segment,
        hour = hour
    })
end

function M.track_notification_opened(self, template_id, user_segment, activated)
    self.metrics.opened = self.metrics.opened + 1

    if activated then
        self.metrics.clicked = self.metrics.clicked + 1
    end

    -- Actualizar métricas por template
    if self.template_metrics[template_id] then
        self.template_metrics[template_id].opened = self.template_metrics[template_id].opened + 1
        if activated then
            self.template_metrics[template_id].clicked = self.template_metrics[template_id].clicked + 1
        end
    end

    -- Actualizar métricas por segmento
    if self.segment_metrics[user_segment] then
        self.segment_metrics[user_segment].opened = self.segment_metrics[user_segment].opened + 1
        if activated then
            self.segment_metrics[user_segment].clicked = self.segment_metrics[user_segment].clicked + 1
        end
    end

    -- Actualizar métricas por hora
    local hour = tonumber(os.date("%H"))
    if self.time_metrics[hour] then
        self.time_metrics[hour].opened = self.time_metrics[hour].opened + 1
    end

    self:send_analytics_event("notification_opened", {
        template_id = template_id,
        user_segment = user_segment,
        activated = activated,
        hour = hour
    })
end

function M.get_performance_report(self)
    return {
        overall = {
            open_rate = self.metrics.opened / math.max(1, self.metrics.sent),
            click_rate = self.metrics.clicked / math.max(1, self.metrics.sent),
            ctr = self.metrics.clicked / math.max(1, self.metrics.opened),
            opt_out_rate = self.metrics.opt_outs / math.max(1, self.metrics.sent)
        },
        by_template = self:calculate_template_performance(),
        by_segment = self:calculate_segment_performance(),
        by_hour = self:calculate_time_performance()
    }
end

function calculate_template_performance(self)
    local performance = {}

    for template_id, metrics in pairs(self.template_metrics) do
        performance[template_id] = {
            open_rate = metrics.opened / math.max(1, metrics.sent),
            click_rate = metrics.clicked / math.max(1, metrics.sent),
            total_sent = metrics.sent
        }
    end

    return performance
end

function calculate_segment_performance(self)
    local performance = {}

    for segment, metrics in pairs(self.segment_metrics) do
        performance[segment] = {
            open_rate = metrics.opened / math.max(1, metrics.sent),
            click_rate = metrics.clicked / math.max(1, metrics.sent),
            total_sent = metrics.sent
        }
    end

    return performance
end

function calculate_time_performance(self)
    local performance = {}

    for hour, metrics in pairs(self.time_metrics) do
        performance[hour] = {
            open_rate = metrics.opened / math.max(1, metrics.sent),
            total_sent = metrics.sent
        }
    end

    return performance
end

function send_analytics_event(self, event_name, properties)
    msg.post("main:/analytics", "track_event", {
        event = event_name,
        properties = properties
    })
end

return M

Mejores Prácticas

1. User Experience

2. Technical

3. Content Strategy

4. Compliance

Esta implementación completa te proporciona un sistema sofisticado de notificaciones que maximiza el engagement mientras respeta la experiencia del usuario.