From c0613853a230b6d5b462eb2b8b52d2e5d17db44e Mon Sep 17 00:00:00 2001 From: min Date: Mon, 8 Jun 2026 05:57:55 +0300 Subject: [PATCH] =?UTF-8?q?fix(studio):=20Team=20Create=20=E2=80=94=20?= =?UTF-8?q?=D1=8D=D0=BA=D1=80=D0=B0=D0=BD=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8=20=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=BB=D0=B5=D0=B5=D1=80=D0=B5,=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BB=D0=BB=D0=B0=D0=B1=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=B9=20?= =?UTF-8?q?=D0=B8=D0=B3=D1=80=D1=8B,=20=D0=B2=D1=85=D0=BE=D0=B4=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B8=D0=BD=D0=B2=D0=B0=D0=B9=D1=82=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1) Стартовый экран загрузки больше НЕ показывается в студии при тестовом запуске (scene._editorMode), только в плеере на rublox.pro «Играть». 2) Новая игра: коллаб-сессия поднимается сразу после первого сохранения (без перезагрузки) + кнопка «Пригласить» авто-сохраняет проект. 3) Незалогиненный по collab-ссылке → форма входа rublox.pro/login (origin без /app, был 404) с ?return → возврат на инвайт-ссылку. Co-Authored-By: Claude Opus 4.8 --- src/auth/AuthContext.jsx | 60 ++++++++++++++++++++++++++++ src/editor/KubikonEditor.jsx | 45 +++++++++++++++++---- src/editor/engine/BabylonScene.js | 4 ++ src/preview-player/KubikonPlayer.jsx | 9 +++-- 4 files changed, 107 insertions(+), 11 deletions(-) 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); -- 2.47.2