mtx init
This commit is contained in:
parent
6f7c2cf571
commit
e74c820ce4
@ -463,6 +463,7 @@ header {
|
||||
}
|
||||
|
||||
.stripe-btn {
|
||||
width: 100%;
|
||||
padding: 0 0.5em;
|
||||
background: whitesmoke;
|
||||
color: black;
|
||||
|
||||
@ -11,12 +11,16 @@ function pingColour(ping) {
|
||||
}
|
||||
|
||||
function BitsBtn(args) {
|
||||
const { stripe } = args;
|
||||
const {
|
||||
stripe,
|
||||
account,
|
||||
} = args;
|
||||
function onClick(e) {
|
||||
stripe.redirectToCheckout({
|
||||
items: [{sku: 'sku_FGjPl1YKoT241P', quantity: 1}],
|
||||
items: [{plan: 'plan_FGmRwawcOJJ7Nv', quantity: 1}],
|
||||
successUrl: 'http://localhost:40080/payments/success',
|
||||
cancelUrl: 'http://localhost:40080/payments/cancel',
|
||||
clientReferenceId: account.id
|
||||
});
|
||||
}
|
||||
return (
|
||||
@ -27,7 +31,7 @@ function BitsBtn(args) {
|
||||
class="stripe-btn"
|
||||
id="checkout-button-sku_FGjPl1YKoT241P"
|
||||
role="link">
|
||||
Buy Bits
|
||||
Subscribe
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
@ -72,7 +76,7 @@ function AccountStatus(args) {
|
||||
<div class="ping-text">{ping}ms</div>
|
||||
</div>
|
||||
<Elements>
|
||||
<StripeBitsBtn />
|
||||
<StripeBitsBtn account={account} />
|
||||
</Elements>
|
||||
<button onClick={() => logout()}>Logout</button>
|
||||
</div>
|
||||
|
||||
@ -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/ws/';
|
||||
const SOCKET_URL = process.env.NODE_ENV === 'production' ? 'wss://mnml.gg/ws' : 'ws://localhost:40000/api/ws';
|
||||
|
||||
function errorToast(err) {
|
||||
console.error(err);
|
||||
|
||||
49
ops/mnml.gg.DEV.nginx.conf
Normal file
49
ops/mnml.gg.DEV.nginx.conf
Normal file
@ -0,0 +1,49 @@
|
||||
upstream mnml_dev {
|
||||
server 0.0.0.0:41337;
|
||||
}
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
# DEV
|
||||
server {
|
||||
root /home/git/mnml/client/dist/;
|
||||
index index.html;
|
||||
|
||||
server_name dev.mnml.gg; # managed by Certbot
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
listen [::]:443;
|
||||
ssl on;
|
||||
listen 443 ssl; # managed by Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/dev.mnml.gg/fullchain.pem; # managed by Certbot
|
||||
ssl_certificate_key /etc/letsencrypt/live/dev.mnml.gg/privkey.pem; # managed by Certbot
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
|
||||
|
||||
location /api/ws {
|
||||
proxy_pass http://mnml_dev;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://mnml_dev;
|
||||
proxy_read_timeout 600s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
# http -> https
|
||||
server {
|
||||
server_name dev.mnml.gg;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ map $http_upgrade $connection_upgrade {
|
||||
'' close;
|
||||
}
|
||||
|
||||
# PRODUCTION
|
||||
server {
|
||||
root /home/git/mnml/client/dist/;
|
||||
index index.html;
|
||||
@ -1,2 +1 @@
|
||||
DATABASE_URL=postgres://mnml:craftbeer@localhost/mnml
|
||||
DEV_CORS=true
|
||||
|
||||
@ -31,3 +31,4 @@ actix-web-actors = "1.0.0"
|
||||
futures = "0.1"
|
||||
bytes = "0.4"
|
||||
|
||||
stripe-rust = "0.10.2"
|
||||
|
||||
@ -21,23 +21,27 @@ extern crate serde_cbor;
|
||||
extern crate fern;
|
||||
#[macro_use] extern crate log;
|
||||
|
||||
extern crate stripe;
|
||||
|
||||
mod account;
|
||||
mod construct;
|
||||
mod effect;
|
||||
mod game;
|
||||
mod instance;
|
||||
mod item;
|
||||
mod mob;
|
||||
mod mtx;
|
||||
mod names;
|
||||
mod net;
|
||||
mod player;
|
||||
mod rpc;
|
||||
mod pubsub;
|
||||
mod rpc;
|
||||
mod skill;
|
||||
mod effect;
|
||||
mod spec;
|
||||
mod util;
|
||||
mod vbox;
|
||||
mod warden;
|
||||
mod ws;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use net::{start};
|
||||
|
||||
15
server/src/mtx.rs
Normal file
15
server/src/mtx.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use actix_web::{web, HttpResponse};
|
||||
use stripe::{CheckoutSession};
|
||||
|
||||
use net::{State, MnmlError};
|
||||
|
||||
pub fn stripe_payment(state: web::Data<State>, body: web::Json::<CheckoutSession>) -> Result<HttpResponse, MnmlError> {
|
||||
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
|
||||
|
||||
let session = body.into_inner();
|
||||
|
||||
info!("{:?}", session);
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
@ -1,129 +1,31 @@
|
||||
use std::time::{Instant, Duration};
|
||||
use std::env;
|
||||
|
||||
use serde_cbor::{to_vec};
|
||||
|
||||
use actix_web::{middleware, web, App, Error, HttpMessage, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix_web::{middleware, web, App, HttpMessage, HttpRequest, HttpResponse, HttpServer};
|
||||
use actix_web::middleware::cors::Cors;
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::{Cookie};
|
||||
use actix_web::cookie::{SameSite};
|
||||
use actix_web_actors::ws;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use r2d2::{Pool};
|
||||
use r2d2::{PooledConnection};
|
||||
use r2d2_postgres::{TlsMode, PostgresConnectionManager};
|
||||
|
||||
use rpc::{receive, RpcResult, RpcErrorResponse, AccountLoginParams, AccountCreateParams};
|
||||
use rpc::{RpcErrorResponse, AccountLoginParams, AccountCreateParams};
|
||||
use warden::{Warden};
|
||||
use pubsub::PubSub;
|
||||
use account::{Account, account_login, account_create, account_from_token, account_set_token};
|
||||
use ws::{connect};
|
||||
use account::{account_login, account_create, account_from_token, account_set_token};
|
||||
use mtx::{stripe_payment};
|
||||
|
||||
pub type Db = PooledConnection<PostgresConnectionManager>;
|
||||
pub type PgPool = Pool<PostgresConnectionManager>;
|
||||
|
||||
const DB_POOL_SIZE: u32 = 20;
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct MnmlSocket {
|
||||
hb: Instant,
|
||||
pool: PgPool,
|
||||
account: Option<Account>,
|
||||
}
|
||||
|
||||
impl Actor for MnmlSocket {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
// once the actor has been started this fn runs
|
||||
// it starts the heartbeat interval and keepalive
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
self.hb(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `ws::Message`
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for MnmlSocket {
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
match self.account.as_ref() {
|
||||
Some(a) => {
|
||||
info!("user connected {:?}", a);
|
||||
let account_state = to_vec(&RpcResult::AccountState(a.clone()))
|
||||
.expect("could not serialize account state");
|
||||
ctx.binary(account_state)
|
||||
},
|
||||
None => info!("new connection"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
// process websocket messages
|
||||
let begin = Instant::now();
|
||||
debug!("msg: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg);
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(_text) => (),
|
||||
ws::Message::Close(_) => {
|
||||
match self.account.as_ref() {
|
||||
Some(a) => info!("disconnected {:?}", a),
|
||||
None => info!("disconnected"),
|
||||
}
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Nop => (),
|
||||
ws::Message::Binary(bin) => {
|
||||
let db_connection = self.pool.get().expect("unable to get db connection");
|
||||
match receive(bin.to_vec(), &db_connection, ctx, begin, self.account.as_ref()) {
|
||||
Ok(reply) => {
|
||||
let response = to_vec(&reply)
|
||||
.expect("failed to serialize response");
|
||||
ctx.binary(response);
|
||||
},
|
||||
Err(e) => {
|
||||
let response = to_vec(&RpcErrorResponse { err: e.to_string() })
|
||||
.expect("failed to serialize error response");
|
||||
ctx.binary(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MnmlSocket {
|
||||
fn new(state: web::Data<State>, account: Option<Account>) -> MnmlSocket {
|
||||
// idk why this has to be cloned again
|
||||
// i guess because each socket is added as a new thread?
|
||||
MnmlSocket { hb: Instant::now(), pool: state.pool.clone(), account }
|
||||
}
|
||||
|
||||
// starts the keepalive interval once actor started
|
||||
fn hb(&self, ctx: &mut <MnmlSocket as Actor>::Context) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
info!("idle connection terminated");
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping("");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
enum MnmlError {
|
||||
pub enum MnmlError {
|
||||
#[fail(display="internal server error")]
|
||||
ServerError,
|
||||
#[fail(display="unauthorized")]
|
||||
@ -153,28 +55,6 @@ impl ResponseError for MnmlError {
|
||||
}
|
||||
}
|
||||
|
||||
// idk how this stuff works
|
||||
// but the args extract what you need from the incoming requests
|
||||
// this grabs
|
||||
// the req obj itself which we need for cookies
|
||||
// the application state
|
||||
// and the websocket stream
|
||||
fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||
let account: Option<Account> = match r.cookie("x-auth-token") {
|
||||
Some(t) => {
|
||||
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
|
||||
match account_from_token(t.value().to_string(), &mut tx) {
|
||||
Ok(a) => Some(a),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
ws::start(MnmlSocket::new(state, account), &r, stream)
|
||||
}
|
||||
|
||||
fn login_res(token: String, secure: bool) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.cookie(Cookie::build("x-auth-token", token)
|
||||
@ -257,8 +137,8 @@ fn create_pool(url: String) -> Pool<PostgresConnectionManager> {
|
||||
.expect("Failed to create pool.")
|
||||
}
|
||||
|
||||
struct State {
|
||||
pool: PgPool,
|
||||
pub struct State {
|
||||
pub pool: PgPool,
|
||||
secure: bool,
|
||||
}
|
||||
|
||||
@ -274,29 +154,18 @@ pub fn start() {
|
||||
let pubsub_conn = pool.get().expect("could not get pubsub pg connection");
|
||||
let pubsub_addr = Supervisor::start(move |_| PubSub::new(pubsub_conn));
|
||||
|
||||
match env::var("DEV_CORS") {
|
||||
Ok(_) => {
|
||||
warn!("enabling dev CORS middleware");
|
||||
HttpServer::new(move || App::new()
|
||||
.data(State { pool: pool.clone(), secure: false })
|
||||
.wrap(middleware::Logger::default())
|
||||
.wrap(Cors::new().supports_credentials())
|
||||
.service(web::resource("/login").route(web::post().to(login)))
|
||||
.service(web::resource("/logout").route(web::post().to(logout)))
|
||||
.service(web::resource("/register").route(web::post().to(register)))
|
||||
.service(web::resource("/ws/").route(web::get().to(connect))))
|
||||
.service(web::resource("/api/login").route(web::post().to(login)))
|
||||
.service(web::resource("/api/logout").route(web::post().to(logout)))
|
||||
.service(web::resource("/api/register").route(web::post().to(register)))
|
||||
|
||||
.service(web::resource("/api/payments/stripe").route(web::post().to(stripe_payment)))
|
||||
.service(web::resource("/api/payments/crypto").route(web::post().to(stripe_payment)))
|
||||
|
||||
.service(web::resource("/api/ws/").route(web::get().to(connect))))
|
||||
.bind("127.0.0.1:40000").expect("could not bind to port")
|
||||
.run().expect("could not start http server")
|
||||
},
|
||||
Err(_) =>
|
||||
HttpServer::new(move || App::new()
|
||||
.data(State { pool: pool.clone(), secure: true })
|
||||
.wrap(middleware::Logger::default())
|
||||
.service(web::resource("/login").route(web::post().to(login)))
|
||||
.service(web::resource("/logout").route(web::post().to(logout)))
|
||||
.service(web::resource("/register").route(web::post().to(register)))
|
||||
.service(web::resource("/ws/").route(web::get().to(connect))))
|
||||
.bind("127.0.0.1:40000").expect("could not bind to port")
|
||||
.run().expect("could not start http server"),
|
||||
};
|
||||
.run().expect("could not start http server");
|
||||
}
|
||||
|
||||
@ -8,7 +8,8 @@ use uuid::Uuid;
|
||||
use failure::Error;
|
||||
use failure::err_msg;
|
||||
|
||||
use net::{Db, MnmlSocket};
|
||||
use net::{Db};
|
||||
use ws::{MnmlSocket};
|
||||
use construct::{Construct, construct_spawn, construct_delete};
|
||||
use game::{Game, game_state, game_skill, game_ready};
|
||||
use account::{Account, account_constructs, account_instances};
|
||||
|
||||
131
server/src/ws.rs
Normal file
131
server/src/ws.rs
Normal file
@ -0,0 +1,131 @@
|
||||
use std::time::{Instant, Duration};
|
||||
|
||||
use actix_web::{web, Error, HttpMessage, HttpRequest, HttpResponse};
|
||||
use actix_web_actors::ws;
|
||||
use actix::prelude::*;
|
||||
|
||||
use account::{Account, account_from_token};
|
||||
use serde_cbor::{to_vec};
|
||||
use net::{PgPool, MnmlError, State};
|
||||
|
||||
use rpc::{receive, RpcResult, RpcErrorResponse};
|
||||
|
||||
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
|
||||
const CLIENT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub struct MnmlSocket {
|
||||
hb: Instant,
|
||||
pool: PgPool,
|
||||
account: Option<Account>,
|
||||
}
|
||||
|
||||
impl Actor for MnmlSocket {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
|
||||
// once the actor has been started this fn runs
|
||||
// it starts the heartbeat interval and keepalive
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
self.hb(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for `ws::Message`
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for MnmlSocket {
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
match self.account.as_ref() {
|
||||
Some(a) => {
|
||||
info!("user connected {:?}", a);
|
||||
let account_state = to_vec(&RpcResult::AccountState(a.clone()))
|
||||
.expect("could not serialize account state");
|
||||
ctx.binary(account_state)
|
||||
},
|
||||
None => info!("new connection"),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
// process websocket messages
|
||||
let begin = Instant::now();
|
||||
debug!("msg: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => {
|
||||
self.hb = Instant::now();
|
||||
ctx.pong(&msg);
|
||||
}
|
||||
ws::Message::Pong(_) => {
|
||||
self.hb = Instant::now();
|
||||
}
|
||||
ws::Message::Text(_text) => (),
|
||||
ws::Message::Close(_) => {
|
||||
match self.account.as_ref() {
|
||||
Some(a) => info!("disconnected {:?}", a),
|
||||
None => info!("disconnected"),
|
||||
}
|
||||
ctx.stop();
|
||||
}
|
||||
ws::Message::Nop => (),
|
||||
ws::Message::Binary(bin) => {
|
||||
let db_connection = self.pool.get().expect("unable to get db connection");
|
||||
match receive(bin.to_vec(), &db_connection, ctx, begin, self.account.as_ref()) {
|
||||
Ok(reply) => {
|
||||
let response = to_vec(&reply)
|
||||
.expect("failed to serialize response");
|
||||
ctx.binary(response);
|
||||
},
|
||||
Err(e) => {
|
||||
let response = to_vec(&RpcErrorResponse { err: e.to_string() })
|
||||
.expect("failed to serialize error response");
|
||||
ctx.binary(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MnmlSocket {
|
||||
fn new(state: web::Data<State>, account: Option<Account>) -> MnmlSocket {
|
||||
// idk why this has to be cloned again
|
||||
// i guess because each socket is added as a new thread?
|
||||
MnmlSocket { hb: Instant::now(), pool: state.pool.clone(), account }
|
||||
}
|
||||
|
||||
// starts the keepalive interval once actor started
|
||||
fn hb(&self, ctx: &mut <MnmlSocket as Actor>::Context) {
|
||||
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
|
||||
if Instant::now().duration_since(act.hb) > CLIENT_TIMEOUT {
|
||||
info!("idle connection terminated");
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
|
||||
// don't try to send a ping
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ping("");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// idk how this stuff works
|
||||
// but the args extract what you need from the incoming requests
|
||||
// this grabs
|
||||
// the req obj itself which we need for cookies
|
||||
// the application state
|
||||
// and the websocket stream
|
||||
pub fn connect(r: HttpRequest, state: web::Data<State>, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||
let account: Option<Account> = match r.cookie("x-auth-token") {
|
||||
Some(t) => {
|
||||
let db = state.pool.get().or(Err(MnmlError::ServerError))?;
|
||||
let mut tx = db.transaction().or(Err(MnmlError::ServerError))?;
|
||||
match account_from_token(t.value().to_string(), &mut tx) {
|
||||
Ok(a) => Some(a),
|
||||
Err(_) => None,
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
ws::start(MnmlSocket::new(state, account), &r, stream)
|
||||
}
|
||||
101
server/test/subscription.json
Normal file
101
server/test/subscription.json
Normal file
@ -0,0 +1,101 @@
|
||||
{
|
||||
"object": {
|
||||
"id": "sub_FGmgBxTVX2xM7H",
|
||||
"object": "subscription",
|
||||
"application_fee_percent": null,
|
||||
"billing": "charge_automatically",
|
||||
"billing_cycle_anchor": 1560755931,
|
||||
"billing_thresholds": null,
|
||||
"cancel_at": null,
|
||||
"cancel_at_period_end": false,
|
||||
"canceled_at": null,
|
||||
"collection_method": "charge_automatically",
|
||||
"created": 1560755931,
|
||||
"current_period_end": 1563347931,
|
||||
"current_period_start": 1560755931,
|
||||
"customer": "cus_FGmgIHDimD5Tei",
|
||||
"days_until_due": null,
|
||||
"default_payment_method": "pm_1EmF78Gf8y65MteUQBHQU1po",
|
||||
"default_source": null,
|
||||
"default_tax_rates": [
|
||||
],
|
||||
"discount": null,
|
||||
"ended_at": null,
|
||||
"items": {
|
||||
"object": "list",
|
||||
"data": [
|
||||
{
|
||||
"id": "si_FGmg6F7DZAyU2Y",
|
||||
"object": "subscription_item",
|
||||
"billing_thresholds": null,
|
||||
"created": 1560755932,
|
||||
"metadata": {
|
||||
},
|
||||
"plan": {
|
||||
"id": "plan_FGmRwawcOJJ7Nv",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 1000,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1560755040,
|
||||
"currency": "aud",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"nickname": "basic",
|
||||
"product": "prod_FGmRFYmB700pM5",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"quantity": 1,
|
||||
"subscription": "sub_FGmgBxTVX2xM7H",
|
||||
"tax_rates": [
|
||||
]
|
||||
}
|
||||
],
|
||||
"has_more": false,
|
||||
"total_count": 1,
|
||||
"url": "/v1/subscription_items?subscription=sub_FGmgBxTVX2xM7H"
|
||||
},
|
||||
"latest_invoice": "in_1EmF7AGf8y65MteUvH8MCela",
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"plan": {
|
||||
"id": "plan_FGmRwawcOJJ7Nv",
|
||||
"object": "plan",
|
||||
"active": true,
|
||||
"aggregate_usage": null,
|
||||
"amount": 1000,
|
||||
"billing_scheme": "per_unit",
|
||||
"created": 1560755040,
|
||||
"currency": "aud",
|
||||
"interval": "month",
|
||||
"interval_count": 1,
|
||||
"livemode": false,
|
||||
"metadata": {
|
||||
},
|
||||
"nickname": "basic",
|
||||
"product": "prod_FGmRFYmB700pM5",
|
||||
"tiers": null,
|
||||
"tiers_mode": null,
|
||||
"transform_usage": null,
|
||||
"trial_period_days": null,
|
||||
"usage_type": "licensed"
|
||||
},
|
||||
"quantity": 1,
|
||||
"schedule": null,
|
||||
"start": 1560755931,
|
||||
"start_date": 1560755931,
|
||||
"status": "active",
|
||||
"tax_percent": null,
|
||||
"trial_end": null,
|
||||
"trial_start": null
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user