Skip to content

Commit

Permalink
Adyen Dropin Checkout (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
bbengfort authored Jul 24, 2024
1 parent ae91a5a commit 605f553
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 9 deletions.
10 changes: 9 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,12 @@ EXCHEQUER_MAINTENANCE=false
EXCHEQUER_MODE=debug
EXCHEQUER_LOG_LEVEL=debug
EXCHEQUER_CONSOLE_LOG=true
EXCHEQUER_BIND_ADDR=:8204
EXCHEQUER_BIND_ADDR=:8204
EXCHEQUER_ORIGIN=http://localhost:8204

EXCHEQUER_ADYEN_MERCHANT_ACCOUNT=
EXCHEQUER_ADYEN_API_KEY=
EXCHEQUER_ADYEN_CLIENT_KEY=
EXCHEQUER_ADYEN_LIVE=false
EXCHEQUER_ADYEN_WEBHOOK_USE_BASIC_AUTH=false
EXCHEQUER_ADYEN_WEBHOOK_VERIFY_HMAC=false
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
Expand All @@ -49,8 +50,10 @@ require (
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
Expand Down Expand Up @@ -1129,6 +1130,8 @@ golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -1409,6 +1412,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
Expand Down
10 changes: 6 additions & 4 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ type Config struct {
}

type AdyenConfig struct {
APIKey string `split_words:"true" required:"true" desc:"api key for adyen payments api access"`
Live bool `default:"false" desc:"set to true to enable live payments and access to the live environment"`
URLPrefix string `split_words:"true" desc:"the live endpoint url prefix used to access the live environment"`
Webhook AdyenWebhookConfig
MerchantAccount string `split_words:"true" required:"true" desc:"the merchant account name configured in adyen"`
APIKey string `split_words:"true" required:"true" desc:"api key for adyen payments api access"`
ClientKey string `split_words:"true" required:"true" desc:"client key for adyen web drop-in"`
Live bool `default:"false" desc:"set to true to enable live payments and access to the live environment"`
URLPrefix string `split_words:"true" desc:"the live endpoint url prefix used to access the live environment"`
Webhook AdyenWebhookConfig
}

type AdyenWebhookConfig struct {
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ var testEnv = map[string]string{
"EXCHEQUER_CONSOLE_LOG": "true",
"EXCHEQUER_BIND_ADDR": ":9000",
"EXCHEQUER_ORIGIN": "http://localhost:9000",
"EXCHEQUER_ADYEN_MERCHANT_ACCOUNT": "MyCompanyECOM",
"EXCHEQUER_ADYEN_API_KEY": "my api key",
"EXCHEQUER_ADYEN_CLIENT_KEY": "my client key",
"EXCHEQUER_ADYEN_LIVE": "true",
"EXCHEQUER_ADYEN_URL_PREFIX": "1797a841fbb37ca7-AdyenDemo",
"EXCHEQUER_ADYEN_WEBHOOK_USE_BASIC_AUTH": "true",
Expand All @@ -42,7 +44,9 @@ func TestConfig(t *testing.T) {
require.True(t, conf.ConsoleLog)
require.Equal(t, testEnv["EXCHEQUER_BIND_ADDR"], conf.BindAddr)
require.Equal(t, testEnv["EXCHEQUER_ORIGIN"], conf.Origin)
require.Equal(t, testEnv["EXCHEQUER_ADYEN_MERCHANT_ACCOUNT"], conf.Adyen.MerchantAccount)
require.Equal(t, testEnv["EXCHEQUER_ADYEN_API_KEY"], conf.Adyen.APIKey)
require.Equal(t, testEnv["EXCHEQUER_ADYEN_CLIENT_KEY"], conf.Adyen.ClientKey)
require.True(t, conf.Adyen.Live)
require.Equal(t, testEnv["EXCHEQUER_ADYEN_URL_PREFIX"], conf.Adyen.URLPrefix)
require.True(t, conf.Adyen.Webhook.UseBasicAuth)
Expand Down
30 changes: 30 additions & 0 deletions pkg/exchequer/adyen.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ import (
"net/http"
"strings"

"github.com/adyen/adyen-go-api-library/v11/src/adyen"
"github.com/adyen/adyen-go-api-library/v11/src/common"
"github.com/adyen/adyen-go-api-library/v11/src/webhook"
"github.com/gin-gonic/gin"
"github.com/rotationalio/exchequer/pkg/api/v1"
"github.com/rotationalio/exchequer/pkg/config"
"github.com/rs/zerolog/log"
)

//===========================================================================
// Adyen Related Middleware
//===========================================================================

func (s *Server) AdyenWebhookAuth() gin.HandlerFunc {
if s.conf.Adyen.Webhook.UseBasicAuth {
return gin.BasicAuth(gin.Accounts{
Expand All @@ -28,6 +35,10 @@ func (s *Server) AdyenWebhookAuth() gin.HandlerFunc {
}
}

//===========================================================================
// Adyen Handlers
//===========================================================================

func (s *Server) AdyenPaymentsWebhook(c *gin.Context) {
var (
err error
Expand Down Expand Up @@ -74,6 +85,10 @@ func (s *Server) AdyenPaymentsWebhook(c *gin.Context) {
c.Status(http.StatusAccepted)
}

//===========================================================================
// Adyen Helper Methods
//===========================================================================

func VerifyAdyenHMAC(payload *webhook.NotificationRequestItem, secret string) (err error) {
// Step 1: Extract the HMAC signature to verify from the additonal data.
if payload.AdditionalData == nil {
Expand Down Expand Up @@ -122,3 +137,18 @@ func VerifyAdyenHMAC(payload *webhook.NotificationRequestItem, secret string) (e
}
return nil
}

func CreateAdyenClient(conf config.AdyenConfig) *adyen.APIClient {
if conf.Live {
return adyen.NewClient(&common.Config{
ApiKey: conf.APIKey,
Environment: common.LiveEnv,
LiveEndpointURLPrefix: conf.URLPrefix,
})
}

return adyen.NewClient(&common.Config{
ApiKey: conf.APIKey,
Environment: common.TestEnv,
})
}
60 changes: 60 additions & 0 deletions pkg/exchequer/checkout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package exchequer

import (
"net/http"
"net/url"

"github.com/adyen/adyen-go-api-library/v11/src/checkout"
"github.com/adyen/adyen-go-api-library/v11/src/common"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/rotationalio/exchequer/pkg/api/v1"
"github.com/rotationalio/exchequer/pkg/ulids"
)

func (s *Server) Checkout(c *gin.Context) {
origin, _ := url.Parse(s.conf.Origin)

// Create the checkout request object
key := ulids.New()
returnURL := origin.JoinPath("/checkout/complete")
returnURL.RawQuery = url.Values{"session": []string{key.String()}}.Encode()

sessionRequest := checkout.CreateCheckoutSessionRequest{
Reference: "INV-0001",
Amount: checkout.Amount{
Currency: "USD",
Value: 1000,
},
MerchantAccount: s.conf.Adyen.MerchantAccount,
CountryCode: common.PtrString("US"),
ReturnUrl: returnURL.String(),
}

// Send the request to Adyen
service := s.adyen.Checkout()
req := service.PaymentsApi.SessionsInput().IdempotencyKey(key.String()).CreateCheckoutSessionRequest(sessionRequest)
rep, _, err := service.PaymentsApi.Sessions(c.Request.Context(), req)

if err != nil {
c.Error(err)
c.Negotiate(http.StatusOK, gin.Negotiate{
Offered: []string{binding.MIMEJSON, binding.MIMEHTML},
Data: api.Error("could not create checkout session with adyen"),
HTMLName: "500.html",
})
return
}

out := gin.H{
"ClientKey": s.conf.Adyen.ClientKey,
"SessionID": rep.Id,
"SessionData": *rep.SessionData,
}

c.Negotiate(http.StatusOK, gin.Negotiate{
Offered: []string{binding.MIMEJSON, binding.MIMEHTML},
Data: out,
HTMLName: "checkout.html",
})
}
12 changes: 8 additions & 4 deletions pkg/exchequer/exchequer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (
"syscall"
"time"

"github.com/adyen/adyen-go-api-library/v11/src/adyen"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"

"github.com/rotationalio/exchequer/pkg/config"
"github.com/rotationalio/exchequer/pkg/logger"
"github.com/rotationalio/exchequer/pkg/metrics"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)

func init() {
Expand Down Expand Up @@ -59,8 +61,9 @@ func New(conf config.Config) (svc *Server, err error) {
}

svc = &Server{
conf: conf,
errc: make(chan error, 1),
conf: conf,
errc: make(chan error, 1),
adyen: CreateAdyenClient(conf.Adyen),
}

// Configure the gin router if enabled
Expand Down Expand Up @@ -93,6 +96,7 @@ type Server struct {
conf config.Config
srv *http.Server
router *gin.Engine
adyen *adyen.APIClient
url *url.URL
started time.Time
healthy bool
Expand Down
1 change: 1 addition & 0 deletions pkg/exchequer/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func (s *Server) setupRoutes() (err error) {

// Pages
s.router.GET("/", s.Index)
s.router.GET("/checkout", s.Checkout)

// API Routes (Including Content Negotiated Partials)
v1 := s.router.Group("/v1")
Expand Down
4 changes: 4 additions & 0 deletions pkg/exchequer/static/css/main.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
html, body {
height: 100%;
width: 100%;
}

#adyen-dropin {
max-width: 920px;
}
1 change: 1 addition & 0 deletions pkg/exchequer/templates/500.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<section class="">
<h1>Internal Server Error</h1>
<p>{{ .Error }}</p>
</section>

{{ end }}
58 changes: 58 additions & 0 deletions pkg/exchequer/templates/checkout.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{{ template "base" . }}
{{ define "styles" }}
<!-- Embed the Adyen Web stylesheet. You can add your own styling by overriding the rules in the CSS file -->
<link rel="stylesheet" href="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.66.1/adyen.css"
integrity="sha384-gpOE6R0K50VgXe6u/pyjzkKl4Kr8hXu93KUCTmC4LqbO9mpoGUYsrmeVLcp2eejn"
crossorigin="anonymous">
{{ end }}

{{ define "content" }}

<section class="text-center py-14">
<h1>Checkout</h1>
<div id="adyen-dropin"></div>
</section>

{{ end }}

{{ define "appcode" }}
<!-- Embed the Adyen Web script element above any other JavaScript in your checkout page. -->
<script src="https://checkoutshopper-live.adyen.com/checkoutshopper/sdk/5.66.1/adyen.js"
integrity="sha384-1hIjspaI91zaZaaFP4++yNNBE9emITOdAAnKcDsVFvA8agUeXq/b9Fh+9q0fx2/2"
crossorigin="anonymous"></script>
<script>
const configuration = {
environment: "test",
clientKey: "{{ .ClientKey }}",
analytics: {
enabled: true
},
session: {
id: "{{ .SessionID }}",
sessionData: "{{ .SessionData }}"
},
onPaymentCompleted: (result, component) => {
console.info(result, component);
},
onError: (error, component) => {
console.error(error.name, error.message, error.stack, component);
},
paymentMethodsConfiguration: {
card: {
hasHolderName: true,
holderNameRequired: true,
billingAddressRequired: true
}
}
};

const setupAdyen = async () => {
console.log("setting up adyen dropin");
const checkout = await AdyenCheckout(configuration);
const dropinComponent = checkout.create('dropin').mount('#adyen-dropin');
console.log("adyen dropin configuration complete");
}

setupAdyen();
</script>
{{ end }}
1 change: 1 addition & 0 deletions pkg/exchequer/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

<section class="text-center py-14">
<h1>Hello World!</h1>
<a href="/checkout" title="Go to Checkout">Checkout</a>
</section>

{{ end }}

0 comments on commit 605f553

Please sign in to comment.