Skip to content

Commit

Permalink
First instance of being able to collect card info and make a payment
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalin-Rudnicki committed Jan 22, 2024
1 parent 38e4e38 commit cdfff5b
Show file tree
Hide file tree
Showing 40 changed files with 751 additions and 15 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Release

Check failure on line 1 in .github/workflows/release.yml

View workflow job for this annotation

GitHub Actions / publish

.github/workflows/release.yml#L1

This run was manually canceled.
on:
push:
branches: [master, main]
branches: [master, main, dev]
tags: ["*"]
jobs:
publish:
Expand Down
24 changes: 24 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ lazy val `harness-root` =
`harness-pk`.jvm,
`harness-email`.jvm,
`harness-email`.js,
`harness-payments`.jvm,
`harness-payments`.js,
`harness-docker`,
`harness-docker-sql`,
`harness-docker-kafka`,
Expand Down Expand Up @@ -318,6 +320,27 @@ lazy val `harness-email` =
`harness-zio` % testAndCompile,
)

lazy val `harness-payments` =
crossProject(JSPlatform, JVMPlatform)
.in(file("harness-payments"))
.settings(
name := "harness-payments",
publishSettings,
miscSettings,
testSettings,
Test / fork := true,
)
.jvmSettings(
libraryDependencies ++= Seq(
"com.stripe" % "stripe-java" % "24.11.0",
),
)
.jsConfigure(_.dependsOn(`harness-web-ui` % testAndCompile))
.dependsOn(
`harness-email` % testAndCompile,
`harness-pk` % testAndCompile,
)

lazy val `harness-web` =
crossProject(JSPlatform, JVMPlatform)
.in(file("harness-web"))
Expand Down Expand Up @@ -533,6 +556,7 @@ lazy val `harness-web-app-template--model` =
.dependsOn(
`harness-web` % testAndCompile,
`harness-email` % testAndCompile,
`harness-payments` % testAndCompile,
)

lazy val `harness-web-app-template--db-model` =
Expand Down
Binary file modified harness-archive/api/src/main/resources/res/favicon.ico
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ object ServerMain {
EmailClient.liveLayer,
HConfig.readLayer[EmailService.Config]("email", "service"),
EmailService.liveLayer,
StaleDataCleanser.live(1.minute, 1.minute, 1.minute, 5.minutes, 15.minutes),
StaleDataCleanser.live(1.minute, 1.minute, 1.minute, 5.minutes, 15.minutes, 1.hour, 1.hour, 6.hours),
)

val storageLayer: URLayer[JDBCConnection, StorageEnv] =
Expand Down
Binary file modified harness-archive/res/favicon.ico
Binary file not shown.
115 changes: 115 additions & 0 deletions harness-payments/js/src/main/scala/harness/payments/PaymentsUI.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package harness.payments

import harness.core.*
import harness.http.client.HttpClient
import harness.payments.facades.*
import harness.webUI.*
import harness.webUI.style.{given, *}
import harness.webUI.vdom.{given, *}
import harness.webUI.widgets.*
import harness.zio.*
import org.scalajs.dom.{console, document, window}
import scala.util.matching.Regex
import zio.*

object PaymentsUI {

private val loader: Promise[Nothing, Unit] =
Unsafe.unsafely { Runtime.default.unsafe.run { Promise.make[Nothing, Unit] }.getOrThrow() }

val addStripeSrc: HRIO[Logger, Unit] =
Logger.log.debug("Loading stripe src") *>
ZIO
.hAttempt {
val elem = document.createElement("script")
elem.setAttribute("src", "https://js.stripe.com/v3/")
elem.asInstanceOf[scalajs.js.Dynamic].onload = { (_: Any) =>
Unsafe.unsafely { Runtime.default.unsafe.run { loader.done(Exit.Success(())) }.getOrThrow() }
}
document.head.append(elem)
}
.mapError(HError.SystemFailure("Unable to add stripe src", _))

val awaitStripeSrc: UIO[Unit] =
loader.await

private val elementName: String = "payment-element"

def createAndMountElements(payments: PaymentEnv, currency: Currency): HTask[Unit] =
ZIO
.hAttempt {
val options = ElementsOptions("setup", currency.toString.toLowerCase)
val elements = payments.stripe.elements(options)
val paymentElement = elements.create("payment")
paymentElement.mount(s"#$elementName")
elements
}
.mapError(HError.SystemFailure("Unable to create & mount stripe payments elements", _))
.flatMap(payments.elementsRef.set)

private implicit class MaybeErrorOps(maybeError: MaybeErrorResponse) {
def toZIO: HTask[Unit] =
maybeError.error.toOption match {
case Some(error) => ZIO.fail(HError.UserError(s"[TODO - message]: ${error.message}"))
case None => ZIO.unit
}
}

final class PaymentEnv private (
private[PaymentsUI] val stripe: Stripe,
private[PaymentsUI] val elementsRef: Ref[Elements],
) {

val elements: UIO[Elements] =
elementsRef.get.tap { elements => ZIO.dieMessage("elements not populated").when(elements == null) }

}
object PaymentEnv {

def create(apiKey: String): UIO[PaymentEnv] =
Ref.make[Elements](null).map { new PaymentEnv(Stripe(apiKey), _) }

}

private val getUrlRegex: Regex =
s"^(https?://[^/]+)".r

def paymentForm(
createIntent: HRIO[HttpClient.ClientT & Logger & Telemetry, ClientSecret],
paymentsEnv: PaymentEnv,
redirectUrl: Url,
): CModifier =
PModifier.builder
.withAction[Submit] { rh =>
form(
id := "payment-form",
div(
id := elementName,
),
FormWidgets
.submitButton(
"Submit",
)
.flatMapActionZM(_ => ZIO.succeed(Nil)), // otherwise we submit twice, consider making normal button?
onSubmit := { event =>
event.preventDefault()
rh.raiseAction(Submit)
},
)
}
.flatMapActionZM { _ =>
for {
elements <- paymentsEnv.elements
_ <- ZIO.fromPromiseJS { elements.submit() }.mapError(HError.fromThrowable).flatMap(_.toZIO)
clientSecret <- createIntent
_ <- Logger.log.info(s"clientSecret: $clientSecret")
host = s"${window.location.protocol}//${window.location.host}"
_ <-
ZIO
.fromPromiseJS { paymentsEnv.stripe.confirmSetup(new ConfirmSetupOptions(elements, clientSecret.value, new ConfirmParams(s"$host${redirectUrl.toString}"))) }
.mapError(HError.fromThrowable)
.flatMap(_.toZIO)
} yield Nil
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package harness.payments.facades

import scala.scalajs.js

final class ConfirmParams(
val `return_url`: String,
) extends js.Object
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package harness.payments.facades

import scala.scalajs.js

final class ConfirmSetupOptions(
val elements: Elements,
val clientSecret: String,
val confirmParams: ConfirmParams
) extends js.Object
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package harness.payments.facades

import scalajs.js

@js.native
trait Elements extends js.Object {
def create(str: String): PaymentElement
def submit(): js.Promise[MaybeErrorResponse]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package harness.payments.facades

import scalajs.js

final class ElementsOptions(
val mode: String,
val currency: String,
// TODO (KR) : appearance
) extends js.Object
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package harness.payments.facades

import scala.scalajs.js

@js.native
trait Err extends js.Object {
val code: String
val message: String
val `type`: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package harness.payments.facades

import scala.scalajs.js

@js.native
trait MaybeErrorResponse extends js.Object {
def error: js.UndefOr[Err]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package harness.payments.facades

import scala.scalajs.js

@js.native
trait PaymentElement extends js.Object {
def mount(str: String): js.Any
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package harness.payments.facades

import scalajs.js
import scalajs.js.annotation.*

@js.native
@JSGlobal
final class Stripe(apiKey: String) extends js.Any {
def elements(options: ElementsOptions): Elements = js.native
def confirmSetup(confirmSetupOptions: ConfirmSetupOptions): js.Promise[MaybeErrorResponse] = js.native
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package harness.payments

final case class CardInfo(
cardNumber: String,
expMonth: Int,
expYear: Int,
cvc: String
)
11 changes: 11 additions & 0 deletions harness-payments/jvm/src/main/scala/harness/payments/Charge.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package harness.payments

import harness.email.*

final case class Charge(
amountInCents: Long,
currency: Currency,
description: String,
source: PaymentSourceId,
email: Option[EmailAddress],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package harness.payments

import harness.email.*

final case class CreateCustomer(
name: Option[String],
email: Option[EmailAddress],
)
12 changes: 12 additions & 0 deletions harness-payments/jvm/src/main/scala/harness/payments/JvmIds.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package harness.payments

import harness.pk.StringId

type ChargeId = ChargeId.Id
object ChargeId extends StringId

type PaymentId = PaymentId.Id
object PaymentId extends StringId

type PaymentSourceId = PaymentSourceId.Id
object PaymentSourceId extends StringId
12 changes: 12 additions & 0 deletions harness-payments/jvm/src/main/scala/harness/payments/Payment.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package harness.payments

import harness.email.*

final case class Payment(
customerId: CustomerId,
paymentMethodId: PaymentMethodId,
amountInCents: Long,
currency: Currency,
description: String,
email: Option[EmailAddress]
)
Loading

0 comments on commit cdfff5b

Please sign in to comment.