WIP - initial exploration of new stripe API
parent
95f9eeae86
commit
297b7ece4b
|
@ -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(
|
||||
|
|
|
@ -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!"
|
||||
),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue