stripe WIP

pull/1979/head
Thibault Duplessis 2016-06-06 11:36:21 +02:00
parent 7a8a1e3827
commit a730c40be4
22 changed files with 303 additions and 16 deletions

View File

@ -151,4 +151,5 @@ object Env {
def explorer = lila.explorer.Env.current
def fishnet = lila.fishnet.Env.current
def study = lila.study.Env.current
def stripe = lila.stripe.Env.current
}

View File

@ -71,12 +71,6 @@ object Main extends LilaController {
}
}
def features = Open { implicit ctx =>
fuccess {
html.site.features()
}
}
def mobile = Open { implicit ctx =>
OptionOk(Prismic getBookmark "mobile-apk") {
case (doc, resolver) => html.mobile.home(doc, resolver)

View File

@ -0,0 +1,41 @@
package controllers
import play.api.libs.json._
import play.api.mvc._, Results._
import lila.app._
import views._
object Plan extends LilaController {
def index = Open { implicit ctx =>
(ctx.userId ?? lila.user.UserRepo.email) map { myEmail =>
html.plan.index(
myEmail = myEmail,
stripePublicKey = Env.stripe.publicKey)
}
}
def features = Open { implicit ctx =>
fuccess {
html.plan.features()
}
}
def charge = AuthBody { implicit ctx =>
me =>
implicit val req = ctx.body
lila.stripe.Checkout.form.bindFromRequest.fold(
err => BadRequest(html.plan.badCheckout(err)).fuccess,
data => Env.stripe.api.checkout(me, data) map { res =>
Ok(html.plan.thanks())
}
)
}
def thanks = Open { implicit ctx =>
fuccess {
html.plan.thanks()
}
}
}

View File

@ -0,0 +1,6 @@
@(form: Form[_])(implicit ctx: Context)
@site.message("Payment can not be processed.") {
<p>The payment didn't go through. Your card was not charged.</p>
<p>@errMsg(form)</p>
}

View File

@ -36,7 +36,7 @@ title = title,
moreCss = cssTag("features.css"),
openGraph = lila.app.ui.OpenGraph(
title = title,
url = s"$netBaseUrl${routes.Main.features.url}",
url = s"$netBaseUrl${routes.Plan.features.url}",
description = "All of lichess features are free for all and forever. We do it for chess!").some) {
<div class="content_box features">

View File

@ -0,0 +1,42 @@
@(stripePublicKey: String, myEmail: Option[String])(implicit ctx: Context)
@title = @{"Become a lichess hero"}
@moreJs = {
<script src="https://checkout.stripe.com/checkout.js"></script>
@jsTag("checkout.js")
@embedJs {
lichess.checkout("@stripePublicKey");
}
}
@site.layout(
title = title,
moreCss = cssTag("plan.css"),
moreJs = moreJs,
openGraph = lila.app.ui.OpenGraph(
title = title,
url = s"$netBaseUrl${routes.Plan.index.url}",
description = "All of lichess features are free for all and forever. We do it for chess!").some) {
<div class="content_box plan">
<h1>@title</h1>
<p>Your plan: @ctx.me.flatMap(_.plan).getOrElse("none")</p>
@ctx.me.map { me =>
<form class="checkout_form none" action="@routes.Plan.charge" method="POST">
<input type="hidden" class="token" name="token" />
<input type="hidden" class="cents" name="cents" />
</form>
<div class="checkout_buttons">
@lila.stripe.Plan.all.map { p =>
<button class="button checkout @p.id"
data-description="$@p.usd/month"
data-cents="@p.cents"
data-panel-label="Subscribe {{amount}}"
data-email="@myEmail"
>$@p.usd</button>
}
</div>
}
</div>
}

View File

@ -0,0 +1,13 @@
@()(implicit ctx: Context)
@title = @{"Thank you!"}
@site.layout(
title = title,
moreCss = cssTag("plan.css")) {
<div class="content_box plan">
<h1>@title</h1>
<p>Your plan: @ctx.me.flatMap(_.plan).getOrElse("none")</p>
</div>
}

View File

@ -598,6 +598,13 @@ slack {
}
domain = ${net.domain}
}
stripe {
keys {
endpoint="https://api.stripe.com/v1"
public="pk_test_31E5TIuGtMs4ojhzMIMu8oIc"
secret="sk_test_erAQMvv5cF90WXUFlkv7Tke0"
}
}
hub {
actor {
game {

View File

@ -132,7 +132,10 @@ GET /study/$id<\w{8}>.pgn controllers.Study.pgn(id: String)
POST /study/$id<\w{8}>/delete controllers.Study.delete(id: String)
GET /study/$id<\w{8}>/$chapterId<\w{8}> controllers.Study.chapter(id: String, chapterId: String)
GET /features controllers.Main.features
GET /plan controllers.Plan.index
POST /plan/charge controllers.Plan.charge
GET /plan/thanks controllers.Plan.thanks
GET /features controllers.Plan.features
# Round
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")

View File

@ -1,7 +1,7 @@
package lila.slack
import play.api.libs.json._
import play.api.libs.ws.{ WS, WSAuthScheme }
import play.api.libs.ws.WS
import play.api.Play.current
import lila.common.PimpedJson._

View File

@ -0,0 +1,17 @@
package lila.stripe
import play.api.data._
import play.api.data.Forms._
case class Checkout(token: String, cents: Int) {
def source = Source(token)
}
object Checkout {
val form = Form(mapping(
"token" -> nonEmptyText,
"cents" -> number(min = 500)
)(Checkout.apply)(Checkout.unapply))
}

View File

@ -0,0 +1,26 @@
package lila.stripe
import akka.actor._
import com.typesafe.config.Config
import lila.common.PimpedConfig._
final class Env(config: Config) {
val publicKey = config getString "keys.public"
private lazy val client = new StripeClient(StripeClient.Config(
endpoint = config getString "endpoint",
publicKey = publicKey,
privateKey = config getString "keys.private"))
lazy val api = new StripeApi(client)
}
object Env {
lazy val current: Env = "stripe" boot new Env(
// system = lila.common.PlayApp.system,
// getLightUser = lila.user.Env.current.lightUser,
config = lila.common.PlayApp loadConfig "stripe")
}

View File

@ -0,0 +1,12 @@
package lila.stripe
import lila.user.User
final class StripeApi(client: StripeClient) {
def checkout(user: User, data: Checkout): Funit =
???
// (user.plan.customerId ?? client.customerExists) flatMap {
// case false => client.createCustomer(user, data.source
// }
}

View File

@ -0,0 +1,40 @@
package lila.stripe
import play.api.libs.json._
import play.api.libs.ws.WS
import play.api.Play.current
import lila.common.PimpedJson._
import lila.user.{ User, UserRepo }
private final class StripeClient(config: StripeClient.Config) {
def createCustomer(user: User, source: Source, plan: Plan): Fu[CustomerId] =
UserRepo email user.id flatMap { email =>
WS.url(config url "customers").post(Json.obj(
"source" -> source.value,
"plan" -> plan.id,
"email" -> email,
"description" -> user.titleName,
"metadata" -> Json.obj("id" -> user.id)
)).flatMap {
case res if res.status == 200 => fuccess(CustomerId((res.json \ "id").as[String]))
case res => fufail(s"[stripe] createCustomer ${res.status} ${res.body}")
}
}
def customerExists(id: CustomerId): Fu[Boolean] =
WS.url(config url s"customers/$id").get() flatMap {
case res if res.status == 200 => fuccess(true)
case res if res.status == 404 => fuccess(false)
case res => fufail(s"[stripe] customerExists ${res.status} ${res.body}")
}
}
private object StripeClient {
case class Config(endpoint: String, publicKey: String, privateKey: String) {
def url(end: String) = s"$endpoint/$end"
}
}

View File

@ -0,0 +1,19 @@
package lila.stripe
case class Source(value: String) extends AnyVal
case class CustomerId(value: String) extends AnyVal
sealed abstract class Plan(val id: String, val usd: Int) {
def cents = usd * 100
}
object Plan {
case object Monthly5 extends Plan("monthly_5", 5)
case object Monthly10 extends Plan("monthly_10", 10)
case object Monthly20 extends Plan("monthly_20", 20)
case object Monthly50 extends Plan("monthly_50", 50)
case object Monthly100 extends Plan("monthly_100", 100)
val all = List(Monthly5, Monthly10, Monthly20, Monthly50, Monthly100)
}

View File

@ -0,0 +1,3 @@
package lila
package object stripe extends PackageObject with WithPlay

View File

@ -0,0 +1,19 @@
package lila.user
import org.joda.time.DateTime
case class Plan(
months: Int,
activeUntil: Option[DateTime],
customerId: String) {
def active = activeUntil ?? DateTime.now.isBefore
def level = months + 1
}
object Plan {
import lila.db.dsl._
private[user] val planBSONHandler = reactivemongo.bson.Macros.handler[Plan]
}

View File

@ -1,7 +1,5 @@
package lila.user
import scala._
case class Profile(
country: Option[String] = None,
location: Option[String] = None,

View File

@ -5,8 +5,8 @@ import scala.concurrent.duration._
import lila.common.LightUser
import chess.Speed
import org.joda.time.DateTime
import lila.rating.PerfType
import org.joda.time.DateTime
case class User(
id: String,
@ -26,7 +26,8 @@ case class User(
createdAt: DateTime,
seenAt: Option[DateTime],
kid: Boolean,
lang: Option[String]) extends Ordered[User] {
lang: Option[String],
plan: Option[Plan] = None) extends Ordered[User] {
override def equals(other: Any) = other match {
case u: User => id == u.id
@ -157,6 +158,7 @@ object User {
val email = "email"
val mustConfirmEmail = "mustConfirmEmail"
val colorIt = "colorIt"
val plan = "plan"
}
import lila.db.BSON
@ -168,6 +170,7 @@ object User {
private implicit def countHandler = Count.countBSONHandler
private implicit def profileHandler = Profile.profileBSONHandler
private implicit def perfsHandler = Perfs.perfsBSONHandler
private implicit def planHandler = Plan.planBSONHandler
def reads(r: BSON.Reader): User = User(
id = r str id,
@ -187,7 +190,8 @@ object User {
seenAt = r dateO seenAt,
kid = r boolD kid,
lang = r strO lang,
title = r strO title)
title = r strO title,
plan = r.getO[Plan](plan))
def writes(w: BSON.Writer, o: User) = BSONDocument(
id -> o.id,
@ -207,6 +211,7 @@ object User {
seenAt -> o.seenAt,
kid -> w.boolO(o.kid),
lang -> o.lang,
title -> o.title)
title -> o.title,
plan -> o.plan)
}
}

View File

@ -60,7 +60,7 @@ object ApplicationBuild extends Build {
evaluation, chat, puzzle, tv, coordinate, blog, donation, qa,
history, worldMap, opening, video, shutup, push,
playban, insight, perfStat, slack, quote, challenge,
study, fishnet, explorer)
study, fishnet, explorer, stripe)
lazy val moduleRefs = modules map projectToRef
lazy val moduleCPDeps = moduleRefs map { new sbt.ClasspathDependency(_, None) }
@ -254,6 +254,10 @@ object ApplicationBuild extends Build {
libraryDependencies ++= provided(play.api, RM)
)
lazy val stripe = project("stripe", Seq(common, user)).settings(
libraryDependencies ++= provided(play.api, RM)
)
lazy val relation = project("relation", Seq(common, db, memo, hub, user, game, pref)).settings(
libraryDependencies ++= provided(play.api, RM)
)

View File

@ -0,0 +1,37 @@
lichess.checkout = function(publicKey) {
var $form = $('.checkout_form');
var handler = StripeCheckout.configure({
key: publicKey,
name: 'lichess.org',
image: 'https://s3.amazonaws.com/stripe-uploads/acct_18J612Fj1uHKxNqMmerchant-icon-1465200826114-logo.512.png',
locale: 'auto',
allowRememberMe: false,
zipCode: false,
billingAddress: false,
currency: 'usd',
token: function(token) {
$('.checkout_buttons').html(lichess.spinnerHtml);
$form.find('.token').val(token.id);
$form.submit();
}
});
$('button.checkout').on('click', function(e) {
var amount = parseInt($(this).data('cents'));
$form.find('.cents').val(amount);
handler.open({
description: $(this).data('description'),
amount: amount,
panelLabel: $(this).data('panel-label'),
email: $(this).data('email')
});
e.preventDefault();
});
// Close Checkout on page navigation:
$(window).on('popstate', function() {
handler.close();
});
};

View File