Allow playing full version in browser via steam sso

This commit is contained in:
tobspr 2022-06-20 18:22:23 +02:00
parent b446a4a915
commit 145f734907
19 changed files with 267 additions and 43 deletions

View File

@ -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) {

BIN
res/ui/steam_signin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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;

View File

@ -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);

View File

@ -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);
}
}
}

View File

@ -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;
}

81
src/js/core/steam_sso.js Normal file
View File

@ -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);
}

View File

@ -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([

View File

@ -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();

View File

@ -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;
}

View File

@ -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", {

View File

@ -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";
}

View File

@ -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;

View File

@ -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) {

View File

@ -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 `
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
${
available
? ""
: `<span class="standaloneOnlyHint">${
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
}</span>`
}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>
<div class="value enum" data-setting="${this.id}"></div>
@ -229,7 +237,13 @@ export class BoolSetting extends BaseSetting {
const available = this.getIsAvailable(app);
return `
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
${
available
? ""
: `<span class="standaloneOnlyHint">${
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
}</span>`
}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>
@ -289,7 +303,13 @@ export class RangeSetting extends BaseSetting {
const available = this.getIsAvailable(app);
return `
<div class="setting cardbox ${available ? "enabled" : "disabled"}">
${available ? "" : `<span class="standaloneOnlyHint">${T.demo.settingNotAvailable}</span>`}
${
available
? ""
: `<span class="standaloneOnlyHint">${
WEB_STEAM_SSO_AUTHENTICATED ? "" : T.demo.settingNotAvailable
}</span>`
}
<div class="row">
<label>${T.settings.labels[this.id].title}</label>

View File

@ -4,6 +4,7 @@ import { GameState } from "../core/game_state";
import { DialogWithForm } from "../core/modal_dialog_elements";
import { FormElementInput } from "../core/modal_dialog_forms";
import { ReadWriteProxy } from "../core/read_write_proxy";
import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso";
import {
formatSecondsToTimeAgo,
generateFileDownload,
@ -39,7 +40,8 @@ export class MainMenuState extends GameState {
getInnerHTML() {
const showLanguageIcon = !G_CHINA_VERSION && !G_WEGAME_VERSION;
const showExitAppButton = G_IS_STANDALONE;
const showPuzzleDLC = !G_WEGAME_VERSION && G_IS_STANDALONE && !G_IS_STEAM_DEMO;
const showPuzzleDLC =
!G_WEGAME_VERSION && (G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED) && !G_IS_STEAM_DEMO;
const showWegameFooter = G_WEGAME_VERSION;
const hasMods = MODS.anyModsActive();
@ -117,6 +119,26 @@ export class MainMenuState extends GameState {
${showExitAppButton ? `<button class="exitAppButton" aria-label="Exit App"></button>` : ""}
</div>
${
G_IS_STANDALONE || WEB_STEAM_SSO_AUTHENTICATED
? ""
: `<div class="steamSso">
${T.mainMenu.playFullVersion}
<a class="ssoSignIn" href="${
this.app.clientApi.getEndpoint() + "/v1/noauth/steam-sso"
}">Sign in</a>
</div>`
}
${
WEB_STEAM_SSO_AUTHENTICATED
? `
<div class="steamSso">${T.mainMenu.playingFullVersion}
<a href="?sso_logout_silent">${T.mainMenu.logout}</a>
</div>
`
: ""
}
<video autoplay muted loop class="fullscreenBackgroundVideo">
<source src="${cachebust("res/bg_render.webm")}" type="video/webm">
</video>

View File

@ -1,12 +1,9 @@
import { openStandaloneLink, THIRDPARTY_URLS } from "../core/config";
import { queryParamOptions } from "../core/query_parameters";
import { WEB_STEAM_SSO_AUTHENTICATED } from "../core/steam_sso";
import { TextualGameState } from "../core/textual_game_state";
import { MODS } from "../mods/modloader";
import { T } from "../translations";
const MODS_SUPPORTED =
!G_IS_STEAM_DEMO && (G_IS_STANDALONE || (G_IS_DEV && !window.location.href.includes("demo")));
export class ModsState extends TextualGameState {
constructor() {
super("ModsState");
@ -16,6 +13,14 @@ export class ModsState extends TextualGameState {
return T.mods.title;
}
get modsSupported() {
return (
!WEB_STEAM_SSO_AUTHENTICATED &&
!G_IS_STEAM_DEMO &&
(G_IS_STANDALONE || (G_IS_DEV && !window.location.href.includes("demo")))
);
}
internalGetFullHtml() {
let headerHtml = `
<div class="headerBar">
@ -23,12 +28,12 @@ export class ModsState extends TextualGameState {
<div class="actions">
${
MODS_SUPPORTED && MODS.mods.length > 0
this.modsSupported && MODS.mods.length > 0
? `<button class="styledButton browseMods">${T.mods.browseMods}</button>`
: ""
}
${
MODS_SUPPORTED
this.modsSupported
? `<button class="styledButton openModsFolder">${T.mods.openFolder}</button>`
: ""
}
@ -45,11 +50,11 @@ export class ModsState extends TextualGameState {
}
getMainContentHTML() {
if (!MODS_SUPPORTED) {
if (!this.modsSupported) {
return `
<div class="noModSupport">
<p>${T.mods.noModSupport}</p>
<p>${WEB_STEAM_SSO_AUTHENTICATED ? T.mods.browserNoSupport : T.mods.noModSupport}</p>
<br>
<button class="styledButton browseMods">${T.mods.browseMods}</button>
<a href="#" class="steamLink steam_dlbtn_0" target="_blank">Get on Steam!</a>

View File

@ -3,7 +3,8 @@ import { cachebust } from "../core/cachebust";
import { globalConfig } from "../core/config";
import { GameState } from "../core/game_state";
import { createLogger } from "../core/logging";
import { getLogoSprite } from "../core/utils";
import { authorizeViaSSOToken } from "../core/steam_sso";
import { getLogoSprite, timeoutPromise } from "../core/utils";
import { getRandomHint } from "../game/hints";
import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs";
import { PlatformWrapperImplBrowser } from "../platform/browser/wrapper";
@ -45,12 +46,7 @@ export class PreloadState extends GameState {
}
async fetchDiscounts() {
await Promise.race([
new Promise((resolve, reject) => {
setTimeout(() => {
reject("Failed to resolve steam discounts within timeout");
}, 2000);
}),
await timeoutPromise(
fetch("https://analytics.shapez.io/v1/discounts")
.then(res => res.json())
.then(data => {
@ -59,7 +55,8 @@ export class PreloadState extends GameState {
);
logger.log("Fetched current discount:", globalConfig.currentDiscount);
}),
]).catch(err => {
2000
).catch(err => {
logger.warn("Failed to fetch current discount:", err);
});
}
@ -72,6 +69,8 @@ export class PreloadState extends GameState {
this.setStatus("Booting")
.then(() => this.setStatus("Creating platform wrapper", 3))
.then(() => authorizeViaSSOToken(this.app, this.dialogs))
.then(() => this.app.platformWrapper.initialize())
.then(() => this.setStatus("Initializing local storage", 6))

View File

@ -131,6 +131,10 @@ mainMenu:
helpTranslate: Help translate!
madeBy: Made by <author-link>
playFullVersion: Sign in to play the full version in your Browser!
playingFullVersion: You are now playing the full version! Not all features work yet, but I'm working on it!
logout: Logout
# This is shown when using firefox and other browsers which are not supported.
browserWarning: >-
Sorry, but the game is known to run slowly on your browser! Get the full version or download Google Chrome for the full experience.
@ -468,6 +472,19 @@ dialogs:
<br><br>
Error Message:
steamSsoError:
title: Full Version Logout
desc: >-
You have been logged out from the Full Browser Version since either your network connection is unstable or you are playing on another device.<br><br>
Please make sure you don't have shapez open in any other browser tab or another computer with the same Steam account.<br><br>
You can login again in the main menu.
steamSsoNoOwnership:
title: Full Edition not owned
desc: >-
In order to play the Full Edition in your Browser, you need to own both the base game and the Puzzle DLC on your Steam account.<br><br>
Please make sure you own both, signed in with the correct Steam account and then try again.
ingame:
# This is shown in the top left corner and displays useful keybindings in
# every situation
@ -1155,6 +1172,7 @@ mods:
modsInfo: >-
To install and manage mods, copy them to the mods folder (use the 'Open Mods Folder' button). Be sure to restart the game afterwards, otherwise the mods will not show up.
noModSupport: Get the full version on Steam to install mods!
browserNoSupport: Due to browser restrictions it is currently only possible to install mods in the Steam version - Sorry!
togglingComingSoon:
title: Coming Soon