SDK Message Protocol
Game giao tiếp với SDK thông qua postMessage trong hidden iframe. Game chỉ cần biết các message types, payloads, và khi nào gửi/nhận.
🔌 Architecture
┌──────────────────────────────┐
│ Game (React/Vue/etc) │
│ │
│ window.parent.postMessage() │
│ window.addEventListener() │
└──────────────┬───────────────┘
│ postMessage
│ (JSON)
↓
┌──────────────────────┐
│ Hidden Iframe │
│ (sdk-iframe.html) │
│ │
│ - Sanitize data │
│ - Verify answers │
│ - Call API │
│ - Send responses │
└──────────────────────┘
📨 Message Types
1️⃣ SDK_INIT (Game → SDK)
Game khởi tạo SDK với mode và game_code.
Gửi:
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'dev' | 'preview' | 'live',
game_code: 'G001' | 'G002' | ... | 'G123',
// LIVE mode only:
assignment_id?: string,
student_id?: string,
api_base_url?: string,
auth_token?: string
}
}, '*');
Ví dụ DEV mode:
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'dev',
game_code: 'G001'
}
}, '*');
Ví dụ LIVE mode:
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode: 'live',
game_code: 'G001',
assignment_id: 'ASSIGN_123',
student_id: 'STU_456',
api_base_url: 'https://api.sena.tech',
auth_token: 'token_xyz'
}
}, '*');
Nhận (SDK gửi lại khi ready):
{
type: 'SDK_READY',
payload: {
mode: 'dev' | 'preview' | 'live',
game_code: 'G001'
}
}
2️⃣ SDK_DATA_READY (SDK → Game)
SDK gửi sanitized data cho game render. Game phải listen sự kiện này.
Nhận:
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_DATA_READY') {
const {
items, // Sanitized items (NO answers!)
total_questions,
completed_count,
resume_data // Optional: previous results
} = event.data.payload;
// Render game
renderGame(items);
}
});
Payload items (tùy game_code):
Quiz Games (G001-G004):
{
"id": "q1",
"question": "What is 2+2?",
"options": [
{"text": "5"},
{"text": "4"},
{"text": "3"}
]
}
Sequence Games (G110-G123):
{
"id": "seq1",
"question": ["H", "", "L", "", "O"],
"options": ["L", "E"],
"audio_url": "https://..." // optional
}
3️⃣ SDK_CHECK_ANSWER (Game → SDK)
Game gửi user's answer để SDK verify.
Gửi:
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'q1',
choice: any, // Index (quiz) hoặc Array (sequence)
time_spent?: number // Milliseconds
}
}, '*');
Quiz example (choice = index):
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'q1',
choice: 1, // User clicked option index 1
time_spent: 5000
}
}, '*');
Sequence example (choice = reordered array):
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: 'seq1',
choice: ["H", "e", "l", "l", "o"], // Reordered
time_spent: 8000
}
}, '*');
4️⃣ SDK_ANSWER_RESULT (SDK → Game)
SDK gửi kết quả verify.
Nhận:
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_ANSWER_RESULT') {
const {
question_id,
correct, // true/false
score, // 0-1 hoặc custom
synced, // true = already synced to server
feedback // Optional: "✅ Correct!" or "❌ Wrong"
} = event.data.payload;
if (correct) {
showCorrectFeedback(question_id);
} else {
showWrongFeedback(question_id);
}
}
});
5️⃣ SDK_PUSH_DATA (Game → SDK, PREVIEW only)
Nếu PREVIEW mode, game có thể push data thay vì SDK fetch.
Gửi:
window.parent.postMessage({
type: 'SDK_PUSH_DATA',
payload: {
items: [
{
id: 'q1',
question: 'What is 2+2?',
options: [
{text: '3'},
{text: '4'},
{text: '5'}
],
answer: '4' // Server data (with answer)
},
// ... more items
]
}
}, '*');
SDK sẽ sanitize (remove answer, shuffle options) rồi gửi SDK_DATA_READY.
6️⃣ SDK_ERROR (SDK → Game)
SDK gửi error notification.
Nhận:
window.addEventListener('message', (event) => {
if (event.data.type === 'SDK_ERROR') {
const {
code, // Error code
message, // Error message
details // Optional: more info
} = event.data.payload;
console.error(`[SDK Error] ${code}: ${message}`);
}
});
🎮 Data Structures by Game Type
Quiz Games (G001-G004)
Sanitized (Game receives):
{
"id": "q1",
"question": "Audio URL or text",
"image_url": "Image URL (optional)",
"options": [
{"text": "Option A"} or {"audio": "Audio URL"}
]
}
User Answer Format:
choice = 0 // Index of selected option
Sequence Word (G110-G113)
Sanitized (Game receives):
{
"id": "seq_word_1",
"question": ["H", "", "L", "", "O"],
"options": ["L", "E"],
"audio_url": "URL (optional)"
}
User Answer Format:
choice = ["H", "e", "l", "l", "o"] // Reordered array
Sequence Sentence (G120-G123)
Sanitized (Game receives):
{
"id": "seq_sent_1",
"question": ["I", "", "reading", ""],
"options": ["love", "books"],
"audio_url": "URL (optional)"
}
User Answer Format:
choice = ["I", "love", "reading", "books"] // Reordered
💻 Complete Game Implementation Example
// ============ INITIALIZE ============
// Listen for SDK messages
window.addEventListener('message', (event) => {
handleSdkMessage(event.data);
});
// Initialize SDK
function initGame() {
const mode = 'live';
const gameCode = 'G001';
window.parent.postMessage({
type: 'SDK_INIT',
payload: {
mode,
game_code: gameCode,
assignment_id: 'ASSIGN_123',
student_id: 'STU_456',
api_base_url: 'https://api.sena.tech'
}
}, '*');
}
// Handle all SDK messages
function handleSdkMessage(data) {
switch (data.type) {
case 'SDK_READY':
console.log('SDK initialized:', data.payload);
break;
case 'SDK_DATA_READY':
const items = data.payload.items;
renderGameItems(items);
break;
case 'SDK_ANSWER_RESULT':
const result = data.payload;
if (result.correct) {
showCorrectFeedback(result.question_id);
} else {
showWrongFeedback(result.question_id);
}
break;
case 'SDK_ERROR':
console.error('SDK Error:', data.payload);
break;
}
}
// ============ RENDER GAME ============
function renderGameItems(items) {
items.forEach(item => {
if (item.options) {
// Quiz game
renderQuizQuestion(item);
} else if (Array.isArray(item.question)) {
// Sequence game
renderSequenceGame(item);
}
});
}
// ============ HANDLE USER ANSWER ============
function submitAnswer(questionId, userChoice) {
window.parent.postMessage({
type: 'SDK_CHECK_ANSWER',
payload: {
question_id: questionId,
choice: userChoice, // Index for quiz, array for sequence
time_spent: 5000
}
}, '*');
}
// ============ GAME START ============
// Start when page loads
window.addEventListener('load', () => {
initGame();
});
🔄 Message Flow Examples
DEV Mode (Local Verify)
Game: SDK_INIT
↓
SDK: SDK_READY
↓
SDK: SDK_DATA_READY (mock items)
↓
Game: SDK_CHECK_ANSWER
↓
SDK: SDK_ANSWER_RESULT (instantly)
LIVE Mode (Server Verify)
Game: SDK_INIT
↓
SDK: SDK_READY
↓
SDK: fetch from API → SDK_DATA_READY
↓
Game: SDK_CHECK_ANSWER
↓
SDK: POST to server
↓
SDK: SDK_ANSWER_RESULT (wait for server)
PREVIEW Mode (Push Data)
Game: SDK_INIT (mode='preview')
↓
SDK: SDK_READY
↓
Game: SDK_PUSH_DATA (items with answers)
↓
SDK: sanitize → SDK_DATA_READY
↓
Game: SDK_CHECK_ANSWER
↓
SDK: SDK_ANSWER_RESULT (local verify)
⚠️ Important Notes
✅ SDK Always Sanitizes
- Removes
answerfield - Removes
word,sentence,parts,missing_letter_count - Shuffles options (for quiz)
✅ Game Never Gets Answer
- Game receives only question + options
- Answer is stored server-side
✅ Shuffled Options
- Game receives shuffled options array
- Game user clicks index
- SDK internally resolves index → text
✅ Sequence Games
- Random positions are blanked (based on
missing_letter_count) - Game receives question with blanks + missing items
- User reorders missing items
🚀 Testing
Test locally without SDK:
// Simulate SDK messages for testing
function simulateSdkMessage(type, payload) {
const event = new MessageEvent('message', {
data: { type, payload }
});
window.dispatchEvent(event);
}
// Simulate SDK_DATA_READY
simulateSdkMessage('SDK_DATA_READY', {
items: [
{
id: 'q1',
question: 'What is 2+2?',
options: [{text: '4'}, {text: '5'}, {text: '3'}]
}
]
});