"use strict"; /** * SenaGame SDK Loader * * Ready-to-use interface for game developers. * Handles SDK iframe creation, communication, and lifecycle. * * Usage: * ```html * * * ``` * * @version 1.0.0 */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SenaGameSDK = void 0; // ======================================== // MESSAGE TYPES (matching SDK) // ======================================== const MSG = { // Outgoing (Game → SDK) INIT: 'SDK_INIT', PUSH_DATA: 'SDK_PUSH_DATA', SUBMIT_ANSWER: 'SDK_CHECK_ANSWER', COMPLETE_GAME: 'SDK_COMPLETE_GAME', GET_STATUS: 'SDK_GET_STATUS', // Incoming (SDK → Game) READY: 'SDK_READY', DATA_READY: 'SDK_DATA_READY', ANSWER_RESULT: 'SDK_ANSWER_RESULT', GAME_COMPLETE: 'SDK_GAME_COMPLETE', SESSION_STARTED: 'SDK_SESSION_STARTED', STATUS: 'SDK_STATUS', ERROR: 'SDK_ERROR', }; // ======================================== // DEFAULT CONFIG // ======================================== const DEFAULT_CONFIG = { iframePath: '../sdk-iframe/index.html', mode: 'preview', gameCode: '', debug: false, timeout: 10000, iframeStyle: 'position:fixed;width:1px;height:1px;left:-9999px;border:none;', }; // ======================================== // SENA GAME SDK CLASS // ======================================== class SenaGameSDK { constructor(config) { this.iframe = null; this.isReady = false; this.isDataReady = false; this.pendingMessages = []; this.initResolver = null; this.timeoutId = null; this.config = { ...DEFAULT_CONFIG, ...config }; // --- CLEANUP OLD LISTENERS --- // Tránh trường hợp init SDK nhiều lần bị trùng listener cũ const oldSDK = window._sena_game_sdk_instance; if (oldSDK && typeof oldSDK.destroy === 'function') { oldSDK.destroy(); } window._sena_game_sdk_instance = this; // Create promise for ready state this.initPromise = new Promise((resolve, reject) => { this.initResolver = { resolve, reject }; }); // Bind methods this._handleMessage = this._handleMessage.bind(this); // Auto-initialize this._init(); } // ---------------------------------------- // PUBLIC API // ---------------------------------------- /** * Push game data to SDK */ pushData(data) { // Safe extraction of the array let itemsArray = []; if (Array.isArray(data)) { itemsArray = data; } else if (data && Array.isArray(data.items)) { itemsArray = data.items; } else if (data && Array.isArray(data.data)) { itemsArray = data.data; } else if (data && data.items && typeof data.items === 'object') { itemsArray = [data.items]; } else if (data && data.data && typeof data.data === 'object') { itemsArray = [data.data]; } // Transform to SDK iframe internal format const payload = { data: itemsArray, completed_question_ids: (data && data.completed_question_ids) || [] }; this._send(MSG.PUSH_DATA, payload); } /** * Submit an answer */ submitAnswer(answer) { // Transform to SDK iframe format const payload = { question_id: answer.questionId, choice: answer.selectedAnswer, time_spent: answer.timeSpent ?? 0 }; this._send(MSG.SUBMIT_ANSWER, payload); } /** * Complete the game */ completeGame() { this._send(MSG.COMPLETE_GAME, {}); } /** * Get current status */ getStatus() { this._send(MSG.GET_STATUS, {}); } /** * Wait for SDK to be ready */ async ready() { if (this.isReady) return this; return this.initPromise; } /** * Check if SDK is ready */ get sdkReady() { return this.isReady; } /** * Check if data is ready */ get dataReady() { return this.isDataReady; } /** * Destroy the SDK instance */ destroy() { window.removeEventListener('message', this._handleMessage); if (this.timeoutId) { clearTimeout(this.timeoutId); } if (this.iframe && this.iframe.parentNode) { this.iframe.parentNode.removeChild(this.iframe); } window.removeEventListener('message', this._handleMessage); this.isReady = false; this._log('SDK destroyed'); } // ---------------------------------------- // PRIVATE METHODS // ---------------------------------------- _init() { this._log('Initializing SenaGameSDK...'); // Setup message listener window.addEventListener('message', this._handleMessage); // Create iframe this._createIframe(); // Setup timeout this.timeoutId = setTimeout(() => { if (!this.isReady) { const error = new Error('SDK initialization timeout'); this._error(error); if (this.initResolver) { this.initResolver.reject(error); } } }, this.config.timeout); } _createIframe() { this.iframe = document.createElement('iframe'); this.iframe.id = 'sena-game-sdk-iframe'; this.iframe.src = this.config.iframePath; this.iframe.style.cssText = this.config.iframeStyle || ''; this.iframe.onload = () => { this._log('Iframe loaded, sending INIT...'); setTimeout(() => { this._send(MSG.INIT, { mode: this.config.mode, game_code: this.config.gameCode, }); }, 100); }; this.iframe.onerror = () => { this._error(new Error('Failed to load SDK iframe')); }; document.body.appendChild(this.iframe); this._log(`Iframe created: ${this.config.iframePath}`); } _send(type, payload) { if (!this.iframe || !this.iframe.contentWindow) { this._log(`Queuing message: ${type}`, 'warn'); this.pendingMessages.push({ type, payload }); return; } const message = { type, payload, timestamp: Date.now() }; this._log(`→ ${type}`, 'send'); this.iframe.contentWindow.postMessage(message, '*'); } _handleMessage(event) { const data = event.data; if (!data || !data.type) return; // Only process SDK messages if (!data.type.startsWith('SDK_')) return; this._log(`← ${data.type}`, 'recv'); switch (data.type) { case MSG.READY: this._onSDKReady(); break; case MSG.DATA_READY: this._onDataReady(data.payload); break; case MSG.ANSWER_RESULT: this._onAnswerResult(data.payload); break; case MSG.GAME_COMPLETE: this._onGameComplete(data.payload); break; case MSG.SESSION_STARTED: this._onSessionStart(data.payload); break; case MSG.ERROR: this._error(new Error(data.payload?.message || 'SDK Error')); break; } } _onSDKReady() { this.isReady = true; if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = null; } // Flush pending messages while (this.pendingMessages.length > 0) { const msg = this.pendingMessages.shift(); if (msg) { this._send(msg.type, msg.payload); } } // Callback if (this.config.onReady) { this.config.onReady(this); } // Resolve promise if (this.initResolver) { this.initResolver.resolve(this); } this._log('✅ SDK Ready!'); } _onDataReady(payload) { this.isDataReady = true; // Normalize payload to camelCase const normalized = { items: payload.items || [], totalQuestions: payload.total_questions || payload.totalQuestions || 0, completedCount: payload.completed_count || payload.completedCount || 0, resumeData: payload.resume_data || payload.resumeData || [] }; if (this.config.onDataReady) { this.config.onDataReady(normalized); } this._log(`✅ Data Ready: ${normalized.items.length} items`); } _onAnswerResult(payload) { const normalized = { questionId: payload.question_id || payload.questionId, isCorrect: payload.correct !== undefined ? payload.correct : payload.isCorrect, correctAnswer: payload.correct_answer || payload.correctAnswer || '', score: payload.score || 0, currentScore: payload.current_score || payload.currentScore || 0, totalAnswered: payload.total_answered || payload.totalAnswered || 0 }; if (this.config.onAnswerResult) { this.config.onAnswerResult(normalized); } } _onGameComplete(payload) { const normalized = { success: payload.success !== undefined ? payload.success : true, finalScore: payload.score !== undefined ? payload.score : (payload.finalScore || 0), correctCount: payload.correct !== undefined ? payload.correct : (payload.correctCount || 0), totalQuestions: payload.total !== undefined ? payload.total : (payload.totalQuestions || 0), wrongCount: payload.wrong !== undefined ? payload.wrong : (payload.wrongCount || 0), total: payload.total || 0 }; if (this.config.onGameComplete) { this.config.onGameComplete(normalized); } } _onSessionStart(payload) { const normalized = { assignmentId: payload.assignment_id || payload.assignmentId, userId: payload.student_id || payload.userId, gameId: payload.game_code || payload.gameId, startedAt: payload.started_at || payload.startedAt || new Date().toISOString() }; if (this.config.onSessionStart) { this.config.onSessionStart(normalized); } } _error(error) { this._log(`❌ Error: ${error.message}`, 'error'); if (this.config.onError) { this.config.onError(error); } } _log(message, type = 'info') { if (!this.config.debug) return; const prefix = '[SenaGameSDK]'; const styles = { info: 'color: #888', send: 'color: #ff0', recv: 'color: #0f0', warn: 'color: #fa0', error: 'color: #f00', }; console.log(`%c${prefix} ${message}`, styles[type] || styles.info); } } exports.SenaGameSDK = SenaGameSDK; /** SDK version */ SenaGameSDK.VERSION = '1.0.0'; // ======================================== // EXPORT FOR BROWSER (UMD) // ======================================== if (typeof window !== 'undefined') { window.SenaGameSDK = SenaGameSDK; } exports.default = SenaGameSDK; //# sourceMappingURL=SenaGameSDK.js.map