diff --git a/gulp/preloader/preloader.js b/gulp/preloader/preloader.js index 553cd9c1..7e2fe518 100644 --- a/gulp/preloader/preloader.js +++ b/gulp/preloader/preloader.js @@ -2,6 +2,20 @@ var loadTimeout = null; var callbackDone = false; + var searchString = window.location.search; + if (searchString.includes("steam_sso_auth_token=")) { + var pos = searchString.indexOf("steam_sso_auth_token"); + const authToken = searchString.substring(pos + 21, pos + 57); + try { + window.localStorage.setItem("steam_sso_auth_token", authToken); + window.location.replace(window.location.protocol + "//" + window.location.host); + } catch (ex) { + alert("Failed to login via Steam SSO: " + ex); + window.location.replace("https://shapez.io"); + } + return; + } + // Catch load errors function errorHandler(event, source, lineno, colno, error) { diff --git a/res/ui/steam_signin.png b/res/ui/steam_signin.png new file mode 100644 index 00000000..cd3120f8 Binary files /dev/null and b/res/ui/steam_signin.png differ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index ef02501e..0d16163b 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -49,6 +49,45 @@ } } + .steamSso { + position: fixed; + @include S(top, 0px); + @include S(left, 10px); + background: rgba(88, 92, 102, 0.4); + @include SuperSmallText; + color: #fff; + @include S(max-width, 150px); + @include S(border-radius, $globalBorderRadius); + border-top-left-radius: 0; + border-top-right-radius: 0; + @include S(padding, 5px); + box-shadow: 0 D(5px) D(15px) rgba(#000, 0.1); + display: flex; + color: #000; + flex-direction: column; + + a.ssoSignIn { + background: #171a23 uiResource("steam_signin.png") center center / contain no-repeat; + @include S(width, 110px); + @include S(height, 19px); + display: inline-flex; + @include S(border-radius, $globalBorderRadius); + @include S(margin-top, 3px); + overflow: hidden; + text-indent: -999em; + &:hover { + opacity: 0.95; + } + } + + @include DarkThemeOverride { + color: #333539; + a { + color: #111; + } + } + } + .fullscreenBackgroundVideo { // display: none !important; z-index: -1; diff --git a/src/css/states/mods.scss b/src/css/states/mods.scss index ac082ec6..60912510 100644 --- a/src/css/states/mods.scss +++ b/src/css/states/mods.scss @@ -24,6 +24,9 @@ justify-content: center; height: 100%; flex-direction: column; + text-align: center; + max-width: 80%; + align-self: center; .steamLink { @include S(height, 50px); diff --git a/src/css/states/settings.scss b/src/css/states/settings.scss index 8435a418..98135909 100644 --- a/src/css/states/settings.scss +++ b/src/css/states/settings.scss @@ -181,10 +181,16 @@ pointer-events: all; display: flex; align-items: center; + z-index: 100; justify-content: center; background: rgba(#fff, 0.5); text-transform: uppercase; color: $colorRedBright; + @include S(border-radius, $globalBorderRadius); + + @include DarkThemeOverride { + background: rgba(#55585f, 0.95); + } } } diff --git a/src/js/core/restriction_manager.js b/src/js/core/restriction_manager.js index d775c06d..33783b58 100644 --- a/src/js/core/restriction_manager.js +++ b/src/js/core/restriction_manager.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { ExplainedResult } from "./explained_result"; import { ReadWriteProxy } from "./read_write_proxy"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso"; export class RestrictionManager extends ReadWriteProxy { /** @@ -64,6 +65,10 @@ export class RestrictionManager extends ReadWriteProxy { return false; } + if (WEB_STEAM_SSO_AUTHENTICATED) { + return false; + } + if (G_IS_DEV) { return typeof window !== "undefined" && window.location.search.indexOf("demo") >= 0; } diff --git a/src/js/core/steam_sso.js b/src/js/core/steam_sso.js new file mode 100644 index 00000000..f04fb3bc --- /dev/null +++ b/src/js/core/steam_sso.js @@ -0,0 +1,81 @@ +import { T } from "../translations"; +import { openStandaloneLink } from "./config"; + +export let WEB_STEAM_SSO_AUTHENTICATED = false; + +export async function authorizeViaSSOToken(app, dialogs) { + if (G_IS_STANDALONE) { + return; + } + + if (window.location.search.includes("sso_logout_silent")) { + window.localStorage.setItem("steam_sso_auth_token", ""); + window.location.replace("/"); + return new Promise(() => null); + } + + if (window.location.search.includes("sso_logout")) { + const { ok } = dialogs.showWarning(T.dialogs.steamSsoError.title, T.dialogs.steamSsoError.desc); + window.localStorage.setItem("steam_sso_auth_token", ""); + ok.add(() => window.location.replace("/")); + return new Promise(() => null); + } + + if (window.location.search.includes("steam_sso_no_ownership")) { + const { ok, getStandalone } = dialogs.showWarning( + T.dialogs.steamSsoNoOwnership.title, + T.dialogs.steamSsoNoOwnership.desc, + ["ok", "getStandalone:good"] + ); + window.localStorage.setItem("steam_sso_auth_token", ""); + getStandalone.add(() => { + openStandaloneLink(app, "sso_ownership"); + window.location.replace("/"); + }); + ok.add(() => window.location.replace("/")); + return new Promise(() => null); + } + + const token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token) { + return Promise.resolve(); + } + + const apiUrl = app.clientApi.getEndpoint(); + console.warn("Authorizing via token:", token); + + const verify = async () => { + const token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token) { + window.location.replace("?sso_logout"); + return; + } + + const response = await Promise.race([ + fetch(apiUrl + "/v1/sso/refresh", { + method: "POST", + body: token, + headers: { + "x-api-key": "d5c54aaa491f200709afff082c153ef2", + }, + }), + new Promise((resolve, reject) => { + setTimeout(() => reject("timeout exceeded"), 20000); + }), + ]); + const responseText = await response.json(); + if (!responseText.token) { + console.warn("Failed to register"); + window.localStorage.setItem("steam_sso_auth_token", ""); + window.location.replace("?sso_logout"); + return; + } + + window.localStorage.setItem("steam_sso_auth_token", responseText.token); + app.clientApi.token = responseText.token; + WEB_STEAM_SSO_AUTHENTICATED = true; + }; + + await verify(); + setInterval(verify, 120000); +} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index e75789b9..9e2126b1 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -1,5 +1,6 @@ import { T } from "../translations"; import { rando } from "@nastyox/rando.js"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "./steam_sso"; const bigNumberSuffixTranslationKeys = ["thousands", "millions", "billions", "trillions"]; @@ -764,7 +765,7 @@ export function getLogoSprite() { return "logo_cn.png"; } - if (G_IS_STANDALONE) { + if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { return "logo.png"; } @@ -777,6 +778,7 @@ export function getLogoSprite() { /** * Rejects a promise after X ms + * @param {Promise} promise */ export function timeoutPromise(promise, timeout = 30000) { return Promise.race([ diff --git a/src/js/game/modes/levels.js b/src/js/game/modes/levels.js index bd404dc5..1976c8c7 100644 --- a/src/js/game/modes/levels.js +++ b/src/js/game/modes/levels.js @@ -1,3 +1,4 @@ +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; import { enumHubGoalRewards } from "../tutorial_goals"; export const finalGameShape = "RuCw--Cw:----Ru--"; @@ -356,7 +357,7 @@ const STANDALONE_LEVELS = () => [ export function generateLevelsForVariant() { if (G_IS_STEAM_DEMO) { return STEAM_DEMO_LEVELS(); - } else if (G_IS_STANDALONE) { + } else if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { return STANDALONE_LEVELS(); } return WEB_DEMO_LEVELS(); diff --git a/src/js/game/modes/regular.js b/src/js/game/modes/regular.js index 7712582f..f0d62211 100644 --- a/src/js/game/modes/regular.js +++ b/src/js/game/modes/regular.js @@ -36,7 +36,8 @@ import { HUDInteractiveTutorial } from "../hud/parts/interactive_tutorial"; import { MetaBlockBuilding } from "../buildings/block"; import { MetaItemProducerBuilding } from "../buildings/item_producer"; import { MOD_SIGNALS } from "../../mods/mod_signals"; -import { finalGameShape, generateLevelsForVariant, LevelSetVariant } from "./levels"; +import { finalGameShape, generateLevelsForVariant } from "./levels"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; /** @typedef {{ * shape: string, @@ -377,7 +378,7 @@ export class RegularGameMode extends GameMode { } get difficultyMultiplicator() { - if (G_IS_STANDALONE) { + if (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) { if (G_IS_STEAM_DEMO) { return 0.75; } diff --git a/src/js/platform/api.js b/src/js/platform/api.js index 95398ca8..5dbac1da 100644 --- a/src/js/platform/api.js +++ b/src/js/platform/api.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; import { compressX64 } from "../core/lzstring"; +import { timeoutPromise } from "../core/utils"; import { T } from "../translations"; const logger = createLogger("puzzle-api"); @@ -53,23 +54,23 @@ export class ClientAPI { headers["x-token"] = this.token; } - return Promise.race([ + return timeoutPromise( fetch(this.getEndpoint() + endpoint, { cache: "no-cache", mode: "cors", headers, method: options.method || "GET", body: options.body ? JSON.stringify(options.body) : undefined, + }), + 15000 + ) + .then(res => { + if (res.status !== 200) { + throw "bad-status: " + res.status + " / " + res.statusText; + } + return res; }) - .then(res => { - if (res.status !== 200) { - throw "bad-status: " + res.status + " / " + res.statusText; - } - return res; - }) - .then(res => res.json()), - new Promise((resolve, reject) => setTimeout(() => reject("timeout"), 15000)), - ]) + .then(res => res.json()) .then(data => { if (data && data.error) { logger.warn("Got error from api:", data); @@ -100,22 +101,17 @@ export class ClientAPI { */ apiTryLogin() { if (!G_IS_STANDALONE) { - let token = window.localStorage.getItem("dev_api_auth_token"); - if (!token) { + let token = window.localStorage.getItem("steam_sso_auth_token"); + if (!token && G_IS_DEV) { token = window.prompt( "Please enter the auth token for the puzzle DLC (If you have none, you can't login):" ); - } - if (token) { window.localStorage.setItem("dev_api_auth_token", token); } return Promise.resolve({ token }); } - return Promise.race([ - ipcRenderer.invoke("steam:get-ticket"), - new Promise((resolve, reject) => setTimeout(() => reject("timeout"), 15000)), - ]).then( + return timeoutPromise(ipcRenderer.invoke("steam:get-ticket"), 15000).then( ticket => { logger.log("Got auth ticket:", ticket); return this._request("/v1/public/login", { diff --git a/src/js/platform/browser/game_analytics.js b/src/js/platform/browser/game_analytics.js index 747f25ef..7694ad4d 100644 --- a/src/js/platform/browser/game_analytics.js +++ b/src/js/platform/browser/game_analytics.js @@ -13,6 +13,7 @@ import { FILE_NOT_FOUND } from "../storage"; import OR from "@openreplay/tracker"; import OR_fetch from "@openreplay/tracker-fetch"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; let eventConnector; if (!G_IS_STANDALONE && !G_IS_DEV) { @@ -57,6 +58,10 @@ export class ShapezGameAnalytics extends GameAnalyticsInterface { return "steam"; } + if (WEB_STEAM_SSO_AUTHENTICATED) { + return "prod-full"; + } + if (G_IS_RELEASE) { return "prod"; } diff --git a/src/js/platform/browser/wrapper.js b/src/js/platform/browser/wrapper.js index 3610b533..267fce08 100644 --- a/src/js/platform/browser/wrapper.js +++ b/src/js/platform/browser/wrapper.js @@ -1,6 +1,7 @@ import { globalConfig, IS_MOBILE } from "../../core/config"; import { createLogger } from "../../core/logging"; import { queryParamOptions } from "../../core/query_parameters"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../../core/steam_sso"; import { clamp } from "../../core/utils"; import { GamedistributionAdProvider } from "../ad_providers/gamedistribution"; import { NoAdProvider } from "../ad_providers/no_ad_provider"; @@ -24,7 +25,7 @@ export class PlatformWrapperImplBrowser extends PlatformWrapperInterface { iogLink: true, }; - if (!G_IS_STANDALONE && queryParamOptions.embedProvider) { + if (!G_IS_STANDALONE && !WEB_STEAM_SSO_AUTHENTICATED && queryParamOptions.embedProvider) { const providerId = queryParamOptions.embedProvider; this.embedProvider.iframed = true; this.embedProvider.iogLink = false; diff --git a/src/js/profile/application_settings.js b/src/js/profile/application_settings.js index 281b532c..33dd9e84 100644 --- a/src/js/profile/application_settings.js +++ b/src/js/profile/application_settings.js @@ -511,6 +511,12 @@ export class ApplicationSettings extends ReadWriteProxy { return ExplainedResult.bad("Bad settings object"); } + // MODS + if (!THEMES[data.settings.theme] || !this.app.restrictionMgr.getHasExtendedSettings()) { + console.log("Resetting theme because its no longer available: " + data.settings.theme); + data.settings.theme = "light"; + } + const settings = data.settings; for (let i = 0; i < this.settingHandles.length; ++i) { diff --git a/src/js/profile/setting_types.js b/src/js/profile/setting_types.js index 943e8e53..ccc90d70 100644 --- a/src/js/profile/setting_types.js +++ b/src/js/profile/setting_types.js @@ -3,6 +3,7 @@ import { Application } from "../application"; /* typehints:end */ import { createLogger } from "../core/logging"; +import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso"; import { T } from "../translations"; const logger = createLogger("setting_types"); @@ -149,9 +150,16 @@ export class EnumSetting extends BaseSetting { */ getHtml(app) { const available = this.getIsAvailable(app); + return `
${T.mods.noModSupport}
+${WEB_STEAM_SSO_AUTHENTICATED ? T.mods.browserNoSupport : T.mods.noModSupport}