Commit 55a27180 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: guest mode, Google sign-in, Play Store compliance

- Guest mode: play vs bots without account, locked multiplayer/social tabs
- Google OAuth sign-in/sign-up via Supabase with auto-profile creation
- Delete account flow (password verify for email, confirm for OAuth)
- Privacy policy + Terms of Service pages (bilingual AR/EN)
- PWA manifest with app icons
- Age confirmation checkbox on registration
- Legal links in login, register, and settings
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent db7cf8ef
......@@ -3,6 +3,10 @@ RewriteEngine On
# Pass Authorization header to PHP
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
# Legal pages
RewriteRule ^privacy-policy$ privacy-policy.php [L]
RewriteRule ^terms$ terms.php [L]
# Serve existing files/dirs directly
RewriteCond %{REQUEST_URI} ^/public/ [OR]
RewriteCond %{REQUEST_URI} ^/api/
......
......@@ -26,6 +26,12 @@ switch ($action) {
case 'logout':
handleLogout();
break;
case 'me':
handleMe();
break;
case 'delete-account':
handleDeleteAccount($input);
break;
default:
jsonError('Invalid action', 400);
}
......@@ -124,3 +130,69 @@ function handleLogout(): void {
}
jsonResponse(['success' => true]);
}
function handleMe(): void {
$token = requireAuth();
$user = verifyToken($token);
if (!$user) {
jsonError('Invalid token', 401);
}
jsonResponse([
'id' => $user['id'],
'email' => $user['email'] ?? null,
'provider' => $user['app_metadata']['provider'] ?? 'email',
'created_at' => $user['created_at'] ?? null
]);
}
function handleDeleteAccount(array $input): void {
$token = requireAuth();
$user = verifyToken($token);
if (!$user) {
jsonError('Invalid token', 401);
}
$userId = $user['id'];
$provider = $user['app_metadata']['provider'] ?? 'email';
if ($provider === 'email') {
$password = $input['password'] ?? '';
if (!$password) {
jsonError('Password required to delete account');
}
$verify = supabaseAuth('POST', 'token?grant_type=password', [
'email' => $user['email'],
'password' => $password
]);
if (isset($verify['error'])) {
jsonError('Incorrect password', 403);
}
} else {
if (empty($input['confirm'])) {
jsonError('Confirmation required to delete account');
}
}
$sdb = supabaseService();
$sdb->delete('profiles', ['id' => 'eq.' . $userId]);
$url = SUPABASE_AUTH . '/admin/users/' . $userId;
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'apikey: ' . SUPABASE_SERVICE_KEY,
'Authorization: Bearer ' . SUPABASE_SERVICE_KEY,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode >= 400) {
jsonError('Failed to delete account', 500);
}
jsonResponse(['success' => true]);
}
......@@ -9,6 +9,7 @@
<title>EL3AB</title>
<link rel="icon" type="image/png" href="https://safe-supabase-kong.caprover.al-arcade.com/storage/v1/object/public/profile-images/branding/favicon.png?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84">
<link rel="apple-touch-icon" href="https://safe-supabase-kong.caprover.al-arcade.com/storage/v1/object/public/profile-images/branding/logo_icon.png?apikey=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84">
<link rel="manifest" href="/manifest.json">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Arabic:wght@400;500;600;700;800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
......
{
"name": "EL3AB - العب",
"short_name": "EL3AB",
"description": "العب شطرنج، دومينو، لودو وطاولة مع أصدقائك",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"theme_color": "#050810",
"background_color": "#050810",
"lang": "ar",
"dir": "rtl",
"categories": ["games", "entertainment"],
"icons": [
{
"src": "/public/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/public/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
]
}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EL3AB - سياسة الخصوصية | Privacy Policy</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'IBM Plex Sans Arabic', -apple-system, BlinkMacSystemFont, sans-serif;
background: #050810;
color: #e0e0e0;
line-height: 1.8;
padding: 24px 16px 64px;
}
.container { max-width: 720px; margin: 0 auto; }
.logo { text-align: center; margin-bottom: 32px; }
.logo span { font-size: 32px; font-weight: 900; color: #E4AC38; font-family: 'Inter', sans-serif; }
h1 { font-size: 24px; color: #fff; margin-bottom: 8px; text-align: center; }
.subtitle { text-align: center; color: #888; font-size: 14px; margin-bottom: 40px; }
h2 { font-size: 18px; color: #E4AC38; margin: 32px 0 12px; }
p, li { font-size: 15px; color: #ccc; margin-bottom: 12px; }
ul { padding-right: 24px; padding-left: 24px; }
li { margin-bottom: 8px; }
.section { background: #0d1117; border-radius: 12px; padding: 24px; margin-bottom: 20px; border: 1px solid #1a2030; }
.lang-switch { text-align: center; margin-bottom: 32px; }
.lang-switch a { color: #E4AC38; text-decoration: none; font-weight: 600; padding: 8px 16px; border: 1px solid #E4AC38; border-radius: 8px; font-size: 14px; }
.en { direction: ltr; text-align: left; }
a { color: #E4AC38; }
.date { text-align: center; color: #666; font-size: 13px; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="logo"><span>EL3AB</span></div>
<h1>سياسة الخصوصية</h1>
<p class="subtitle">Privacy Policy</p>
<div class="lang-switch"><a href="#en">English Version Below</a></div>
<!-- Arabic -->
<div class="section">
<h2>١. المقدمة</h2>
<p>مرحباً بك في EL3AB ("التطبيق"). نحن نحترم خصوصيتك ونلتزم بحماية بياناتك الشخصية. توضح هذه السياسة كيف نجمع ونستخدم ونحمي معلوماتك عند استخدامك للتطبيق.</p>
</div>
<div class="section">
<h2>٢. البيانات التي نجمعها</h2>
<ul>
<li><strong>بيانات الحساب:</strong> البريد الإلكتروني، اسم المستخدم، كلمة المرور (مشفرة)</li>
<li><strong>بيانات الملف الشخصي:</strong> الصورة الشخصية (اختياري)، المستوى، النقاط</li>
<li><strong>بيانات اللعب:</strong> نتائج المباريات، التصنيف، الإحصائيات</li>
<li><strong>بيانات تقنية:</strong> عنوان IP، نوع المتصفح، نظام التشغيل</li>
<li><strong>بيانات تسجيل الدخول بـ Google:</strong> الاسم والبريد الإلكتروني من حساب Google (عند استخدام تسجيل الدخول بـ Google)</li>
</ul>
</div>
<div class="section">
<h2>٣. كيف نستخدم بياناتك</h2>
<ul>
<li>إدارة حسابك والتحقق من هويتك</li>
<li>تشغيل نظام المطابقة والتصنيف</li>
<li>عرض لوحات المتصدرين والإحصائيات</li>
<li>إرسال إشعارات متعلقة باللعب (تحديات، دعوات)</li>
<li>تحسين تجربة المستخدم وأداء التطبيق</li>
</ul>
</div>
<div class="section">
<h2>٤. الأطراف الثالثة</h2>
<p>نستخدم الخدمات التالية:</p>
<ul>
<li><strong>Supabase:</strong> لتخزين البيانات والمصادقة</li>
<li><strong>Google Sign-In:</strong> لتسجيل الدخول (اختياري)</li>
</ul>
<p>لا نبيع أو نشارك بياناتك الشخصية مع أطراف ثالثة لأغراض تسويقية.</p>
</div>
<div class="section">
<h2>٥. الاحتفاظ بالبيانات</h2>
<p>نحتفظ ببياناتك طوال فترة وجود حسابك. عند حذف حسابك، يتم حذف جميع بياناتك نهائياً خلال ٣٠ يوماً.</p>
</div>
<div class="section">
<h2>٦. حقوقك</h2>
<ul>
<li>الوصول إلى بياناتك الشخصية</li>
<li>تعديل أو تحديث معلوماتك</li>
<li>حذف حسابك وجميع بياناتك المرتبطة</li>
<li>سحب موافقتك في أي وقت</li>
</ul>
<p>يمكنك حذف حسابك من إعدادات التطبيق في أي وقت.</p>
</div>
<div class="section">
<h2>٧. أمان البيانات</h2>
<p>نستخدم تقنيات تشفير وحماية قياسية لحماية بياناتك. جميع الاتصالات مشفرة عبر HTTPS.</p>
</div>
<div class="section">
<h2>٨. الأطفال</h2>
<p>هذا التطبيق مخصص للمستخدمين بعمر ١٣ سنة فأكثر. لا نجمع بيانات من أطفال دون هذا السن عن علم.</p>
</div>
<div class="section">
<h2>٩. التواصل</h2>
<p>لأي استفسارات حول الخصوصية، تواصل معنا عبر: <a href="mailto:support@al-arcade.com">support@al-arcade.com</a></p>
</div>
<!-- English -->
<div id="en" style="margin-top:64px;"></div>
<h1 class="en" style="color:#fff;">Privacy Policy</h1>
<p class="subtitle">سياسة الخصوصية</p>
<div class="section en">
<h2>1. Introduction</h2>
<p>Welcome to EL3AB ("the App"). We respect your privacy and are committed to protecting your personal data. This policy explains how we collect, use, and protect your information when you use the App.</p>
</div>
<div class="section en">
<h2>2. Data We Collect</h2>
<ul>
<li><strong>Account data:</strong> Email address, username, password (encrypted)</li>
<li><strong>Profile data:</strong> Avatar (optional), level, points</li>
<li><strong>Gameplay data:</strong> Match results, rankings, statistics</li>
<li><strong>Technical data:</strong> IP address, browser type, operating system</li>
<li><strong>Google Sign-In data:</strong> Name and email from your Google account (when using Google Sign-In)</li>
</ul>
</div>
<div class="section en">
<h2>3. How We Use Your Data</h2>
<ul>
<li>Managing your account and verifying your identity</li>
<li>Operating matchmaking and ranking systems</li>
<li>Displaying leaderboards and statistics</li>
<li>Sending gameplay-related notifications (challenges, invitations)</li>
<li>Improving user experience and app performance</li>
</ul>
</div>
<div class="section en">
<h2>4. Third Parties</h2>
<p>We use the following services:</p>
<ul>
<li><strong>Supabase:</strong> For data storage and authentication</li>
<li><strong>Google Sign-In:</strong> For login (optional)</li>
</ul>
<p>We do not sell or share your personal data with third parties for marketing purposes.</p>
</div>
<div class="section en">
<h2>5. Data Retention</h2>
<p>We retain your data for as long as your account exists. When you delete your account, all your data is permanently deleted within 30 days.</p>
</div>
<div class="section en">
<h2>6. Your Rights</h2>
<ul>
<li>Access your personal data</li>
<li>Modify or update your information</li>
<li>Delete your account and all associated data</li>
<li>Withdraw your consent at any time</li>
</ul>
<p>You can delete your account from the app settings at any time.</p>
</div>
<div class="section en">
<h2>7. Data Security</h2>
<p>We use industry-standard encryption and security measures to protect your data. All communications are encrypted via HTTPS.</p>
</div>
<div class="section en">
<h2>8. Children</h2>
<p>This app is intended for users aged 13 and older. We do not knowingly collect data from children under this age.</p>
</div>
<div class="section en">
<h2>9. Contact</h2>
<p>For any privacy inquiries, contact us at: <a href="mailto:support@al-arcade.com">support@al-arcade.com</a></p>
</div>
<p class="date">آخر تحديث: يونيو ٢٠٢٦ | Last updated: June 2026</p>
</div>
</body>
</html>
......@@ -5,8 +5,10 @@ import { t } from './i18n.js';
import { emoji, assetImg } from './theme.js';
let hudEl, tabBar;
let isGuestMode = false;
export function init() {
export function init(guestMode = false) {
isGuestMode = guestMode;
hudEl = document.getElementById('hud');
renderHud();
renderTabBar();
......@@ -69,7 +71,8 @@ function renderHud() {
function renderTabBar() {
tabBar = document.createElement('div');
tabBar.className = 'tab-bar';
const worlds = scene.getWorlds();
const worlds = isGuestMode ? ['play'] : scene.getWorlds();
const allWorlds = scene.getWorlds();
const svgFallback = {
rank: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 7h7l-5.5 4.5 2 7L12 16l-6.5 4.5 2-7L2 9h7z"/></svg>',
social: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M16 11c1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 3-1.34 3-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>',
......@@ -86,17 +89,26 @@ function renderTabBar() {
};
const labels = { rank: 'nav.rank', social: 'nav.social', play: 'nav.play', tournaments: 'nav.tournaments', profile: 'nav.profile' };
const current = scene.getCurrentWorld();
const guestLocked = ['rank', 'social', 'tournaments', 'profile'];
tabBar.innerHTML = worlds.map(w => `
<div class="tab-item ${w === current ? 'active' : ''}" data-world="${w}">
<div class="tab-icon">${icons[w]}</div>
<span>${t(labels[w])}</span>
</div>
`).join('');
tabBar.innerHTML = allWorlds.map(w => {
const locked = isGuestMode && guestLocked.includes(w);
return `
<div class="tab-item ${w === current ? 'active' : ''} ${locked ? 'tab-locked' : ''}" data-world="${w}" ${locked ? 'data-locked="true"' : ''}>
<div class="tab-icon" ${locked ? 'style="opacity:0.4;"' : ''}>${icons[w]}</div>
<span ${locked ? 'style="opacity:0.4;"' : ''}>${t(labels[w])}</span>
</div>`;
}).join('');
tabBar.addEventListener('click', (e) => {
const item = e.target.closest('.tab-item');
if (!item) return;
if (item.dataset.locked) {
showGuestToast();
return;
}
const world = item.dataset.world;
const isAlreadyActive = item.classList.contains('active');
scene.switchWorld(world, isAlreadyActive);
......@@ -105,6 +117,24 @@ function renderTabBar() {
document.body.appendChild(tabBar);
}
function showGuestToast() {
const container = document.getElementById('toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.style.cssText = 'background:var(--bg-elevated);color:var(--text-primary);padding:12px 20px;border-radius:var(--r-md);font-size:14px;display:flex;flex-direction:column;align-items:center;gap:8px;animation:fadeIn 0.2s;';
toast.innerHTML = `
<span>${t('guest.locked_feature')}</span>
<button class="btn btn-primary" style="font-size:13px;padding:6px 16px;min-height:32px;">${t('guest.create_account')}</button>
`;
toast.querySelector('button').addEventListener('click', () => {
store.set('auth.isGuest', false);
store.set('auth.guestId', null);
bus.emit('auth:logout');
});
container.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
}
function updateTabs({ to }) {
tabBar.querySelectorAll('.tab-item').forEach(el => {
el.classList.toggle('active', el.dataset.world === to);
......
......@@ -68,7 +68,24 @@ const strings = {
'common.search': 'بحث',
'common.empty': 'لا توجد بيانات',
'daily.claim': 'اجمع المكافأة',
'daily.streak': 'أيام متتالية'
'daily.streak': 'أيام متتالية',
'auth.guest_btn': 'العب كضيف',
'auth.google_btn': 'الدخول بحساب Google',
'auth.google_register': 'التسجيل بحساب Google',
'auth.age_confirm': 'أؤكد أن عمري 13 سنة أو أكثر',
'auth.agree_terms': 'بالتسجيل أنت توافق على',
'guest.upgrade_prompt': 'سجّل حسابك للعب أونلاين',
'guest.locked_feature': 'سجّل للوصول لهذه الميزة',
'guest.create_account': 'إنشاء حساب',
'settings.delete_account': 'حذف الحساب',
'settings.delete_confirm': 'هل أنت متأكد؟ سيتم حذف جميع بياناتك نهائياً.',
'settings.delete_password': 'أدخل كلمة المرور للتأكيد',
'settings.privacy_policy': 'سياسة الخصوصية',
'settings.terms': 'الشروط والأحكام',
'settings.sound': 'الصوت',
'settings.language': 'اللغة',
'settings.on': 'مفعل',
'settings.off': 'معطل'
},
en: {
'app.name': 'EL3AB',
......@@ -137,7 +154,24 @@ const strings = {
'common.search': 'Search',
'common.empty': 'No data',
'daily.claim': 'Claim Reward',
'daily.streak': 'Day Streak'
'daily.streak': 'Day Streak',
'auth.guest_btn': 'Play as Guest',
'auth.google_btn': 'Sign in with Google',
'auth.google_register': 'Sign up with Google',
'auth.age_confirm': 'I confirm I am 13 years or older',
'auth.agree_terms': 'By registering you agree to',
'guest.upgrade_prompt': 'Create an account to play online',
'guest.locked_feature': 'Sign up to access this feature',
'guest.create_account': 'Create Account',
'settings.delete_account': 'Delete Account',
'settings.delete_confirm': 'Are you sure? All your data will be permanently deleted.',
'settings.delete_password': 'Enter password to confirm',
'settings.privacy_policy': 'Privacy Policy',
'settings.terms': 'Terms of Service',
'settings.sound': 'Sound',
'settings.language': 'Language',
'settings.on': 'On',
'settings.off': 'Off'
}
};
......
......@@ -3,7 +3,7 @@ import * as bus from './bus.js';
const STORAGE_KEY = 'el3ab_state';
const defaultState = {
auth: { token: null, refreshToken: null, userId: null },
auth: { token: null, refreshToken: null, userId: null, isGuest: false, guestId: null },
player: null,
notifications: { unreadCount: 0 },
friends: { onlineCount: 0 },
......
......@@ -8,11 +8,28 @@ import * as theme from './core/theme.js';
import { setLang } from './core/i18n.js';
import { getRecoverableMatch } from './core/match-session.js';
import * as tournamentSession from './core/tournament-session.js';
import * as nativeBridge from './core/native-bridge.js';
async function boot() {
// Handle OAuth callback — extract tokens from URL hash before anything else
const hash = window.location.hash;
if (hash.includes('access_token')) {
const params = new URLSearchParams(hash.substring(1));
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
if (accessToken) {
store.set('auth.token', accessToken);
store.set('auth.refreshToken', refreshToken);
store.set('auth.isGuest', false);
store.set('auth.guestId', null);
}
history.replaceState(null, '', '/');
}
setLang(store.get('language') || 'ar');
await theme.load();
audio.init();
if (window.flutter_bridge || window.IS_NATIVE_APP) nativeBridge.init();
input.init();
scene.init();
......@@ -63,11 +80,14 @@ async function boot() {
}
bus.on('auth:success', onAuthSuccess);
bus.on('auth:guestSuccess', onGuestSuccess);
bus.on('auth:expired', onAuthExpired);
bus.on('auth:logout', onLogout);
}
function onAuthSuccess() {
store.set('auth.isGuest', false);
store.set('auth.guestId', null);
hud.init();
hud.show();
scene.setRoot('play', 'play-table');
......@@ -79,6 +99,13 @@ function onAuthSuccess() {
tournamentSession.init();
}
function onGuestSuccess() {
hud.init(true);
hud.show();
scene.setRoot('play', 'play-table');
scene.switchWorld('play');
}
function onAuthExpired() {
store.set('auth.token', null);
store.set('auth.refreshToken', null);
......
......@@ -6,6 +6,8 @@ import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js';
import { assetImg } from '../../../core/theme.js';
const SUPABASE_URL = 'https://safe-supabase-kong.caprover.al-arcade.com';
export function mountLogin(el) {
el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:var(--s-6);">
......@@ -17,9 +19,28 @@ export function mountLogin(el) {
<div id="login-error" style="color:var(--error);font-size:13px;min-height:20px;"></div>
<button type="submit" class="btn btn-primary w-full" id="login-btn">${t('auth.login.btn')}</button>
</form>
<div style="width:100%;max-width:320px;margin-top:var(--s-4);display:flex;flex-direction:column;gap:var(--s-3);">
<div style="display:flex;align-items:center;gap:var(--s-3);margin:var(--s-2) 0;">
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
<span style="color:var(--text-secondary);font-size:12px;">أو</span>
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
</div>
<button class="btn w-full" id="google-btn" style="background:#fff;color:#333;font-weight:600;min-height:44px;display:flex;align-items:center;justify-content:center;gap:var(--s-2);">
<svg width="18" height="18" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
${t('auth.google_btn')}
</button>
<button class="btn btn-secondary w-full" id="guest-btn" style="min-height:44px;font-weight:600;">
${t('auth.guest_btn')}
</button>
</div>
<p style="margin-top:var(--s-6);color:var(--text-secondary);font-size:14px;">
${t('auth.no_account')} <a href="#" id="goto-register" style="color:var(--gold);text-decoration:none;font-weight:700;">${t('auth.register')}</a>
</p>
<p style="margin-top:var(--s-8);font-size:12px;">
<a href="/privacy-policy" target="_blank" style="color:var(--text-secondary);text-decoration:none;">${t('settings.privacy_policy')}</a>
</p>
</div>
`;
......@@ -62,4 +83,18 @@ export function mountLogin(el) {
audio.play('click');
scene.replace('auth-register');
});
el.querySelector('#google-btn').addEventListener('click', () => {
audio.play('click');
const redirect = encodeURIComponent(window.location.origin + '/');
window.location.href = `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${redirect}`;
});
el.querySelector('#guest-btn').addEventListener('click', () => {
audio.play('click');
store.set('auth.isGuest', true);
store.set('auth.guestId', crypto.randomUUID());
store.set('player', { username: 'ضيف', level: 1, coins: 0, gems: 0 });
bus.emit('auth:guestSuccess');
});
}
......@@ -5,6 +5,8 @@ import * as net from '../../../core/net.js';
import * as audio from '../../../core/audio.js';
import { t } from '../../../core/i18n.js';
const SUPABASE_URL = 'https://safe-supabase-kong.caprover.al-arcade.com';
export function mountRegister(el) {
el.innerHTML = `
<div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;padding:var(--s-6);">
......@@ -14,9 +16,28 @@ export function mountRegister(el) {
<input class="input" type="text" id="reg-username" placeholder="${t('auth.username')}" autocomplete="username" required minlength="3" maxlength="20">
<input class="input" type="email" id="reg-email" placeholder="${t('auth.email')}" autocomplete="email" required>
<input class="input" type="password" id="reg-pass" placeholder="${t('auth.password')}" autocomplete="new-password" required minlength="6">
<label style="display:flex;align-items:flex-start;gap:var(--s-2);font-size:13px;color:var(--text-secondary);cursor:pointer;">
<input type="checkbox" id="reg-age" required style="margin-top:2px;accent-color:var(--gold);">
<span>${t('auth.age_confirm')}</span>
</label>
<p style="font-size:12px;color:var(--text-secondary);text-align:center;">
${t('auth.agree_terms')} <a href="/terms" target="_blank" style="color:var(--gold);text-decoration:none;">${t('settings.terms')}</a> · <a href="/privacy-policy" target="_blank" style="color:var(--gold);text-decoration:none;">${t('settings.privacy_policy')}</a>
</p>
<div id="reg-error" style="color:var(--error);font-size:13px;min-height:20px;"></div>
<button type="submit" class="btn btn-primary w-full" id="reg-btn">${t('auth.register.btn')}</button>
</form>
<div style="width:100%;max-width:320px;margin-top:var(--s-4);">
<div style="display:flex;align-items:center;gap:var(--s-3);margin:var(--s-2) 0;">
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
<span style="color:var(--text-secondary);font-size:12px;">أو</span>
<div style="flex:1;height:1px;background:var(--bg-elevated);"></div>
</div>
<button class="btn w-full" id="google-reg-btn" style="background:#fff;color:#333;font-weight:600;min-height:44px;display:flex;align-items:center;justify-content:center;gap:var(--s-2);">
<svg width="18" height="18" viewBox="0 0 24 24"><path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/><path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/><path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/><path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/></svg>
${t('auth.google_register')}
</button>
</div>
<p style="margin-top:var(--s-6);color:var(--text-secondary);font-size:13px;">
${t('auth.have_account')} <a href="#" id="goto-login" style="color:var(--gold);text-decoration:none;font-weight:600;">${t('auth.login')}</a>
</p>
......@@ -63,4 +84,10 @@ export function mountRegister(el) {
audio.play('click');
scene.replace('auth-login');
});
el.querySelector('#google-reg-btn').addEventListener('click', () => {
audio.play('click');
const redirect = encodeURIComponent(window.location.origin + '/');
window.location.href = `${SUPABASE_URL}/auth/v1/authorize?provider=google&redirect_to=${redirect}`;
});
}
......@@ -22,6 +22,11 @@ export function mountSplash(el) {
`;
setTimeout(async () => {
if (store.get('auth.isGuest')) {
bus.emit('auth:guestSuccess');
return;
}
const token = store.get('auth.token');
if (token) {
try {
......
......@@ -245,6 +245,14 @@ export function mountTable(el) {
const menu = el.querySelector('#game-menu');
const isGuest = store.get('auth.isGuest');
// Hide multiplayer quick actions for guests
if (isGuest) {
const challengeBtn = el.querySelector('#btn-challenge-friend');
if (challengeBtn) challengeBtn.style.display = 'none';
}
// Daily widget buttons
el.querySelector('#btn-challenge-friend')?.addEventListener('click', () => {
audio.play('click');
......@@ -358,11 +366,21 @@ function showGameMenu(menu, game) {
menu.querySelector('#btn-multi').addEventListener('click', () => {
audio.play('click');
if (store.get('auth.isGuest')) {
const toast = document.getElementById('toast-container');
if (toast) {
const msg = document.createElement('div');
msg.style.cssText = 'background:var(--bg-elevated);color:var(--text-primary);padding:12px 20px;border-radius:var(--r-md);font-size:14px;text-align:center;animation:fadeIn 0.2s;';
msg.textContent = t('guest.upgrade_prompt');
toast.appendChild(msg);
setTimeout(() => msg.remove(), 3000);
}
return;
}
menu.classList.add('hidden');
if (game.key === 'chess') {
scene.push('play-time-select', { game: game.key, mode: 'human' });
} else {
// Ludo/Domino don't have time controls — go straight to queue
scene.push('play-queue', { game: game.key, mode: 'human', timeControl: 'standard' });
}
});
......
import * as store from '../../../core/store.js';
import * as bus from '../../../core/bus.js';
import * as scene from '../../../core/scene.js';
import * as audio from '../../../core/audio.js';
import * as net from '../../../core/net.js';
import { t, setLang } from '../../../core/i18n.js';
import { emoji } from '../../../core/theme.js';
......@@ -17,14 +19,42 @@ export function mountSettings(el) {
<div class="card" style="display:flex;flex-direction:column;gap:var(--s-4);">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>الصوت</span>
<button class="btn btn-secondary" id="toggle-audio" style="min-height:36px;padding:var(--s-1) var(--s-3);">${audioOn ? emoji('speaker_on', '🔊', 16) + ' مفعل' : emoji('speaker_off', '🔇', 16) + ' معطل'}</button>
<span>${t('settings.sound')}</span>
<button class="btn btn-secondary" id="toggle-audio" style="min-height:36px;padding:var(--s-1) var(--s-3);">${audioOn ? emoji('speaker_on', '🔊', 16) + ' ' + t('settings.on') : emoji('speaker_off', '🔇', 16) + ' ' + t('settings.off')}</button>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;">
<span>اللغة</span>
<span>${t('settings.language')}</span>
<button class="btn btn-secondary" id="toggle-lang" style="min-height:36px;padding:var(--s-1) var(--s-3);">${lang === 'ar' ? 'العربية' : 'English'}</button>
</div>
</div>
<div class="card" style="display:flex;flex-direction:column;gap:var(--s-3);">
<a href="/privacy-policy" target="_blank" style="color:var(--text-secondary);font-size:14px;text-decoration:none;display:flex;align-items:center;gap:var(--s-2);">
${emoji('lock', '🔒', 14)} ${t('settings.privacy_policy')}
</a>
<a href="/terms" target="_blank" style="color:var(--text-secondary);font-size:14px;text-decoration:none;display:flex;align-items:center;gap:var(--s-2);">
${emoji('document', '📄', 14)} ${t('settings.terms')}
</a>
</div>
<div class="card" style="border:1px solid var(--error);">
<button class="btn w-full" id="delete-account-btn" style="background:transparent;color:var(--error);font-weight:700;min-height:44px;">
${t('settings.delete_account')}
</button>
</div>
<div id="delete-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.8);z-index:9999;display:none;align-items:center;justify-content:center;padding:var(--s-4);">
<div style="background:var(--bg-card);border-radius:var(--r-xl);padding:var(--s-6);max-width:340px;width:100%;display:flex;flex-direction:column;gap:var(--s-4);">
<h3 style="color:var(--error);font-size:16px;text-align:center;">${t('settings.delete_account')}</h3>
<p style="color:var(--text-secondary);font-size:14px;text-align:center;">${t('settings.delete_confirm')}</p>
<input class="input" type="password" id="delete-password" placeholder="${t('settings.delete_password')}" style="display:none;">
<div id="delete-error" style="color:var(--error);font-size:13px;min-height:18px;text-align:center;"></div>
<div style="display:flex;gap:var(--s-3);">
<button class="btn btn-secondary w-full" id="delete-cancel">${t('common.cancel')}</button>
<button class="btn w-full" id="delete-confirm" style="background:var(--error);color:#fff;">${t('common.confirm')}</button>
</div>
</div>
</div>
</div>
`;
......@@ -44,4 +74,52 @@ export function mountSettings(el) {
audio.play('click');
mountSettings(el);
});
const modal = el.querySelector('#delete-modal');
const passInput = el.querySelector('#delete-password');
const errEl = el.querySelector('#delete-error');
el.querySelector('#delete-account-btn').addEventListener('click', async () => {
audio.play('click');
modal.style.display = 'flex';
try {
const me = await net.post('auth.php', { action: 'me' });
if (me.provider === 'email') {
passInput.style.display = 'block';
}
} catch (e) {
passInput.style.display = 'block';
}
});
el.querySelector('#delete-cancel').addEventListener('click', () => {
modal.style.display = 'none';
errEl.textContent = '';
passInput.value = '';
});
el.querySelector('#delete-confirm').addEventListener('click', async () => {
const btn = el.querySelector('#delete-confirm');
btn.disabled = true;
errEl.textContent = '';
try {
const payload = { action: 'delete-account' };
if (passInput.style.display !== 'none' && passInput.value) {
payload.password = passInput.value;
} else {
payload.confirm = true;
}
const res = await net.post('auth.php', payload);
if (res.error) throw new Error(res.error);
store.reset();
bus.emit('auth:logout');
} catch (e) {
errEl.textContent = e.message || t('common.error');
btn.disabled = false;
}
});
}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EL3AB - الشروط والأحكام | Terms of Service</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'IBM Plex Sans Arabic', -apple-system, BlinkMacSystemFont, sans-serif;
background: #050810;
color: #e0e0e0;
line-height: 1.8;
padding: 24px 16px 64px;
}
.container { max-width: 720px; margin: 0 auto; }
.logo { text-align: center; margin-bottom: 32px; }
.logo span { font-size: 32px; font-weight: 900; color: #E4AC38; font-family: 'Inter', sans-serif; }
h1 { font-size: 24px; color: #fff; margin-bottom: 8px; text-align: center; }
.subtitle { text-align: center; color: #888; font-size: 14px; margin-bottom: 40px; }
h2 { font-size: 18px; color: #E4AC38; margin: 32px 0 12px; }
p, li { font-size: 15px; color: #ccc; margin-bottom: 12px; }
ul { padding-right: 24px; padding-left: 24px; }
li { margin-bottom: 8px; }
.section { background: #0d1117; border-radius: 12px; padding: 24px; margin-bottom: 20px; border: 1px solid #1a2030; }
.lang-switch { text-align: center; margin-bottom: 32px; }
.lang-switch a { color: #E4AC38; text-decoration: none; font-weight: 600; padding: 8px 16px; border: 1px solid #E4AC38; border-radius: 8px; font-size: 14px; }
.en { direction: ltr; text-align: left; }
a { color: #E4AC38; }
.date { text-align: center; color: #666; font-size: 13px; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="logo"><span>EL3AB</span></div>
<h1>الشروط والأحكام</h1>
<p class="subtitle">Terms of Service</p>
<div class="lang-switch"><a href="#en">English Version Below</a></div>
<!-- Arabic -->
<div class="section">
<h2>١. القبول</h2>
<p>باستخدامك لتطبيق EL3AB، فإنك توافق على هذه الشروط والأحكام. إذا كنت لا توافق، يرجى عدم استخدام التطبيق.</p>
</div>
<div class="section">
<h2>٢. الأهلية</h2>
<p>يجب أن يكون عمرك ١٣ سنة أو أكثر لاستخدام هذا التطبيق. باستخدامك للتطبيق، فإنك تؤكد أنك تستوفي هذا الشرط.</p>
</div>
<div class="section">
<h2>٣. الحساب</h2>
<ul>
<li>أنت مسؤول عن الحفاظ على أمان حسابك وكلمة مرورك</li>
<li>يجب أن يكون اسم المستخدم مناسباً ولا يحتوي على محتوى مسيء</li>
<li>يحق لنا تعليق أو حذف الحسابات التي تنتهك هذه الشروط</li>
</ul>
</div>
<div class="section">
<h2>٤. سلوك المستخدم</h2>
<p>يُحظر عليك:</p>
<ul>
<li>الغش أو استخدام برامج مساعدة غير مصرح بها</li>
<li>التحرش أو إساءة معاملة اللاعبين الآخرين</li>
<li>انتحال شخصية أي شخص أو جهة</li>
<li>محاولة اختراق أو تعطيل الخدمة</li>
<li>استخدام التطبيق لأي غرض غير قانوني</li>
</ul>
</div>
<div class="section">
<h2>٥. المحتوى والملكية</h2>
<ul>
<li>جميع حقوق التطبيق والمحتوى محفوظة لـ EL3AB</li>
<li>لا يجوز نسخ أو توزيع أي جزء من التطبيق دون إذن</li>
<li>بيانات اللعب والإحصائيات مملوكة لك ويمكنك حذفها في أي وقت</li>
</ul>
</div>
<div class="section">
<h2>٦. إنهاء الخدمة</h2>
<p>يحق لنا تعليق أو إنهاء حسابك في حالة انتهاك هذه الشروط. يمكنك حذف حسابك في أي وقت من إعدادات التطبيق.</p>
</div>
<div class="section">
<h2>٧. إخلاء المسؤولية</h2>
<ul>
<li>التطبيق مقدم "كما هو" دون أي ضمانات</li>
<li>لا نتحمل مسؤولية أي خسائر ناتجة عن استخدام التطبيق</li>
<li>قد يتم تعطيل الخدمة مؤقتاً للصيانة</li>
</ul>
</div>
<div class="section">
<h2>٨. التعديلات</h2>
<p>يحق لنا تعديل هذه الشروط في أي وقت. سيتم إعلامك بأي تغييرات جوهرية.</p>
</div>
<div class="section">
<h2>٩. التواصل</h2>
<p>لأي استفسارات، تواصل معنا عبر: <a href="mailto:support@al-arcade.com">support@al-arcade.com</a></p>
</div>
<!-- English -->
<div id="en" style="margin-top:64px;"></div>
<h1 class="en" style="color:#fff;">Terms of Service</h1>
<p class="subtitle">الشروط والأحكام</p>
<div class="section en">
<h2>1. Acceptance</h2>
<p>By using the EL3AB app, you agree to these Terms of Service. If you do not agree, please do not use the app.</p>
</div>
<div class="section en">
<h2>2. Eligibility</h2>
<p>You must be 13 years of age or older to use this app. By using the app, you confirm that you meet this requirement.</p>
</div>
<div class="section en">
<h2>3. Account</h2>
<ul>
<li>You are responsible for maintaining the security of your account and password</li>
<li>Usernames must be appropriate and not contain offensive content</li>
<li>We reserve the right to suspend or delete accounts that violate these terms</li>
</ul>
</div>
<div class="section en">
<h2>4. User Conduct</h2>
<p>You must not:</p>
<ul>
<li>Cheat or use unauthorized assistance software</li>
<li>Harass or abuse other players</li>
<li>Impersonate any person or entity</li>
<li>Attempt to hack or disrupt the service</li>
<li>Use the app for any illegal purpose</li>
</ul>
</div>
<div class="section en">
<h2>5. Content and Ownership</h2>
<ul>
<li>All app rights and content are reserved by EL3AB</li>
<li>You may not copy or distribute any part of the app without permission</li>
<li>Your gameplay data and statistics belong to you and can be deleted at any time</li>
</ul>
</div>
<div class="section en">
<h2>6. Termination</h2>
<p>We reserve the right to suspend or terminate your account for violating these terms. You can delete your account at any time from the app settings.</p>
</div>
<div class="section en">
<h2>7. Disclaimer</h2>
<ul>
<li>The app is provided "as is" without warranties of any kind</li>
<li>We are not liable for any losses resulting from using the app</li>
<li>The service may be temporarily unavailable for maintenance</li>
</ul>
</div>
<div class="section en">
<h2>8. Changes</h2>
<p>We reserve the right to modify these terms at any time. You will be notified of any material changes.</p>
</div>
<div class="section en">
<h2>9. Contact</h2>
<p>For any inquiries, contact us at: <a href="mailto:support@al-arcade.com">support@al-arcade.com</a></p>
</div>
<p class="date">آخر تحديث: يونيو ٢٠٢٦ | Last updated: June 2026</p>
</div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment