Merge branch 'accounting' into develop

This commit is contained in:
ntr
2019-06-28 16:10:17 +10:00
32 changed files with 1988 additions and 706 deletions
+23 -6
View File
@@ -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;
+1
View File
@@ -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
+6
View File
@@ -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
View File
@@ -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>
);
+107
View File
@@ -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);
+22 -4
View File
@@ -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>
+4 -3
View File
@@ -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>
+11 -19
View File
@@ -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
View File
@@ -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
View File
@@ -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,