stripe sub cancellations
This commit is contained in:
parent
54a65fc160
commit
d726aa8d33
11
WORKLOG.md
11
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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -78,6 +78,10 @@ p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
dl {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
#mnml {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(min-content, 1fr) 8fr 1fr;
|
||||
|
||||
@ -30,7 +30,7 @@ document.fonts.load('16pt "Jura"').then(() => {
|
||||
|
||||
const App = () => (
|
||||
<Provider store={store}>
|
||||
<StripeProvider apiKey="pk_test_PiLzjIQE7zUy3Xpott7tdQbl00uLiCesTa">
|
||||
<StripeProvider apiKey="pk_test_Cb49tTqTXpzk7nEmlGzRrNJg00AU0aNZDj">
|
||||
<Mnml />
|
||||
</StripeProvider>
|
||||
</Provider>
|
||||
|
||||
@ -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 <button class={classes} onClick={unsub}>{text}</button>
|
||||
}
|
||||
|
||||
const tlClick = e => {
|
||||
e.stopPropagation();
|
||||
return this.setState({ unsubState: false });
|
||||
}
|
||||
|
||||
return (
|
||||
<section class='account'>
|
||||
<section class='account' onClick={tlClick}>
|
||||
<div>
|
||||
<h1>{account.name}</h1>
|
||||
<dl>
|
||||
<dt>Subscription</dt>
|
||||
<dd>{account.subscribed ? 'some date' : 'unsubscribed'}</dd>
|
||||
<dd>{account.subscribed ? 'Active' : 'Unsubscribed'}</dd>
|
||||
</dl>
|
||||
<button><a href={`mailto:humans@mnml.gg?subject=Account%20Support:%20${account.name}`}>✉ support</a></button>
|
||||
{unsubBtn()}
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
<button><a href={`mailto:humans@mnml.gg?subject=Account%20Support:%20${account.name}`}>✉ support</a></button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email">Email Settings:</label>
|
||||
@ -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"
|
||||
/>
|
||||
<button onClick={() => setEmail(this.state.email)}>Update</button>
|
||||
<button onClick={() => sendSetEmail(emailState)}>Update</button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="current">Password:</label>
|
||||
@ -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"
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
name="new"
|
||||
value={this.state.setPassword.password}
|
||||
onInput={linkState(this, 'setPassword.password')}
|
||||
value={passwordState.password}
|
||||
onInput={linkState(this, 'passwordState.password')}
|
||||
placeholder="new password"
|
||||
/>
|
||||
<input
|
||||
class="login-input"
|
||||
type="password"
|
||||
name="confirm"
|
||||
value={this.state.setPassword.confirm}
|
||||
onInput={linkState(this, 'setPassword.confirm')}
|
||||
value={passwordState.confirm}
|
||||
onInput={linkState(this, 'passwordState.confirm')}
|
||||
placeholder="confirm"
|
||||
/>
|
||||
<button
|
||||
disabled={setPasswordDisabled()}
|
||||
onClick={() => setPassword(this.state.setPassword.current, this.state.setPassword.password)}>
|
||||
onClick={() => sendSetPassword(passwordState.current, passwordState.password)}>
|
||||
Set Password
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
@ -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" }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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<Uuid, Error> {
|
||||
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<Uuid, E
|
||||
Ok(row.get(0))
|
||||
}
|
||||
|
||||
pub fn subscription_cancel(tx: &mut Transaction, client: &Client, account: &Account) -> Result<Option<String>, 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<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
@ -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<Construct>),
|
||||
AccountInstances(Vec<Instance>),
|
||||
AccountShop(mtx::Shop),
|
||||
AccountSubscription(Option<String>),
|
||||
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<Uuid> },
|
||||
|
||||
SubscriptionCancel {},
|
||||
|
||||
InstanceQueue {},
|
||||
InstancePractice {},
|
||||
InstanceReady { instance_id: Uuid },
|
||||
@ -92,6 +99,7 @@ struct Connection {
|
||||
pub id: usize,
|
||||
pub ws: CbSender<RpcMessage>,
|
||||
pool: PgPool,
|
||||
stripe: StripeClient,
|
||||
account: Option<Account>,
|
||||
events: CbSender<Event>,
|
||||
}
|
||||
@ -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<Event>) {
|
||||
pub fn start(pool: PgPool, events_tx: CbSender<Event>, 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<Event>) {
|
||||
account: None,
|
||||
ws: tx,
|
||||
pool: pool.clone(),
|
||||
stripe: stripe.clone(),
|
||||
events: events_tx.clone(),
|
||||
}
|
||||
}).unwrap();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user