stripe WIP
parent
7a8a1e3827
commit
a730c40be4
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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">
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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._
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
// }
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package lila
|
||||
|
||||
package object stripe extends PackageObject with WithPlay
|
|
@ -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]
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package lila.user
|
||||
|
||||
import scala._
|
||||
|
||||
case class Profile(
|
||||
country: Option[String] = None,
|
||||
location: Option[String] = None,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
};
|
Loading…
Reference in New Issue