diff --git a/src/auth/AuthContext.jsx b/src/auth/AuthContext.jsx
index 4794ef1..ec61a7e 100644
--- a/src/auth/AuthContext.jsx
+++ b/src/auth/AuthContext.jsx
@@ -71,6 +71,24 @@ export function AuthProvider({ children }) {
} catch {}
};
+ // После успешного входа: если есть запомненная collab-ссылка и мы сейчас
+ // НЕ на ней — переходим. Это и есть «возврат на приглашение» (минкин логин
+ // сам этого не делает). Чистим ключ, чтобы не зациклить.
+ const maybeResumePendingCollab = () => {
+ try {
+ const pending = localStorage.getItem('kbn_pending_collab_url');
+ if (!pending) return;
+ localStorage.removeItem('kbn_pending_collab_url');
+ // уже на нужной collab-странице? не дёргаем
+ if (window.location.href === pending) return;
+ const u = new URL(pending);
+ // безопасность: переходим только в пределах этого же origin (студии)
+ if (u.origin !== window.location.origin) return;
+ if (!/[?&]collab=/.test(pending)) return;
+ window.location.replace(pending);
+ } catch {}
+ };
+
const startWithJwt = (raw) => {
const payload = decodeJwtSafe(raw);
if (!payload) {
@@ -96,6 +114,9 @@ export function AuthProvider({ children }) {
role: profile.role || 'user',
jwt: raw,
});
+ // Возврат на инвайт-ссылку после входа: минка вернула нас в студию
+ // (на главную/кабинет), но мы запомнили collab-URL — идём на него.
+ maybeResumePendingCollab();
})
.catch(() => {
if (cancelled) return;
@@ -149,3 +170,42 @@ export function AuthProvider({ children }) {
return {children};
}
+
+/**
+ * Редирект незалогиненного пользователя на форму входа Рублокса (SSO).
+ * В студии НЕТ собственной страницы /login — вход живёт на КОРНЕ домена
+ * Рублокса (`https://rublox.pro/login`, НЕ `/app/login` — тот даёт 404).
+ * После входа лендинг возвращает в студию/плеер с #ticket=
+ * (его обменивает AuthProvider выше). `return`-параметр сохраняет ПОЛНЫЙ
+ * текущий URL (включая ?collab=), чтобы человек попал ровно на ту
+ * инвайт-ссылку, с которой пришёл.
+ */
+// Ключ, под которым студия запоминает collab-ссылку на время входа.
+// Основной путь возврата — rublox.pro/login читает наш ?return и сам
+// редиректит обратно с #ticket (см. rublox-site LoginPage.finishLogin).
+// Этот pending-ключ — РЕЗЕРВ на случай, если юзер вернулся в студию иным
+// путём: AuthProvider после входа перекинет на запомненную ссылку.
+export const PENDING_COLLAB_KEY = 'kbn_pending_collab_url';
+
+export function redirectToLogin() {
+ const env = (typeof import.meta !== 'undefined' && import.meta.env) || {};
+ // VITE_RUBLOX_HOME = "https://rublox.pro/app" — нам нужен ORIGIN (без /app),
+ // т.к. форма входа сидит на корне домена: https://rublox.pro/login.
+ const rawHome = env.VITE_RUBLOX_HOME || 'https://rublox.pro/app';
+ let origin;
+ try {
+ origin = new URL(rawHome).origin; // https://rublox.pro
+ } catch {
+ origin = rawHome.replace(/\/app\/?$/, '').replace(/\/$/, '');
+ }
+ // Если уходим со страницы с инвайт-токеном — запомним её, чтобы вернуться
+ // после входа (минка сама вернёт только в кабинет, не на эту ссылку).
+ try {
+ const cur = window.location.href;
+ if (/[?&]collab=/.test(cur)) {
+ localStorage.setItem(PENDING_COLLAB_KEY, cur);
+ }
+ } catch {}
+ const back = encodeURIComponent(window.location.href);
+ window.location.href = `${origin}/login?return=${back}`;
+}
diff --git a/src/editor/KubikonEditor.jsx b/src/editor/KubikonEditor.jsx
index 789eda2..ccebae2 100644
--- a/src/editor/KubikonEditor.jsx
+++ b/src/editor/KubikonEditor.jsx
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
-import { useAuth } from '../auth/AuthContext.jsx';
+import { useAuth, redirectToLogin } from '../auth/AuthContext.jsx';
import { useSanctions } from '../auth/SanctionsContext.jsx';
import { BabylonScene } from './engine/BabylonScene';
import { StudioCollab } from './engine/StudioCollab';
@@ -470,6 +470,9 @@ const KubikonEditor = () => {
// Team Create — клиент совместного редактирования + presence-overlay.
const collabRef = useRef(null);
const collabOverlayRef = useRef(null);
+ // ref на initCollab — чтобы вызвать его из doSave (объявлен раньше initCollab)
+ // после создания нового проекта, не дожидаясь перезагрузки страницы.
+ const initCollabRef = useRef(null);
const [collabActive, setCollabActive] = useState(false); // подключены к комнате
const [collabPeers, setCollabPeers] = useState(0); // сколько ДРУГИХ соавторов
// Роль в коллабе: 'owner' (владелец) | 'collab' (приглашённый по ссылке).
@@ -988,10 +991,12 @@ const KubikonEditor = () => {
useEffect(() => {
if (isLoading) return;
// Доступ открыт всем авторизованным (раньше — только admin)
+ // Нет своей страницы /login — уходим на вход минки (SSO) с возвратом
+ // на текущий URL (важно для инвайт-ссылок ?collab=).
if (!isAuthenticated) {
- navigate('/login', { replace: true });
+ redirectToLogin();
}
- }, [isAuthenticated, isLoading, navigate]);
+ }, [isAuthenticated, isLoading]);
/**
* Реальное сохранение на сервер.
@@ -1207,6 +1212,11 @@ const KubikonEditor = () => {
if (newId) {
currentProjectIdRef.current = newId;
window.history.replaceState({}, '', `/edit/${newId}`);
+ // Team Create: проект только что получил реальный ID — поднимаем
+ // совместную сессию сразу, без перезагрузки страницы. Иначе
+ // индикатор соавторов и инвайт не появлялись, пока юзер не
+ // поставит блок → сохранит → обновит (баг 2026-06-08).
+ try { initCollabRef.current?.(Number(newId)); } catch (_) {}
}
} else {
await Kubikon3DApi.updateProject(currentProjectIdRef.current, payload);
@@ -1318,6 +1328,8 @@ const KubikonEditor = () => {
console.warn('[collab] init skipped:', e?.message || e);
}
}, []);
+ // Держим актуальную ссылку на initCollab для вызова из doSave (см. выше).
+ initCollabRef.current = initCollab;
/**
* Team Create: «Пригласить» — запросить collab-токен у realtime, собрать
@@ -1325,10 +1337,25 @@ const KubikonEditor = () => {
*/
const handleInvite = useCallback(async () => {
try {
- if (!/^\d+$/.test(id)) { alert('Сначала сохрани проект.'); return; }
+ // Реальный ID берём из ref (он обновляется после сохранения нового
+ // проекта), а НЕ из URL-параметра id — при создании нового проекта
+ // URL остаётся /edit/new даже после успешного сохранения, поэтому
+ // проверка по id давала вечное «Сначала сохрани» (баг 2026-06-08).
+ let pid = currentProjectIdRef.current;
+ // Не сохранён ещё? Попробуем сохранить прямо сейчас, затем пригласить.
+ if (pid == null) {
+ try {
+ await doSave?.();
+ } catch (_) { /* ignore — проверим pid ниже */ }
+ pid = currentProjectIdRef.current;
+ }
+ if (pid == null || !/^\d+$/.test(String(pid))) {
+ alert('Сначала сохрани проект — добавь хотя бы один объект на сцену и нажми «Сохранить».');
+ return;
+ }
const tokenRaw = localStorage.getItem('Authorization') || localStorage.getItem('jwt') || '';
const base = (REALTIME_HTTP || '').replace(/\/$/, '');
- const res = await fetch(`${base}/studio-invite/${id}`, {
+ const res = await fetch(`${base}/studio-invite/${pid}`, {
method: 'POST',
headers: { Authorization: tokenRaw, 'Content-Type': 'application/json' },
});
@@ -1338,7 +1365,7 @@ const KubikonEditor = () => {
return;
}
const { token } = await res.json();
- const link = `${window.location.origin}/edit/${id}?collab=${token}`;
+ const link = `${window.location.origin}/edit/${pid}?collab=${token}`;
try {
await navigator.clipboard.writeText(link);
collabOverlayRef.current?.toast('Ссылка-приглашение скопирована! Отправь другу.');
@@ -1349,7 +1376,7 @@ const KubikonEditor = () => {
console.warn('[collab] invite failed', e);
alert('Не удалось создать приглашение. Realtime недоступен?');
}
- }, [id]);
+ }, [doSave]);
// Инициализация Babylon + загрузка проекта (если редактируем существующий)
useEffect(() => {
@@ -1365,6 +1392,10 @@ const KubikonEditor = () => {
if (!canvasRef.current) return;
const scene = new BabylonScene(canvasRef.current);
+ // Студия-редактор: тестовый «Запуск» НЕ должен показывать стартовый
+ // экран загрузки игры (он нужен только в плеере на rublox.pro, чтобы
+ // дать время подгрузить ассеты). В студии всё уже в памяти.
+ scene._editorMode = true;
scene.init();
sceneRef.current = scene;
diff --git a/src/editor/engine/BabylonScene.js b/src/editor/engine/BabylonScene.js
index ac0d69d..e1feeeb 100644
--- a/src/editor/engine/BabylonScene.js
+++ b/src/editor/engine/BabylonScene.js
@@ -5972,6 +5972,10 @@ export class BabylonScene {
* Зовётся из enterPlayMode; держится минимум `duration` сек либо до готовности сцены.
*/
showStartupLoadingScreen() {
+ // В студии-редакторе тестовый «Запуск» НИКОГДА не показывает стартовый
+ // экран загрузки — он нужен только в плеере (rublox.pro «Играть»),
+ // чтобы дать время подгрузить ассеты. В редакторе всё уже в памяти.
+ if (this._editorMode) return;
const cfg = this._loadingConfig;
if (!cfg || cfg.enabled === false) return;
if (!this.gameRuntime) return;
diff --git a/src/preview-player/KubikonPlayer.jsx b/src/preview-player/KubikonPlayer.jsx
index 4b85d34..40b4c77 100644
--- a/src/preview-player/KubikonPlayer.jsx
+++ b/src/preview-player/KubikonPlayer.jsx
@@ -17,7 +17,7 @@ import ModalOverlay from '../editor/ModalOverlay';
import SkinShopOverlay from '../editor/SkinShopOverlay';
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
import KubikonChatPanel from './KubikonChatPanel';
-import { useAuth } from '../auth/AuthContext.jsx';
+import { useAuth, redirectToLogin } from '../auth/AuthContext.jsx';
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
import useDeviceType from '../hooks/useDeviceType';
import KubikonMobileControls from './KubikonMobileControls';
@@ -96,13 +96,14 @@ const KubikonPlayer = () => {
};
}, []);
- // Только зарегистрированные. Если гость / без токена — на логин.
+ // Только зарегистрированные. Если гость / без токена — на вход минки (SSO)
+ // с возвратом на текущий URL (своей страницы /login в студии нет).
useEffect(() => {
if (authLoading) return;
if (!isAuthenticated) {
- navigate('/login', { replace: true });
+ redirectToLogin();
}
- }, [isAuthenticated, authLoading, navigate]);
+ }, [isAuthenticated, authLoading]);
const canvasRef = useRef(null);
const sceneRef = useRef(null);