stripe WIP
parent
7a8a1e3827
commit
a730c40be4
|
@ -151,4 +151,5 @@ object Env {
|
||||||
def explorer = lila.explorer.Env.current
|
def explorer = lila.explorer.Env.current
|
||||||
def fishnet = lila.fishnet.Env.current
|
def fishnet = lila.fishnet.Env.current
|
||||||
def study = lila.study.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 =>
|
def mobile = Open { implicit ctx =>
|
||||||
OptionOk(Prismic getBookmark "mobile-apk") {
|
OptionOk(Prismic getBookmark "mobile-apk") {
|
||||||
case (doc, resolver) => html.mobile.home(doc, resolver)
|
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"),
|
moreCss = cssTag("features.css"),
|
||||||
openGraph = lila.app.ui.OpenGraph(
|
openGraph = lila.app.ui.OpenGraph(
|
||||||
title = title,
|
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) {
|
description = "All of lichess features are free for all and forever. We do it for chess!").some) {
|
||||||
|
|
||||||
<div class="content_box features">
|
<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}
|
domain = ${net.domain}
|
||||||
}
|
}
|
||||||
|
stripe {
|
||||||
|
keys {
|
||||||
|
endpoint="https://api.stripe.com/v1"
|
||||||
|
public="pk_test_31E5TIuGtMs4ojhzMIMu8oIc"
|
||||||
|
secret="sk_test_erAQMvv5cF90WXUFlkv7Tke0"
|
||||||
|
}
|
||||||
|
}
|
||||||
hub {
|
hub {
|
||||||
actor {
|
actor {
|
||||||
game {
|
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)
|
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 /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
|
# Round
|
||||||
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")
|
GET /$gameId<\w{8}> controllers.Round.watcher(gameId: String, color: String = "white")
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package lila.slack
|
package lila.slack
|
||||||
|
|
||||||
import play.api.libs.json._
|
import play.api.libs.json._
|
||||||
import play.api.libs.ws.{ WS, WSAuthScheme }
|
import play.api.libs.ws.WS
|
||||||
import play.api.Play.current
|
import play.api.Play.current
|
||||||
|
|
||||||
import lila.common.PimpedJson._
|
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
|
package lila.user
|
||||||
|
|
||||||
import scala._
|
|
||||||
|
|
||||||
case class Profile(
|
case class Profile(
|
||||||
country: Option[String] = None,
|
country: Option[String] = None,
|
||||||
location: Option[String] = None,
|
location: Option[String] = None,
|
||||||
|
|
|
@ -5,8 +5,8 @@ import scala.concurrent.duration._
|
||||||
import lila.common.LightUser
|
import lila.common.LightUser
|
||||||
|
|
||||||
import chess.Speed
|
import chess.Speed
|
||||||
import org.joda.time.DateTime
|
|
||||||
import lila.rating.PerfType
|
import lila.rating.PerfType
|
||||||
|
import org.joda.time.DateTime
|
||||||
|
|
||||||
case class User(
|
case class User(
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -26,7 +26,8 @@ case class User(
|
||||||
createdAt: DateTime,
|
createdAt: DateTime,
|
||||||
seenAt: Option[DateTime],
|
seenAt: Option[DateTime],
|
||||||
kid: Boolean,
|
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 {
|
override def equals(other: Any) = other match {
|
||||||
case u: User => id == u.id
|
case u: User => id == u.id
|
||||||
|
@ -157,6 +158,7 @@ object User {
|
||||||
val email = "email"
|
val email = "email"
|
||||||
val mustConfirmEmail = "mustConfirmEmail"
|
val mustConfirmEmail = "mustConfirmEmail"
|
||||||
val colorIt = "colorIt"
|
val colorIt = "colorIt"
|
||||||
|
val plan = "plan"
|
||||||
}
|
}
|
||||||
|
|
||||||
import lila.db.BSON
|
import lila.db.BSON
|
||||||
|
@ -168,6 +170,7 @@ object User {
|
||||||
private implicit def countHandler = Count.countBSONHandler
|
private implicit def countHandler = Count.countBSONHandler
|
||||||
private implicit def profileHandler = Profile.profileBSONHandler
|
private implicit def profileHandler = Profile.profileBSONHandler
|
||||||
private implicit def perfsHandler = Perfs.perfsBSONHandler
|
private implicit def perfsHandler = Perfs.perfsBSONHandler
|
||||||
|
private implicit def planHandler = Plan.planBSONHandler
|
||||||
|
|
||||||
def reads(r: BSON.Reader): User = User(
|
def reads(r: BSON.Reader): User = User(
|
||||||
id = r str id,
|
id = r str id,
|
||||||
|
@ -187,7 +190,8 @@ object User {
|
||||||
seenAt = r dateO seenAt,
|
seenAt = r dateO seenAt,
|
||||||
kid = r boolD kid,
|
kid = r boolD kid,
|
||||||
lang = r strO lang,
|
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(
|
def writes(w: BSON.Writer, o: User) = BSONDocument(
|
||||||
id -> o.id,
|
id -> o.id,
|
||||||
|
@ -207,6 +211,7 @@ object User {
|
||||||
seenAt -> o.seenAt,
|
seenAt -> o.seenAt,
|
||||||
kid -> w.boolO(o.kid),
|
kid -> w.boolO(o.kid),
|
||||||
lang -> o.lang,
|
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,
|
evaluation, chat, puzzle, tv, coordinate, blog, donation, qa,
|
||||||
history, worldMap, opening, video, shutup, push,
|
history, worldMap, opening, video, shutup, push,
|
||||||
playban, insight, perfStat, slack, quote, challenge,
|
playban, insight, perfStat, slack, quote, challenge,
|
||||||
study, fishnet, explorer)
|
study, fishnet, explorer, stripe)
|
||||||
|
|
||||||
lazy val moduleRefs = modules map projectToRef
|
lazy val moduleRefs = modules map projectToRef
|
||||||
lazy val moduleCPDeps = moduleRefs map { new sbt.ClasspathDependency(_, None) }
|
lazy val moduleCPDeps = moduleRefs map { new sbt.ClasspathDependency(_, None) }
|
||||||
|
@ -254,6 +254,10 @@ object ApplicationBuild extends Build {
|
||||||
libraryDependencies ++= provided(play.api, RM)
|
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(
|
lazy val relation = project("relation", Seq(common, db, memo, hub, user, game, pref)).settings(
|
||||||
libraryDependencies ++= provided(play.api, RM)
|
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