WIP - initial exploration of new stripe API

pull/5841/head
Lakin Wecker 2019-12-29 17:17:22 -07:00
parent 95f9eeae86
commit 297b7ece4b
9 changed files with 139 additions and 39 deletions

View File

@ -1,11 +1,12 @@
package controllers
import play.api.mvc._
import play.api.libs.json._
import lila.api.Context
import lila.app._
import lila.common.EmailAddress
import lila.plan.{ MonthlyCustomerInfo, OneTimeCustomerInfo, StripeCustomer }
import lila.plan.{ MonthlyCustomerInfo, OneTimeCustomerInfo, StripeCustomer, StripeReturnUrls }
import lila.user.{ User => UserModel }
import views._
@ -127,6 +128,38 @@ final class Plan(env: Env) extends LilaController(env) {
}
}
def badStripeSession[A: Writes](err: A) = BadRequest(jsonError(err))
def returnUrls = StripeReturnUrls(
s"${env.net.protocol}${env.net.domain}${routes.Plan.thanks}",
s"${env.net.protocol}${env.net.domain}${routes.Plan.index}"
)
def stripeSession = AuthBody { implicit ctx => me =>
import lila.plan.PlanApi.SyncResult._
import lila.plan.StripeClient._
XhrOrRedirectHome {
env.plan.api.sync(me) flatMap {
case Synced(Some(patron), _) => {
implicit val req = ctx.body
lila.plan.Checkout.form.bindFromRequest.fold(
err => badStripeSession(err.toString()).fuccess,
data =>
env.plan.api
.createSession(returnUrls, data, patron.stripe.map(_.customerId))
.map(session => Ok(Json.obj("id" -> session.id.value)) as JSON)
.recover({
case e: StripeException =>
logger.error("Plan.stripeSession", e)
badStripeSession("Stripe API call failed")
})
)
}
case _ => fuccess(BadRequest)
}
}
}
def payPalIpn = Action.async { implicit req =>
import lila.plan.Patron.PayPal
lila.plan.DataForm.ipn.bindFromRequest.fold(

View File

@ -22,7 +22,7 @@ object index {
title = title,
moreCss = cssTag("plan"),
moreJs = frag(
script(src := "https://checkout.stripe.com/checkout.js"),
script(src := "https://js.stripe.com/v3/"),
jsTag("checkout.js"),
embedJsUnsafe(s"""lichess.checkout("$stripePublicKey", "//${env.net.assetDomain.value}/assets/logo/lichess-stripe.png");""")
),
@ -97,12 +97,6 @@ object index {
attr("data-lifetime-cents") := lila.plan.Cents.lifetime.value
)(
raw(s"""
<form class="stripe_checkout none" action="${routes.Plan.charge}" method="POST">
<input type="hidden" class="token" name="token" />
<input type="hidden" class="email" name="email" />
<input type="hidden" class="amount" name="amount" />
<input type="hidden" class="freq" name="freq" />
</form>
<form class="paypal_checkout onetime none" action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<input type="hidden" name="custom" value="${~ctx.userId}">
<input type="hidden" name="amount" class="amount" value="">
@ -220,12 +214,13 @@ object index {
)
),
div(cls := "service")(
button(cls := "stripe button")("Credit Card"),
button(cls := "stripe button")("Stripe"),
button(cls := "paypal button")("PayPal")
)
)
)
),
p(id := "error")(),
p(cls := "small_team")(
"We are a small team, so your support makes a huge difference!"
),

View File

@ -155,6 +155,7 @@ GET /patron/list controllers.Plan.list
POST /patron/switch controllers.Plan.switch
POST /patron/cancel controllers.Plan.cancel
POST /patron/webhook controllers.Plan.webhook
POST /patron/stripe-session controllers.Plan.stripeSession
POST /patron/ipn controllers.Plan.payPalIpn
GET /features controllers.Plan.features

View File

@ -4,6 +4,7 @@ import play.api.libs.json._
private[plan] object JsonHandlers {
implicit val StripeSessionId = Reads.of[String].map(SessionId.apply)
implicit val StripeCustomerId = Reads.of[String].map(CustomerId.apply)
implicit val StripeChargeId = Reads.of[String].map(ChargeId.apply)
implicit val StripeCents = Reads.of[Int].map(Cents.apply)
@ -13,4 +14,5 @@ private[plan] object JsonHandlers {
implicit val StripeCustomerReads = Json.reads[StripeCustomer]
implicit val StripeChargeReads = Json.reads[StripeCharge]
implicit val StripeInvoiceReads = Json.reads[StripeInvoice]
implicit val StripeSessionReads = Json.reads[StripeSession]
}

View File

@ -10,6 +10,8 @@ import lila.db.dsl._
import lila.memo.CacheApi._
import lila.user.{ User, UserRepo }
case class StripeReturnUrls(successUrl: String, cancelUrl: String)
final class PlanApi(
stripeClient: StripeClient,
patronColl: Coll,
@ -465,6 +467,33 @@ final class PlanApi(
patronColl.one[Patron]($doc("stripe.customerId" -> id))
def userPatron(user: User): Fu[Option[Patron]] = patronColl.one[Patron]($id(user.id))
def createSession(
urls: StripeReturnUrls,
data: Checkout,
customerId: Option[CustomerId]
): Fu[StripeSession] =
data.freq match {
case Freq.Onetime => createOneTimeSession(urls, data, customerId)
case Freq.Monthly => createMonthlySession(urls, data, customerId)
}
def createOneTimeSession(
urls: StripeReturnUrls,
data: Checkout,
customerId: Option[CustomerId]
): Fu[StripeSession] =
stripeClient.createOneTimeSession(urls, data, customerId)
def createMonthlySession(
urls: StripeReturnUrls,
data: Checkout,
customerId: Option[CustomerId]
): Fu[StripeSession] =
getOrMakePlan(data.cents, data.freq) flatMap { plan =>
stripeClient.createMonthlySession(urls, plan, data, customerId)
}
}
object PlanApi {

View File

@ -14,6 +14,42 @@ final private class StripeClient(
import StripeClient._
import JsonHandlers._
def sessionArgs(urls: StripeReturnUrls, data: Checkout, customerId: Option[CustomerId]): List[(String, Any)] =
List(
"payment_method_types[]" -> "card",
"success_url" -> urls.successUrl,
"cancel_url" -> urls.cancelUrl,
) ++ customerId.fold[List[(String, Any)]](
List("customer_email" -> data.email)
)(id => List("customer" -> id.value))
def createOneTimeSession(urls: StripeReturnUrls, data: Checkout, customerId: Option[CustomerId]): Fu[StripeSession] = {
val args = sessionArgs(urls, data, customerId) ++ List(
"line_items[][name]" -> "One-time payment",
"line_items[][quantity]" -> 1,
"line_items[][amount]" -> data.amount.value,
"line_items[][currency]" -> "usd",
"line_items[][description]" -> {
if (data.amount.value > 25000) {
s"One month of patron status on lichess.org. <3 Your support makes a huge difference!",
} else {
s"Lifetime patron status on lichess.org. <3 Your support makes a huge difference!",
}
}
)
postOne[StripeSession]("checkout/sessions", args: _*)
}
def createMonthlySession(
urls: StripeReturnUrls,
plan: StripePlan,
data: Checkout,
customerId: Option[CustomerId]
): Fu[StripeSession] = {
val args = sessionArgs(urls, data, customerId) ++ List("subscription_data[items][][plan]" -> plan.id)
postOne[StripeSession]("checkout/sessions", args: _*)
}
def createCustomer(user: User, data: Checkout, plan: StripePlan): Fu[StripeCustomer] =
postOne[StripeCustomer](
"customers",

View File

@ -2,6 +2,7 @@ package lila.plan
import org.joda.time.DateTime
case class SessionId(value: String) extends AnyVal
case class CustomerId(value: String) extends AnyVal
case class ChargeId(value: String) extends AnyVal
@ -59,6 +60,8 @@ object StripePlan {
val defaultAmounts = List(5, 10, 20, 50).map(Usd.apply).map(_.cents)
}
case class StripeSession(id: SessionId)
case class StripeSubscription(
id: String,
plan: StripePlan,

View File

@ -65,45 +65,45 @@ lichess.checkout = function (publicKey, logo) {
$checkout.find('.service').html(lichess.spinnerHtml);
});
let showError = (error) => {
// TODO: make this show an actual error
console.log(error);
};
$checkout.find('button.stripe').on('click', function () {
var freq = getFreq(), usd, amount;
var freq = getFreq(), amount;
if (freq == 'lifetime') {
usd = lifetime.usd;
amount = lifetime.cents;
} else {
var $input = $checkout.find('group.amount input:checked');
usd = $input.data('usd');
amount = parseInt($input.data('amount'));
}
if (amount < min || amount > max) return;
$stripeForm.find('.amount').val(amount);
$stripeForm.find('.freq').val(freq);
var desc = freq === 'monthly' ? usd + '/month' : usd + ' one-time';
stripeHandler.open({
description: desc,
amount: amount,
panelLabel: '{{amount}}',
email: $checkout.data('email')
});
$.ajax({
url: "/patron/stripe-session",
method: "post",
data: {
email: $checkout.data('email'),
amount: amount,
freq: freq,
token: "asdfasdf" // TODO: remove this
}
}).then(
data => {
stripe.redirectToCheckout({
sessionId: data.id
}).then(function (result) {
showError(result.error.message);
});
},
err => {
showError(err);
}
);
});
var stripeHandler = StripeCheckout.configure({
key: publicKey,
name: 'lichess.org',
image: logo,
locale: 'auto',
allowRememberMe: false,
zipCode: false,
billingAddress: false,
currency: 'usd',
token: function (token) {
$checkout.find('.service').html(lichess.spinnerHtml);
$stripeForm.find('.token').val(token.id);
$stripeForm.find('.email').val(token.email);
$stripeForm.submit();
}
});
var stripe = Stripe(publicKey);
// Close Checkout on page navigation:
$(window).on('popstate', function () {
stripeHandler.close();

View File

@ -148,10 +148,11 @@
.service button {
flex: 1 1 auto;
font-weight: normal;
}
.service button:first-child {
margin-right: 1em;
}
.service button:last-child {
margin-right: 0em;
}
}
.small_team {