diff --git a/.github/workflows/build-client.yml b/.github/workflows/build-client.yml deleted file mode 100644 index 53730ac..0000000 --- a/.github/workflows/build-client.yml +++ /dev/null @@ -1,228 +0,0 @@ -name: Build Client for All Platforms - -on: - push: - branches: [ main, develop ] - paths: - - 'Client/**' - pull_request: - branches: [ main ] - paths: - - 'Client/**' - workflow_dispatch: - -jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - cache: 'npm' - cache-dependency-path: Client/package-lock.json - - - name: Install system dependencies (Ubuntu) - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt-get update - sudo apt-get install -y libnss3-dev libatk-bridge2.0-dev libdrm2 libxkbcommon-dev libxcomposite-dev libxdamage-dev libxrandr-dev libgbm-dev libxss1 libasound2-dev - - - name: Install system dependencies (macOS) - if: matrix.os == 'macos-latest' - run: | - # Install Xcode command line tools if not present - xcode-select --install 2>/dev/null || true - - - name: Install system dependencies (Windows) - if: matrix.os == 'windows-latest' - run: | - # Windows builds typically have required dependencies pre-installed - # Ensure chocolatey is available for any additional packages - choco --version || echo "Chocolatey not available" - - - name: Install dependencies - working-directory: ./Client - run: | - if [ "${{ matrix.os }}" = "windows-latest" ]; then - npm ci --include=dev - else - npm ci - fi - shell: bash - - - name: Clean previous builds (Windows) - if: matrix.os == 'windows-latest' - working-directory: ./Client - run: if exist dist rmdir /s /q dist - shell: cmd - - - name: Clean previous builds (Unix) - if: matrix.os != 'windows-latest' - working-directory: ./Client - run: rm -rf dist - shell: bash - - - name: Build for Linux - if: matrix.os == 'ubuntu-latest' - working-directory: ./Client - run: npm run build-linux - - - name: Build for Windows - if: matrix.os == 'windows-latest' - working-directory: ./Client - run: npm run build-win - - - name: Build for macOS - if: matrix.os == 'macos-latest' - working-directory: ./Client - run: npm run build-mac - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: client-${{ matrix.os }} - path: | - Client/dist/*.exe - Client/dist/*.msi - Client/dist/*.dmg - Client/dist/*.zip - Client/dist/*.AppImage - Client/dist/*.deb - Client/dist/*.rpm - Client/dist/*.app - retention-days: 30 - continue-on-error: true - - package: - needs: build - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - - name: Create release directory - run: mkdir -p release - - - name: Organize executables by platform - run: | - # Create platform directories - mkdir -p release/Windows - mkdir -p release/macOS - mkdir -p release/Linux - - # Windows executables - copy directly from dist - if [ -d "artifacts/client-windows-latest" ]; then - cp artifacts/client-windows-latest/*.exe release/Windows/ 2>/dev/null || true - cp artifacts/client-windows-latest/*.msi release/Windows/ 2>/dev/null || true - fi - - # macOS executables - copy directly from dist - if [ -d "artifacts/client-macos-latest" ]; then - cp artifacts/client-macos-latest/*.dmg release/macOS/ 2>/dev/null || true - cp artifacts/client-macos-latest/*.zip release/macOS/ 2>/dev/null || true - # Handle .app bundles (directories) - if [ -d "artifacts/client-macos-latest" ]; then - find artifacts/client-macos-latest -maxdepth 1 -name "*.app" -type d -exec cp -r {} release/macOS/ \; 2>/dev/null || true - fi - fi - - # Linux executables - copy directly from dist - if [ -d "artifacts/client-ubuntu-latest" ]; then - cp artifacts/client-ubuntu-latest/*.AppImage release/Linux/ 2>/dev/null || true - cp artifacts/client-ubuntu-latest/*.deb release/Linux/ 2>/dev/null || true - cp artifacts/client-ubuntu-latest/*.rpm release/Linux/ 2>/dev/null || true - fi - - # List what we actually got - echo "=== Executables Found ===" - find release -type f -name "*" -o -name "*.app" | head -20 || true - - # Create platform info file - cat > release/README.txt << EOF - Galaxy Strike Online - Multi-Platform Client - ========================================== - - Windows: - - Run the .exe installer or portable executable - - macOS: - - Open the .dmg file and drag to Applications - - Or extract the .zip and run the .app - - Linux: - - Make the .AppImage executable: chmod +x *.AppImage - - Or install the .deb package: sudo dpkg -i *.deb - - Build Date: $(date -u +"%Y-%m-%d %H:%M:%S UTC") - Commit: ${{ github.sha }} - EOF - - - name: Create all-in-one zip - run: | - cd release - zip -r ../Galaxy-Strike-Online-Client-${{ github.sha }}.zip . - - - name: Upload all-in-one zip - uses: actions/upload-artifact@v4 - with: - name: galaxy-strike-online-client-all-platforms - path: Galaxy-Strike-Online-Client-${{ github.sha }}.zip - retention-days: 90 - - - name: Create Release (Main Branch) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - uses: softprops/action-gh-release@v1 - with: - files: Galaxy-Strike-Online-Client-${{ github.sha }}.zip - name: Galaxy Strike Online Client - Latest - body: | - Latest multi-platform client release for Galaxy Strike Online. - - Includes: - - Windows (NSIS installer + Portable) - - macOS (DMG + ZIP) - - Linux (AppImage + Debian package) - - Commit: ${{ github.sha }} - Build Date: ${{ github.event.head_commit.timestamp }} - - **Download the all-in-one zip file below for all platforms.** - draft: false - prerelease: false - tag_name: latest - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Create Release (Tags) - if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v1 - with: - files: Galaxy-Strike-Online-Client-${{ github.sha }}.zip - name: Galaxy Strike Online Client - ${{ github.ref_name }} - body: | - Multi-platform client release for Galaxy Strike Online. - - Includes: - - Windows (NSIS installer + Portable) - - macOS (DMG + ZIP) - - Linux (AppImage + Debian package) - - Commit: ${{ github.sha }} - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/API/models/PlayerData.js b/API/models/PlayerData.js new file mode 100644 index 0000000..80fd04d --- /dev/null +++ b/API/models/PlayerData.js @@ -0,0 +1,31 @@ +/** + * API/models/PlayerData.js + * + * Minimal PlayerData model for the API server — only what the payment webhook + * needs to read and credit gems. Shares the 'playerdatas' collection with + * the GameServer's full PlayerData model (same MongoDB, same schema name). + * + * Do NOT add complex game logic here — that belongs in GameServer. + */ + +const mongoose = require('mongoose'); + +// Minimal schema — Mixed types let us read/write without strict field validation +const playerDataSchema = new mongoose.Schema({ + userId: { type: String, required: true, unique: true }, + username: { type: String }, + stats: { type: mongoose.Schema.Types.Mixed, default: {} }, +}, { + strict: false, // allow all fields to be stored (don't strip unknown fields) + timestamps: true, +}); + +// Prevent model re-registration error on hot-reload +let PlayerData; +try { + PlayerData = mongoose.model('PlayerData'); +} catch (e) { + PlayerData = mongoose.model('PlayerData', playerDataSchema); +} + +module.exports = PlayerData; diff --git a/API/routes/payments.js b/API/routes/payments.js new file mode 100644 index 0000000..32bba28 --- /dev/null +++ b/API/routes/payments.js @@ -0,0 +1,173 @@ +/** + * routes/payments.js — GSO Premium Gem Store (GDD Phase 3 Monetisation) + * + * Endpoints: + * POST /api/payments/create-checkout — create Stripe Checkout session + * POST /api/payments/webhook — Stripe webhook (payment confirmation) + * GET /api/payments/products — list purchasable gem packages + * + * Requires ENV vars: + * STRIPE_SECRET_KEY — sk_live_... (or sk_test_... for development) + * STRIPE_WEBHOOK_SECRET — whsec_... (from Stripe Dashboard → Webhooks) + * CLIENT_URL — https://galaxystrike.online (for redirect URLs) + * + * Stripe is loaded lazily so the server starts even if the package isn't installed yet. + * Run: npm install stripe in the API directory before going live. + */ + +const express = require('express'); +const router = express.Router(); + +// ── Gem packages available for purchase ──────────────────────────────────────── +const GEM_PACKAGES = [ + { id: 'gems_80', gems: 80, priceUSD: 0.99, label: 'Starter Pack', bonus: 0, popular: false }, + { id: 'gems_200', gems: 200, priceUSD: 1.99, label: 'Pilot Pack', bonus: 0, popular: false }, + { id: 'gems_500', gems: 500, priceUSD: 4.99, label: 'Commander Pack', bonus: 50, popular: true }, + { id: 'gems_1200', gems: 1200, priceUSD: 9.99, label: 'Admiral Pack', bonus: 200,popular: false }, + { id: 'gems_2500', gems: 2500, priceUSD: 19.99, label: 'Fleet Admiral', bonus: 500,popular: false }, + { id: 'gems_6500', gems: 6500, priceUSD: 49.99, label: 'Grand Admiral', bonus: 1500,popular: false}, +]; + +// ── Helper: load Stripe lazily ──────────────────────────────────────────────── +function getStripe() { + if (!process.env.STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not configured'); + // eslint-disable-next-line global-require + return require('stripe')(process.env.STRIPE_SECRET_KEY); +} + +// ── GET /api/payments/products ──────────────────────────────────────────────── +router.get('/products', (req, res) => { + res.json({ packages: GEM_PACKAGES }); +}); + +// ── POST /api/payments/create-checkout ──────────────────────────────────────── +// Body: { packageId, userId, username } +router.post('/create-checkout', async (req, res) => { + try { + const { packageId, userId, username } = req.body; + if (!packageId || !userId) return res.status(400).json({ error: 'packageId and userId required' }); + + const pkg = GEM_PACKAGES.find(p => p.id === packageId); + if (!pkg) return res.status(404).json({ error: 'Unknown package' }); + + const stripe = getStripe(); + const totalGems = pkg.gems + pkg.bonus; + const clientUrl = process.env.CLIENT_URL || 'http://localhost:3000'; + + const session = await stripe.checkout.sessions.create({ + mode: 'payment', + payment_method_types: ['card'], + line_items: [{ + price_data: { + currency: 'usd', + unit_amount: Math.round(pkg.priceUSD * 100), // cents + product_data: { + name: `${pkg.label} — ${totalGems} 💎 Gems`, + description: pkg.bonus > 0 + ? `${pkg.gems} gems + ${pkg.bonus} bonus gems (${totalGems} total)` + : `${pkg.gems} Galaxy Strike Online gems`, + metadata: { packageId: pkg.id }, + }, + }, + quantity: 1, + }], + client_reference_id: userId, + customer_email: req.body.email || undefined, + metadata: { + userId, + username: username || 'unknown', + packageId: pkg.id, + gems: String(totalGems), + }, + success_url: `${clientUrl}/payment-success?session={CHECKOUT_SESSION_ID}`, + cancel_url: `${clientUrl}/payment-cancel`, + }); + + res.json({ sessionId: session.id, url: session.url }); + } catch (err) { + console.error('[PAYMENTS] create-checkout error:', err.message); + res.status(500).json({ error: err.message }); + } +}); + +// ── POST /api/payments/webhook ──────────────────────────────────────────────── +// Stripe sends raw body — must use express.raw() middleware for this route. +router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { + const sig = req.headers['stripe-signature']; + const secret = process.env.STRIPE_WEBHOOK_SECRET; + + if (!secret) { + console.error('[PAYMENTS] STRIPE_WEBHOOK_SECRET not set — webhook rejected'); + return res.status(400).send('Webhook secret not configured'); + } + + let event; + try { + const stripe = getStripe(); + event = stripe.webhooks.constructEvent(req.body, sig, secret); + } catch (err) { + console.error('[PAYMENTS] Webhook signature verification failed:', err.message); + return res.status(400).send(`Webhook Error: ${err.message}`); + } + + if (event.type === 'checkout.session.completed') { + const session = event.data.object; + const { userId, username, packageId, gems } = session.metadata || {}; + + if (!userId || !gems) { + console.error('[PAYMENTS] Webhook missing metadata', session.metadata); + return res.json({ received: true }); + } + + const gemCount = parseInt(gems, 10); + const amountCents = session.amount_total || 0; + + console.log(`[PAYMENTS] ✅ Payment confirmed: ${username} (${userId}) — ${gemCount} gems — $${(amountCents/100).toFixed(2)}`); + + // Credit gems in the database directly + try { + const PlayerData = require('../models/PlayerData'); + const player = await PlayerData.findOne({ userId }); + if (player) { + player.stats.gems = (player.stats.gems || 0) + gemCount; + player.markModified('stats'); + await player.save(); + console.log(`[PAYMENTS] Credited ${gemCount} gems to ${username}. New balance: ${player.stats.gems}`); + } else { + console.warn(`[PAYMENTS] Player not found for userId: ${userId}`); + } + } catch (dbErr) { + console.error('[PAYMENTS] DB credit error:', dbErr.message); + // Return 200 to Stripe regardless — Stripe will retry on 5xx + } + + // Attempt to push live notification to game server via internal HTTP + // (Game server will push 'gems_credited' to the player's socket if online) + const gameServerUrl = process.env.GAME_SERVER_URL || 'http://localhost:3002'; + try { + const http = require('http'); + const body = JSON.stringify({ userId, gems: gemCount, packageId, amountCents }); + const options = { + hostname: new URL(gameServerUrl).hostname, + port: new URL(gameServerUrl).port || 3002, + path: '/internal/credit-gems', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body), + 'X-Internal-Key': process.env.INTERNAL_API_KEY || 'gso-internal', + }, + }; + const req2 = http.request(options); + req2.write(body); + req2.end(); + } catch (notifyErr) { + // Non-fatal — player will see updated gems on next login + } + } + + res.json({ received: true }); +}); + +module.exports = router; +module.exports.GEM_PACKAGES = GEM_PACKAGES; diff --git a/API/server.js b/API/server.js index 3ca426a..947b07b 100644 --- a/API/server.js +++ b/API/server.js @@ -9,8 +9,9 @@ require('dotenv').config(); const logger = require('./utils/logger'); const connectDB = require('./config/database'); -const authRoutes = require('./routes/auth'); -const serverRoutes = require('./routes/servers'); +const authRoutes = require('./routes/auth'); +const serverRoutes = require('./routes/servers'); +const paymentRoutes = require('./routes/payments'); const { errorHandler, notFound } = require('./middleware/errorHandler'); const GameServer = require('./models/GameServer'); @@ -96,6 +97,7 @@ app.use('/api/', async (req, res, next) => { // Routes - API Server Only (Auth + Server Browser) app.use('/api/auth', authRoutes); app.use('/api/servers', serverRoutes); +app.use('/api/payments', paymentRoutes); // Manual cleanup endpoint (for testing) app.post('/api/admin/cleanup-dead-servers', async (req, res) => { diff --git a/Client/electron-main.js b/Client/electron-main.js index 3296162..5acd597 100644 --- a/Client/electron-main.js +++ b/Client/electron-main.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, Menu, shell, ipcMain } = require('electron'); +const { app, BrowserWindow, Menu, shell, ipcMain, Notification } = require('electron'); const path = require('path'); const fs = require('fs'); const logger = require('./js/core/Logger'); @@ -312,6 +312,35 @@ ipcMain.handle('delete-save-file', async (event, slot) => { }); // IPC handlers for window controls + +// ── Push Notifications (Electron desktop) ──────────────────────────────────── +ipcMain.on('show-notification', (event, { title, body, icon, tag }) => { + try { + if (!Notification.isSupported()) return; + const n = new Notification({ + title: title || 'Galaxy Strike Online', + body: body || '', + icon: icon || path.join(__dirname, 'assets/icon.png'), + silent: false, + urgency: 'normal', // 'low' | 'normal' | 'critical' + }); + n.on('click', () => { + if (mainWindow) { + if (mainWindow.isMinimized()) mainWindow.restore(); + mainWindow.focus(); + } + }); + n.show(); + } catch (err) { + console.error('[MAIN PROCESS] Notification error:', err.message); + } +}); + +// Allow renderer to check if window is focused (suppress notifications when in focus) +ipcMain.handle('is-window-focused', () => { + return mainWindow ? mainWindow.isFocused() : true; +}); + // Handle logging from renderer process ipcMain.on('log-message', async (event, { level, message, data }) => { try { diff --git a/Client/index.html b/Client/index.html index d00a00e..2a5829a 100644 --- a/Client/index.html +++ b/Client/index.html @@ -24,6 +24,11 @@ + + +
+ +
@@ -560,133 +565,133 @@ -
@@ -830,7 +845,7 @@ -
+
@@ -932,7 +947,15 @@

Offline Time: 0h 0m

-

Resources Gained: 0

+

Offline Buffer

+ +
+ +
+ +
+ +
@@ -940,6 +963,28 @@ + + + +
@@ -1070,6 +1115,19 @@
+ + + + @@ -1570,6 +1628,22 @@ + + + +
@@ -1634,14 +1708,14 @@ - - + +
@@ -1656,6 +1730,16 @@ + + + @@ -1666,6 +1750,112 @@
+ + + + + + + + + + + + + + + @@ -1948,6 +2138,7 @@ +
@@ -1958,6 +2149,36 @@
+ + + + + + +
+ +
+

Loading fleet…

@@ -2140,6 +2361,10 @@ + + + +
@@ -2488,6 +2713,35 @@
+ +
+
+

+ 🔬 Alliance Research + +

+
+
Loading research tree…
+
+
+
+ + +
+
+

💬 Alliance Chat

+
+
No messages yet
+
+
+ + +
+
+
+
@@ -2688,15 +2942,38 @@ -