Merge branch 'accounting' into develop
This commit is contained in:
@@ -297,7 +297,7 @@ button[disabled] {
|
||||
}
|
||||
|
||||
/*
|
||||
HEADER
|
||||
account
|
||||
*/
|
||||
|
||||
header {
|
||||
@@ -307,23 +307,26 @@ header {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
.account {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.account-title {
|
||||
flex: 1;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.header-status {
|
||||
margin: 1em 0;
|
||||
.account-status {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header-username {
|
||||
.account-header {
|
||||
letter-spacing: 0.05em;
|
||||
flex: 1;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.header-status svg {
|
||||
.account-status svg {
|
||||
margin: 0.5em 0 0 1em;
|
||||
height: 1em;
|
||||
background-color: black;
|
||||
@@ -466,6 +469,20 @@ header {
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
.stripe-btn {
|
||||
width: 100%;
|
||||
padding: 0 0.5em;
|
||||
margin: 0.25em 0;
|
||||
background: whitesmoke;
|
||||
color: black;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.stripe-btn:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
|
||||
.refresh-btn {
|
||||
border: 1px solid #222;
|
||||
float: right;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="./index.js"></script>
|
||||
<script>
|
||||
// Check that service workers are registered
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
"node-sass": "^4.12.0",
|
||||
"parcel": "^1.12.3",
|
||||
"preact": "^8.4.2",
|
||||
"preact-compat": "^3.19.0",
|
||||
"preact-context": "^1.1.3",
|
||||
"preact-redux": "^2.1.0",
|
||||
"react-stripe-elements": "^3.0.0",
|
||||
"redux": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -39,5 +41,9 @@
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"jest": "^18.0.0"
|
||||
},
|
||||
"alias": {
|
||||
"react": "preact-compat",
|
||||
"react-dom": "preact-compat"
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -3,6 +3,7 @@ const preact = require('preact');
|
||||
|
||||
const { Provider, connect } = require('preact-redux');
|
||||
const { createStore, combineReducers } = require('redux');
|
||||
const { StripeProvider } = require('react-stripe-elements');
|
||||
|
||||
const reducers = require('./reducers');
|
||||
const actions = require('./actions');
|
||||
@@ -28,7 +29,9 @@ document.fonts.load('16pt "Jura"').then(() => {
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<Mnml />
|
||||
<StripeProvider apiKey="pk_test_PiLzjIQE7zUy3Xpott7tdQbl00uLiCesTa">
|
||||
<Mnml />
|
||||
</StripeProvider>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
const { connect } = require('preact-redux');
|
||||
const preact = require('preact');
|
||||
const { Elements, injectStripe } = require('react-stripe-elements');
|
||||
|
||||
const { saw } = require('./shapes');
|
||||
|
||||
function pingColour(ping) {
|
||||
if (ping < 100) return 'forestgreen';
|
||||
if (ping < 200) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
function BitsBtn(args) {
|
||||
const {
|
||||
stripe,
|
||||
account,
|
||||
} = args;
|
||||
function subscribeClick(e) {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}],
|
||||
successUrl: 'http://localhost:40080/payments/success',
|
||||
cancelUrl: 'http://localhost:40080/payments/cancel',
|
||||
clientReferenceId: account.id
|
||||
});
|
||||
}
|
||||
|
||||
function bitsClick(e) {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{sku: 'sku_FHUfNEhWQaVDaT', quantity: 1}],
|
||||
successUrl: 'http://localhost:40080/payments/success',
|
||||
cancelUrl: 'http://localhost:40080/payments/cancel',
|
||||
clientReferenceId: account.id
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = account.subscribed
|
||||
? <h3 class="account-header">Subscribed</h3>
|
||||
: <button
|
||||
onClick={subscribeClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Subscribe
|
||||
</button>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div id="error-message"></div>
|
||||
{subscription}
|
||||
<button
|
||||
onClick={bitsClick}
|
||||
class="stripe-btn"
|
||||
role="link">
|
||||
Get Bits
|
||||
</button>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const StripeBitsBtn = injectStripe(BitsBtn);
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
} = state;
|
||||
|
||||
function logout() {
|
||||
postData('/logout').then(() => window.location.reload(true));
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
function AccountStatus(args) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
logout,
|
||||
} = args;
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div class="account">
|
||||
<div class="account-status">
|
||||
<h2 class="account-header">{account.name}</h2>
|
||||
{saw(pingColour(ping))}
|
||||
<div class="ping-text">{ping}ms</div>
|
||||
</div>
|
||||
<h3 class="account-header">{`¤${account.credits}`}</h3>
|
||||
<Elements>
|
||||
<StripeBitsBtn account={account} />
|
||||
</Elements>
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = addState(AccountStatus);
|
||||
@@ -3,16 +3,29 @@ const preact = require('preact');
|
||||
const { Component } = require('preact')
|
||||
const { connect } = require('preact-redux');
|
||||
|
||||
const { postData } = require('../utils');
|
||||
|
||||
const addState = connect(
|
||||
(state) => {
|
||||
const { ws, account } = state;
|
||||
const {
|
||||
ws
|
||||
} = state;
|
||||
function submitLogin(name, password) {
|
||||
return ws.sendAccountLogin(name, password);
|
||||
postData('/login', { name, password })
|
||||
.then(data => ws.connect())
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
|
||||
function submitRegister(name, password, code) {
|
||||
return ws.sendAccountCreate(name, password, code);
|
||||
postData('/register', { name, password, code })
|
||||
.then(data => ws.connect())
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
|
||||
return {
|
||||
submitLogin,
|
||||
submitRegister,
|
||||
}
|
||||
return { account, submitLogin, submitRegister };
|
||||
},
|
||||
);
|
||||
|
||||
@@ -65,6 +78,7 @@ class Login extends Component {
|
||||
class="login-input"
|
||||
type="email"
|
||||
placeholder="username"
|
||||
tabIndex={1}
|
||||
value={this.state.name}
|
||||
onInput={this.nameInput}
|
||||
/>
|
||||
@@ -72,6 +86,7 @@ class Login extends Component {
|
||||
class="login-input"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
tabIndex={2}
|
||||
value={this.state.password}
|
||||
onInput={this.passwordInput}
|
||||
/>
|
||||
@@ -79,16 +94,19 @@ class Login extends Component {
|
||||
class="login-input"
|
||||
type="text"
|
||||
placeholder="code"
|
||||
tabIndex={3}
|
||||
value={this.state.code}
|
||||
onInput={this.codeInput}
|
||||
/>
|
||||
<button
|
||||
class="login-btn"
|
||||
tabIndex={4}
|
||||
onClick={this.loginSubmit}>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
class="login-btn"
|
||||
tabIndex={5}
|
||||
onClick={this.registerSubmit}>
|
||||
Register
|
||||
</button>
|
||||
|
||||
@@ -10,8 +10,8 @@ const List = require('./list');
|
||||
|
||||
const addState = connect(
|
||||
state => {
|
||||
const { game, instance, account, nav, team } = state;
|
||||
return { game, instance, account, nav, team };
|
||||
const { game, instance, account, nav, team, constructs } = state;
|
||||
return { game, instance, account, nav, team, constructs };
|
||||
}
|
||||
);
|
||||
|
||||
@@ -22,6 +22,7 @@ function Main(props) {
|
||||
account,
|
||||
nav,
|
||||
team,
|
||||
constructs,
|
||||
} = props;
|
||||
|
||||
if (!account) {
|
||||
@@ -36,8 +37,8 @@ function Main(props) {
|
||||
return <Instance />;
|
||||
}
|
||||
|
||||
if (nav === 'team' || !team.some(t => t) || constructs.length < 3) return <Team />;
|
||||
if (nav === 'list') return <List />;
|
||||
if (nav === 'team' || !team.some(t => t)) return <Team />;
|
||||
|
||||
return (
|
||||
<main></main>
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
const { connect } = require('preact-redux');
|
||||
const preact = require('preact');
|
||||
const { Fragment } = require('preact');
|
||||
const actions = require('../actions');
|
||||
|
||||
const { saw } = require('./shapes');
|
||||
const { postData } = require('../utils');
|
||||
const actions = require('../actions');
|
||||
const AccountStatus = require('./account.status');
|
||||
|
||||
const testGame = process.env.NODE_ENV === 'development' && require('./../test.game');
|
||||
const testInstance = process.env.NODE_ENV === 'development' && require('./../test.instance');
|
||||
|
||||
function pingColour(ping) {
|
||||
if (ping < 100) return 'forestgreen';
|
||||
if (ping < 200) return 'yellow';
|
||||
return 'red';
|
||||
}
|
||||
|
||||
const addState = connect(
|
||||
function receiveState(state) {
|
||||
const {
|
||||
@@ -38,6 +33,10 @@ const addState = connect(
|
||||
return ws.sendInstanceList();
|
||||
}
|
||||
|
||||
function logout() {
|
||||
postData('/logout').then(() => window.location.reload(true));
|
||||
}
|
||||
|
||||
return {
|
||||
account,
|
||||
instances,
|
||||
@@ -47,6 +46,7 @@ const addState = connect(
|
||||
sendInstanceState,
|
||||
sendAccountInstances,
|
||||
sendInstanceList,
|
||||
logout,
|
||||
};
|
||||
},
|
||||
function receiveDispatch(dispatch) {
|
||||
@@ -89,10 +89,9 @@ const addState = connect(
|
||||
function Nav(args) {
|
||||
const {
|
||||
account,
|
||||
ping,
|
||||
team,
|
||||
instances,
|
||||
game,
|
||||
team,
|
||||
|
||||
sendInstanceState,
|
||||
sendAccountInstances,
|
||||
@@ -138,18 +137,11 @@ function Nav(args) {
|
||||
|
||||
const canJoin = team.some(c => !c);
|
||||
|
||||
const accountStatus = account
|
||||
? (<div class="header-status">
|
||||
<h2 class="header-username">{account.name}</h2>
|
||||
{saw(pingColour(ping))}
|
||||
<div class="ping-text">{ping}ms</div>
|
||||
</div>)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<nav onClick={hideNav} >
|
||||
<h1 class="header-title">mnml.gg</h1>
|
||||
{accountStatus}
|
||||
<AccountStatus />
|
||||
<hr />
|
||||
<button onClick={() => navTo('team')}>Select Team</button>
|
||||
<button disabled={canJoin} onClick={() => navTo('list')}>Play</button>
|
||||
<hr />
|
||||
|
||||
+73
-90
@@ -1,7 +1,7 @@
|
||||
const toast = require('izitoast');
|
||||
const cbor = require('borc');
|
||||
|
||||
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000';
|
||||
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/api/ws' : 'ws://localhost:40000/api/ws';
|
||||
|
||||
function errorToast(err) {
|
||||
console.error(err);
|
||||
@@ -15,21 +15,20 @@ function errorToast(err) {
|
||||
function createSocket(events) {
|
||||
let ws;
|
||||
|
||||
// handle account auth within the socket itself
|
||||
// https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
|
||||
let account;
|
||||
try {
|
||||
account = JSON.parse(localStorage.getItem('account'));
|
||||
} catch (e) {
|
||||
localStorage.removeItem('account');
|
||||
}
|
||||
// // handle account auth within the socket itself
|
||||
// // https://www.christian-schneider.net/CrossSiteWebSocketHijacking.html
|
||||
// let account;
|
||||
// try {
|
||||
// account = JSON.parse(localStorage.getItem('account'));
|
||||
// } catch (e) {
|
||||
// localStorage.removeItem('account');
|
||||
// }
|
||||
|
||||
// -------------
|
||||
// Outgoing
|
||||
// -------------
|
||||
function send(msg) {
|
||||
if (msg.method !== 'ping') console.log('outgoing msg', msg);
|
||||
msg.token = account && account.token && account.token;
|
||||
ws.send(cbor.encode(msg));
|
||||
}
|
||||
|
||||
@@ -143,29 +142,22 @@ function createSocket(events) {
|
||||
// -------------
|
||||
// Incoming
|
||||
// -------------
|
||||
function accountLogin(res) {
|
||||
const [struct, login] = res;
|
||||
|
||||
account = login;
|
||||
localStorage.setItem('account', JSON.stringify(login));
|
||||
function onAccount(login) {
|
||||
events.setAccount(login);
|
||||
sendAccountConstructs();
|
||||
sendAccountInstances();
|
||||
}
|
||||
|
||||
function accountInstanceList(res) {
|
||||
const [struct, playerList] = res;
|
||||
events.setAccountInstances(playerList);
|
||||
function onAccountInstances(list) {
|
||||
events.setAccountInstances(list);
|
||||
setTimeout(sendAccountInstances, 5000);
|
||||
}
|
||||
|
||||
function accountConstructs(response) {
|
||||
const [structName, constructs] = response;
|
||||
function onAccountConstructs(constructs) {
|
||||
events.setConstructList(constructs);
|
||||
}
|
||||
|
||||
function gameState(response) {
|
||||
const [structName, game] = response;
|
||||
function onGameState(game) {
|
||||
events.setGame(game);
|
||||
}
|
||||
|
||||
@@ -180,15 +172,6 @@ function createSocket(events) {
|
||||
clearTimeout(gameStateTimeout);
|
||||
}
|
||||
|
||||
function constructSpawn(response) {
|
||||
const [structName, construct] = response;
|
||||
}
|
||||
|
||||
function zoneState(response) {
|
||||
const [structName, zone] = response;
|
||||
events.setZone(zone);
|
||||
}
|
||||
|
||||
let instanceStateTimeout;
|
||||
function startInstanceStateTimeout(id) {
|
||||
clearTimeout(instanceStateTimeout);
|
||||
@@ -196,13 +179,12 @@ function createSocket(events) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function instanceState(response) {
|
||||
const [structName, i] = response;
|
||||
events.setInstance(i);
|
||||
function onInstanceState(instance) {
|
||||
events.setInstance(instance);
|
||||
return true;
|
||||
}
|
||||
|
||||
function instanceList([, list]) {
|
||||
function onOpenInstances(list) {
|
||||
events.setInstanceList(list);
|
||||
return true;
|
||||
}
|
||||
@@ -211,14 +193,14 @@ function createSocket(events) {
|
||||
clearTimeout(instanceStateTimeout);
|
||||
}
|
||||
|
||||
function itemInfo(response) {
|
||||
const [structName, info] = response;
|
||||
function onItemInfo(info) {
|
||||
events.setItemInfo(info);
|
||||
}
|
||||
|
||||
function pong() {
|
||||
let pongTimeout;
|
||||
function onPong() {
|
||||
events.setPing(Date.now() - ping);
|
||||
setTimeout(sendPing, 1000);
|
||||
pongTimeout = setTimeout(sendPing, 1000);
|
||||
}
|
||||
|
||||
// -------------
|
||||
@@ -228,16 +210,14 @@ function createSocket(events) {
|
||||
// when the server sends a reply it will have one of these message types
|
||||
// this object wraps the reply types to a function
|
||||
const handlers = {
|
||||
construct_spawn: constructSpawn,
|
||||
game_state: gameState,
|
||||
account_login: accountLogin,
|
||||
account_create: accountLogin,
|
||||
account_constructs: accountConstructs,
|
||||
account_instances: accountInstanceList,
|
||||
instance_list: instanceList,
|
||||
instance_state: instanceState,
|
||||
item_info: itemInfo,
|
||||
pong,
|
||||
AccountState: onAccount,
|
||||
AccountConstructs: onAccountConstructs,
|
||||
AccountInstances: onAccountInstances,
|
||||
GameState: onGameState,
|
||||
InstanceState: onInstanceState,
|
||||
ItemInfo: onItemInfo,
|
||||
OpenInstances: onOpenInstances,
|
||||
Pong: onPong,
|
||||
};
|
||||
|
||||
function logout() {
|
||||
@@ -249,7 +229,6 @@ function createSocket(events) {
|
||||
function errHandler(error) {
|
||||
switch (error) {
|
||||
case 'invalid token': return logout();
|
||||
case 'no active zone': return sendZoneCreate();
|
||||
case 'no constructs selected': return events.errorPrompt('select_constructs');
|
||||
case 'node requirements not met': return events.errorPrompt('complete_nodes');
|
||||
case 'construct at max skills (4)': return events.errorPrompt('max_skills');
|
||||
@@ -265,58 +244,62 @@ function createSocket(events) {
|
||||
// decode binary msg from server
|
||||
const blob = new Uint8Array(event.data);
|
||||
const res = cbor.decode(blob);
|
||||
const { method, params } = res;
|
||||
const [msgType, params] = res;
|
||||
|
||||
if (method !== 'pong' ) console.log(res);
|
||||
if (msgType !== 'Pong') console.log(res);
|
||||
|
||||
// check for error and split into response type and data
|
||||
if (res.err) return errHandler(res.err);
|
||||
if (!handlers[method]) return errorToast(`${method} handler missing`);
|
||||
return handlers[method](params);
|
||||
if (!handlers[msgType]) return errorToast(`${msgType} handler missing`);
|
||||
return handlers[msgType](params);
|
||||
}
|
||||
|
||||
// Connection opened
|
||||
function onOpen() {
|
||||
toast.info({
|
||||
message: 'connected',
|
||||
position: 'topRight',
|
||||
});
|
||||
|
||||
sendPing();
|
||||
sendItemInfo();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function onError(event) {
|
||||
console.error('WebSocket error', event);
|
||||
}
|
||||
|
||||
function onClose(event) {
|
||||
console.error('WebSocket closed', event);
|
||||
toast.warning({
|
||||
message: 'disconnected',
|
||||
position: 'topRight',
|
||||
});
|
||||
return setTimeout(connect, 5000);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (ws) {
|
||||
clearGameStateTimeout();
|
||||
clearInstanceStateTimeout();
|
||||
clearTimeout(pongTimeout);
|
||||
ws.removeEventListener('open', onOpen);
|
||||
ws.removeEventListener('message', onMessage);
|
||||
ws.removeEventListener('error', onError);
|
||||
ws.removeEventListener('close', onClose);
|
||||
ws = null;
|
||||
}
|
||||
|
||||
ws = new WebSocket(SOCKET_URL);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
|
||||
// Connection opened
|
||||
ws.addEventListener('open', () => {
|
||||
toast.info({
|
||||
message: 'connected',
|
||||
position: 'topRight',
|
||||
});
|
||||
|
||||
sendPing();
|
||||
sendItemInfo();
|
||||
|
||||
if (account) {
|
||||
events.setAccount(account);
|
||||
sendAccountInstances();
|
||||
sendInstanceList();
|
||||
sendAccountConstructs();
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Listen for messages
|
||||
ws.addEventListener('open', onOpen);
|
||||
ws.addEventListener('message', onMessage);
|
||||
|
||||
ws.addEventListener('error', (event) => {
|
||||
console.error('WebSocket error', event);
|
||||
// account = null;
|
||||
// return setTimeout(connect, 5000);
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
console.error('WebSocket closed', event);
|
||||
toast.warning({
|
||||
message: 'disconnected',
|
||||
position: 'topRight',
|
||||
});
|
||||
return setTimeout(connect, 5000);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', onError);
|
||||
ws.addEventListener('close', onClose);
|
||||
return ws;
|
||||
}
|
||||
|
||||
|
||||
+21
-1
@@ -383,7 +383,7 @@ function randomPoints(numPoints, radius, dimensions) {
|
||||
}
|
||||
|
||||
const removeTier = skill => {
|
||||
|
||||
|
||||
if (skill.includes('SiphonTick')) return 'SiphonTick';
|
||||
if (skill.includes('Strike')) return 'Strike';
|
||||
if (skill.includes('Heal')) return 'Heal';
|
||||
@@ -424,6 +424,24 @@ const removeTier = skill => {
|
||||
};
|
||||
|
||||
|
||||
const SERVER = process.env.NODE_ENV === 'production' ? '/api/' : 'http://localhost:40000/api';
|
||||
function postData(url = '/', data = {}) {
|
||||
// Default options are marked with *
|
||||
return fetch(`${SERVER}${url}`, {
|
||||
method: "POST", // *GET, POST, PUT, DELETE, etc.
|
||||
// mode: "no-cors", // no-cors, cors, *same-origin
|
||||
cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: "include", // include, same-origin, *omit
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
redirect: "error", // manual, *follow, error
|
||||
// referrer: "", // no-referrer, *client
|
||||
body: JSON.stringify(data), // body data type must match "Content-Type" header
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
stringSort,
|
||||
convertItem,
|
||||
@@ -431,6 +449,8 @@ module.exports = {
|
||||
eventClasses,
|
||||
getCombatSequence,
|
||||
getCombatText,
|
||||
postData,
|
||||
SERVER,
|
||||
NULL_UUID,
|
||||
STATS,
|
||||
COLOURS,
|
||||
|
||||
Reference in New Issue
Block a user