diff --git a/WORKLOG.md b/WORKLOG.md index bb9934a4..7860dae3 100644 --- a/WORKLOG.md +++ b/WORKLOG.md @@ -3,8 +3,7 @@ *PRODUCTION* * ACP * essential - * error log - * account lookup w/ pw reset + * stripe verify * treats * constructs jiggle when clicked @@ -13,12 +12,9 @@ * bot game grind * serde serialize privatise -* stripe prod * mobile styles * change score to enum * pct based translates for combat animation -* account page -* graphs n shit * acp init * DO postgres * make our own toasts / msg pane @@ -28,6 +24,8 @@ * clear skill (if currently targetted) * increase power to speed up early rounds +* only login / logout / register http + ## SOON *SERVER* * modules @@ -38,12 +36,15 @@ * empower on ko * rework vecs into sets +* remove names so games/instances are copy *$$$* * chatwheel * eth adapter * illusions * vaporwave +* crop circles +* insects * sacred geometry * skulls / day of the dead * Aztec diff --git a/client/assets/styles/account.less b/client/assets/styles/account.less index a73faaae..cd4730ca 100644 --- a/client/assets/styles/account.less +++ b/client/assets/styles/account.less @@ -20,6 +20,18 @@ display: block; } + .unsub { + &:hover { + color: @red; + border-color: @red; + }; + + &:active, &.confirming { + background: @red; + color: black; + border: 1px solid black; + } + } .list { letter-spacing: 0.25em; diff --git a/client/assets/styles/styles.less b/client/assets/styles/styles.less index f234fcd0..30b6d8e1 100644 --- a/client/assets/styles/styles.less +++ b/client/assets/styles/styles.less @@ -78,6 +78,10 @@ p { margin-bottom: 1em; } +dl { + margin: 1em 0; +} + #mnml { display: grid; grid-template-columns: minmax(min-content, 1fr) 8fr 1fr; diff --git a/client/src/app.jsx b/client/src/app.jsx index 2e64724f..ddd7a1ea 100644 --- a/client/src/app.jsx +++ b/client/src/app.jsx @@ -30,7 +30,7 @@ document.fonts.load('16pt "Jura"').then(() => { const App = () => ( - + diff --git a/client/src/components/account.management.jsx b/client/src/components/account.management.jsx index 05a8b04b..d20c52a1 100644 --- a/client/src/components/account.management.jsx +++ b/client/src/components/account.management.jsx @@ -18,7 +18,7 @@ const addState = connect( } = state; - function setPassword(current, password) { + function sendSetPassword(current, password) { postData('/account/password', { current, password }) .then(res => res.json()) .then(data => { @@ -29,7 +29,7 @@ const addState = connect( .catch(error => errorToast(error)); } - function setEmail(email) { + function sendSetEmail(email) { postData('/account/email', { email }) .then(res => res.json()) .then(data => { @@ -49,14 +49,19 @@ const addState = connect( return ws.sendMtxConstructSpawn(name); } + function sendSubscriptionCancel() { + return ws.sendSubscriptionCancel(); + } + return { account, ping, email, logout, - setPassword, - setEmail, + sendSetPassword, + sendSetEmail, sendConstructSpawn, + sendSubscriptionCancel, }; }, ); @@ -67,42 +72,73 @@ class AccountStatus extends Component { super(props); this.state = { - setPassword: { current: '', password: '', confirm: ''}, - email: null, + passwordState: { current: '', password: '', confirm: ''}, + emailState: null, + unsubState: false, }; } - render(args) { + render(args, state) { const { account, ping, email, logout, - setPassword, - setEmail, + sendSetEmail, + sendSetPassword, sendConstructSpawn, + sendSubscriptionCancel, } = args; + const { + passwordState, + emailState, + unsubState, + } = state; + if (!account) return null; const passwordsEqual = () => - this.state.setPassword.password === this.state.setPassword.confirm; + passwordState.password === passwordState.confirm; const setPasswordDisabled = () => { - const { current, password, confirm } = this.state.setPassword; + const { current, password, confirm } = passwordState; return !(passwordsEqual() && password && current && confirm); } + const unsub = e => { + if (unsubState) { + return sendSubscriptionCancel(); + } + + e.stopPropagation(); + return this.setState({ unsubState: true }); + } + + const unsubBtn = () => { + if (!account.subscribed) return false; + + const classes = `unsub ${unsubState ? 'confirming' : ''}`; + const text = unsubState ? 'Confirm' : 'Unsubscribe' + return + } + + const tlClick = e => { + e.stopPropagation(); + return this.setState({ unsubState: false }); + } + return ( -
+

{account.name}

Subscription
-
{account.subscribed ? 'some date' : 'unsubscribed'}
+
{account.subscribed ? 'Active' : 'Unsubscribed'}
- + {unsubBtn()} +
@@ -116,11 +152,11 @@ class AccountStatus extends Component { class="login-input" type="email" name="email" - value={this.state.email} - onInput={linkState(this, 'email')} + value={emailState} + onInput={linkState(this, 'emailState')} placeholder="recovery@email.tld" /> - +
@@ -128,29 +164,29 @@ class AccountStatus extends Component { class="login-input" type="password" name="current" - value={this.state.setPassword.current} - onInput={linkState(this, 'setPassword.current')} + value={passwordState.current} + onInput={linkState(this, 'passwordState.current')} placeholder="current" />
diff --git a/client/src/components/stripe.buttons.jsx b/client/src/components/stripe.buttons.jsx index e660e4a9..7d15770e 100644 --- a/client/src/components/stripe.buttons.jsx +++ b/client/src/components/stripe.buttons.jsx @@ -8,7 +8,7 @@ function BitsBtn(args) { } = args; function subscribeClick() { stripe.redirectToCheckout({ - items: [{ plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1 }], + items: [{ plan: 'plan_Fhl9r7UdMadjGi', quantity: 1 }], successUrl: 'http://localhost', cancelUrl: 'http://localhost', clientReferenceId: account.id, diff --git a/client/src/events.jsx b/client/src/events.jsx index 15f9f35b..04a4e6f6 100644 --- a/client/src/events.jsx +++ b/client/src/events.jsx @@ -3,6 +3,7 @@ const eachSeries = require('async/eachSeries'); const actions = require('./actions'); const { TIMES } = require('./constants'); const animations = require('./animations.utils'); +const { infoToast, errorToast } = require('./utils'); function registerEvents(store) { function setPing(ping) { @@ -170,6 +171,10 @@ function registerEvents(store) { return store.dispatch(actions.setItemInfo(v)); } + function notify(msg) { + return infoToast(msg); + } + // events.on('SET_PLAYER', setInstance); // events.on('SEND_SKILL', function skillActive(gameId, constructId, targetConstructId, skill) { @@ -223,6 +228,8 @@ function registerEvents(store) { setShop, setTeam, setWs, + + notify, }; } diff --git a/client/src/socket.jsx b/client/src/socket.jsx index 4fe78845..02c1735d 100644 --- a/client/src/socket.jsx +++ b/client/src/socket.jsx @@ -54,6 +54,10 @@ function createSocket(events) { send(['AccountSetTeam', { ids }]); } + function sendSubscriptionCancel() { + send(['SubscriptionCancel', { }]); + } + function sendGameState(id) { send(['GameState', { id }]); } @@ -166,6 +170,10 @@ function createSocket(events) { events.setTeam(constructs); } + function onAccountSubscription() { + events.notify('Your account has been set to cancelled. You will no longer be billed. Thanks for playing.'); + } + function onConstructSpawn(construct) { events.setNewConstruct(construct); } @@ -200,6 +208,7 @@ function createSocket(events) { AccountTeam: onAccountTeam, AccountInstances: onAccountInstances, AccountShop: onAccountShop, + AccountSubscription: onAccountSubscription, ConstructSpawn: onConstructSpawn, GameState: onGameState, EmailState: onEmailState, @@ -296,6 +305,8 @@ function createSocket(events) { sendAccountInstances, sendAccountSetTeam, + sendSubscriptionCancel, + sendGameState, sendGameReady, sendGameSkill, diff --git a/etc/mnml/gs.SAMPLE.conf b/etc/mnml/gs.conf similarity index 83% rename from etc/mnml/gs.SAMPLE.conf rename to etc/mnml/gs.conf index 20435945..6fafaf6d 100644 --- a/etc/mnml/gs.SAMPLE.conf +++ b/etc/mnml/gs.conf @@ -3,3 +3,5 @@ DATABASE_URL=postgres://mnml:password@somewhere/mnml MAIL_ADDRESS=machines@mnml.gg MAIL_DOMAIN=vinyl.mnml.gg MAIL_PASSWORD=mmmmmmmmmmmmmmmm + +STRIPE_SECRET=shhhhhhhhhh diff --git a/server/Cargo.toml b/server/Cargo.toml index f66c0f87..ab043db4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -38,4 +38,5 @@ ws = "0.8" lettre = "0.9" lettre_email = "0.9" -stripe-rust = { version = "0.10.4", features = ["webhooks"] } +stripe-rust = "0.10" +# stripe-rust = { path = "/home/ntr/code/stripe-rs" } diff --git a/server/src/main.rs b/server/src/main.rs index b452f137..cad0c1f7 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -146,6 +146,8 @@ fn main() { let pg_pool = pool.clone(); let mailer = mail::listen(mail_rx); + let stripe = payments::stripe_client(); + spawn(move || http::start(http_pool, mailer)); spawn(move || warden.listen()); spawn(move || warden::upkeep_tick(warden_tick_tx)); @@ -154,5 +156,5 @@ fn main() { // the main thread becomes this ws listener let rpc_pool = pool.clone(); - rpc::start(rpc_pool, rpc_events_tx); + rpc::start(rpc_pool, rpc_events_tx, stripe); } diff --git a/server/src/payments.rs b/server/src/payments.rs index 7f02388c..c1a56597 100644 --- a/server/src/payments.rs +++ b/server/src/payments.rs @@ -1,3 +1,4 @@ +use std::env; use std::io::Read; use http::State; @@ -12,15 +13,16 @@ use postgres::transaction::Transaction; use failure::Error; use failure::err_msg; -use stripe::{Event, EventObject, CheckoutSession, SubscriptionStatus}; +use stripe::{Client, Event, EventObject, CheckoutSession, SubscriptionStatus}; use http::{MnmlHttpError}; use pg::{PgPool}; use account; +use account::Account; pub fn subscription_account(tx: &mut Transaction, sub: String) -> Result { let query = " - SELECT account + SELECT account, customer, checkout, subscription FROM stripe_subscriptions WHERE subscription = $1; "; @@ -34,6 +36,41 @@ pub fn subscription_account(tx: &mut Transaction, sub: String) -> Result Result, Error> { + let query = " + SELECT account, customer, checkout, subscription + FROM stripe_subscriptions + WHERE account = $1; + "; + + let result = tx + .query(query, &[&account.id])?; + + let row = result.iter().next() + .ok_or(err_msg("user not subscribed"))?; + + let _customer: String = row.get(1); + let _checkout: String = row.get(2); + let subscription: String = row.get(3); + + let id = subscription.parse()?; + let mut params = stripe::UpdateSubscription::new(); + params.cancel_at_period_end = Some(true); + + let updated = match stripe::Subscription::update(client, &id, params) { + Ok(s) => s, + Err(e) => { + warn!("{:?}", e); + return Err(err_msg("unable to cancel subscription")); + } + }; + + info!("subscription cancelled account={:?} subscription={:?}", account, updated); + + Ok(Some(updated.status.to_string())) +} + + // we use i64 because it is converted to BIGINT for pg // and we can losslessly pull it into u32 which is big // enough for the ballers @@ -45,7 +82,7 @@ const CREDITS_SUB_BONUS: i64 = 40; // we ensure that we store each object in pg with a link to the object // and to the account id in case of refunds #[derive(Debug,Clone,Serialize,Deserialize)] -enum StripeData { +pub enum StripeData { Customer { account: Uuid, customer: String, checkout: String }, Subscription { account: Uuid, customer: String, checkout: String, subscription: String, }, @@ -211,6 +248,13 @@ pub fn stripe(req: &mut Request) -> IronResult { } } +pub fn stripe_client() -> Client { + let secret = env::var("STRIPE_SECRET") + .expect("STRIPE_SECRET must be set"); + + stripe::Client::new(secret) +} + #[cfg(test)] mod tests { use super::*; diff --git a/server/src/rpc.rs b/server/src/rpc.rs index eb97be9a..13a4fa08 100644 --- a/server/src/rpc.rs +++ b/server/src/rpc.rs @@ -11,6 +11,8 @@ use failure::err_msg; use serde_cbor::{from_slice, to_vec}; use cookie::Cookie; +use stripe::Client as StripeClient; + use crossbeam_channel::{unbounded, Sender as CbSender}; use ws::{listen, CloseCode, Message, Handler, Request, Response}; @@ -23,6 +25,7 @@ use instance::{Instance, instance_state, instance_practice, instance_ready}; use item::{Item, ItemInfoCtr, item_info}; use mtx; use mail; +use payments; use mail::Email; use pg::{Db}; use pg::{PgPool}; @@ -37,6 +40,7 @@ pub enum RpcMessage { AccountTeam(Vec), AccountInstances(Vec), AccountShop(mtx::Shop), + AccountSubscription(Option), ConstructSpawn(Construct), EmailState(Email), GameState(Game), @@ -44,6 +48,7 @@ pub enum RpcMessage { InstanceState(Instance), + Pong(()), DevResolutions(Resolutions), @@ -75,6 +80,8 @@ enum RpcRequest { AccountConstructs {}, AccountSetTeam { ids: Vec }, + SubscriptionCancel {}, + InstanceQueue {}, InstancePractice {}, InstanceReady { instance_id: Uuid }, @@ -92,6 +99,7 @@ struct Connection { pub id: usize, pub ws: CbSender, pool: PgPool, + stripe: StripeClient, account: Option, events: CbSender, } @@ -192,6 +200,9 @@ impl Connection { RpcRequest::MtxBuy { mtx } => Ok(RpcMessage::AccountShop(mtx::buy(&mut tx, account, mtx)?)), + RpcRequest::SubscriptionCancel { } => + Ok(RpcMessage::AccountSubscription(payments::subscription_cancel(&mut tx, &self.stripe, account)?)), + _ => Err(format_err!("unknown request request={:?}", request)), }; @@ -270,8 +281,10 @@ impl Handler for Connection { // if the user queries the state of something // we tell events to push updates to them match reply { - RpcMessage::AccountState(ref v) => - self.events.send(Event::Subscribe(self.id, v.id)).unwrap(), + RpcMessage::AccountState(ref v) => { + self.account = Some(v.clone()); + self.events.send(Event::Subscribe(self.id, v.id)).unwrap() + }, RpcMessage::GameState(ref v) => self.events.send(Event::Subscribe(self.id, v.id)).unwrap(), RpcMessage::InstanceState(ref v) => @@ -332,7 +345,7 @@ impl Handler for Connection { } } -pub fn start(pool: PgPool, events_tx: CbSender) { +pub fn start(pool: PgPool, events_tx: CbSender, stripe: StripeClient) { let mut rng = thread_rng(); listen("127.0.0.1:40055", move |out| { @@ -364,6 +377,7 @@ pub fn start(pool: PgPool, events_tx: CbSender) { account: None, ws: tx, pool: pool.clone(), + stripe: stripe.clone(), events: events_tx.clone(), } }).unwrap();