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

  • Solicitar permisos en momento apropiado
  • Personalizar mensajes según comportamiento del usuario
  • Respetar horas de sueño y preferencias
  • Ofrecer control granular de notificaciones
  • No sobrecargar al usuario

2. Technical

  • Validar tokens regularmente
  • Manejar errores de envío gracefully
  • Implementar retry logic
  • Usar analytics para optimizar
  • Testear en múltiples dispositivos

3. Content Strategy

  • Crear mensajes valiosos y relevantes
  • Usar localización apropiada
  • A/B test diferentes mensajes
  • Optimizar timing basado en datos
  • Segmentar usuarios efectivamente

4. Compliance

  • Seguir políticas de cada plataforma
  • Implementar opt-out fácil
  • Respetar GDPR y regulaciones locales
  • Documentar consent del usuario
  • Mantener data privacy

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