Skip to content

Commit

Permalink
Merge pull request #1 from LucasRouckhout/master
Browse files Browse the repository at this point in the history
Use functional options pattern to configure Twikey Client
  • Loading branch information
koen-serry authored Mar 13, 2024
2 parents ff7e2dd + b70f1a8 commit e675688
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 37 deletions.
111 changes: 85 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,51 +52,110 @@ Initializing the Twikey API client by configuring your API key which you can fin
the [Twikey merchant interface](https://www.twikey.com).

```go
package example

import "github.com/twikey/twikey-api-go"

func main() {
client := twikey.NewClient("YOU_API_KEY")
}

```

It's possible to further configure the API client if so desired using functional options.
For example providing a custom HTTP client with a different timeout and a Custom logger
implementation.

```go
package example

import (
twikey "github.com/twikey/twikey-api-go"
"github.com/twikey/twikey-api-go"
"log"
"net/http"
)

var twikeyClient = twikey.NewClient(os.Getenv("TWIKEY_API_KEY"))
func main() {
client := twikey.NewClient("YOUR_API_KEY",
twikey.WithHTTPClient(&http.Client{Timeout: time.Minute}),
twikey.WithLogger(log.Default()),
)
}

```

Another example, it's possible to disable logging by setting the value of the logger to `NullLogger`.
By default, the Twikey client will print logs using the default standard library logger.

var twikeyClient = &twikey.TwikeyClient{
ApiKey: os.Getenv("TWIKEY_API_KEY"),
//Debug: log.Default(),
HTTPClient: &http.Client{
Timeout: time.Minute,
},
```go
package example

import (
"github.com/twikey/twikey-api-go"
"log"
"net/http"
)

func main() {
client := twikey.NewClient("YOUR_API_KEY",
twikey.WithLogger(twikey.NullLogger),
)
}

```

## Documents

Invite a customer to sign a SEPA mandate using a specific behaviour template (ct) that allows you to configure
the behaviour or flow that the customer will experience. This 'ct' can be found in the template section of the settings.
Invite a customer to sign a SEPA mandate using a specific behaviour template (Template) that allows you to configure
the behaviour or flow that the customer will experience. This can be found in the template section of the settings.
The extra can be used to pass in extra attributes linked to the mandate.

```go
invite, err := twikeyClient.DocumentInvite(context.Background(), &InviteRequest{
ct: yourct,
customerNumber: "123",
email: "[email protected]",
firstname: "John",
lastname: "Doe",
l: "en",
address: "Abbey road",
city: "Liverpool",
zip: "1526",
country: "BE",
}, nil)
if err != nil {
t.Fatal(err)
package example

import (
"context"
"fmt"
"github.com/twikey/twikey-api-go"
"log"
"os"
)

func main() {
client := twikey.NewClient(os.Getenv("TWIKEY_API_KEY"))

ctx := context.Background()
invite, err := client.DocumentSign(ctx, &twikey.InviteRequest{
Template: "YOUR_TEMPLATE_ID",
CustomerNumber: "123",
Email: "[email protected]",
Language: "en",
Lastname: "Doe",
Firstname: "John",
Address: "Abbey Road",
City: "Liverpool",
Zip: "1562",
Country: "EN",
Iban: "GB32BARC20040198915359",
Bic: "GEBEBEB",
Method: "sms",
Extra: map[string]string{
"SomeKey": "VALUE",
},
})
if err != nil {
log.Fatal(err)
}

fmt.Println(invite.Url)
}
fmt.println(invite.Url)

```

_After creation, the link available in invite.Url can be used to redirect the customer into the signing flow or even
send him a link through any other mechanism. Ideally you store the mandatenumber for future usage (eg. sending transactions)._

The DocumentSign function has a similar syntax only that it requires a method and is mosly used for interactive sessions
The DocumentSign function has a similar syntax only that it requires a method and is mostly used for interactive sessions
where no screens are involved. See [the documentation](https://api.twikey.com) for more info.

### Feed
Expand Down
38 changes: 38 additions & 0 deletions example/simple.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"context"
"fmt"
"github.com/twikey/twikey-api-go"
"log"
"os"
)

func main() {
client := twikey.NewClient(os.Getenv("TWIKEY_API_KEY"))

ctx := context.Background()
invite, err := client.DocumentSign(ctx, &twikey.InviteRequest{
Template: "YOUR_TEMPLATE_ID",
CustomerNumber: "123",
Email: "[email protected]",
Language: "en",
Lastname: "Doe",
Firstname: "John",
Address: "Abbey Road",
City: "Liverpool",
Zip: "1562",
Country: "EN",
Iban: "GB32BARC20040198915359",
Bic: "GEBEBEB",
Method: "sms",
Extra: map[string]string{
"SomeKey": "VALUE",
},
})
if err != nil {
log.Fatal(err)
}

fmt.Println(invite.Url)
}
4 changes: 2 additions & 2 deletions session.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func generateOtp(_salt string, _privKey string) (int, error) {

func (c *Client) refreshTokenIfRequired() error {

if c.timeProvider.Now().Sub(c.lastLogin).Hours() < 23 {
if c.TimeProvider.Now().Sub(c.lastLogin).Hours() < 23 {
return nil
}

Expand All @@ -71,7 +71,7 @@ func (c *Client) refreshTokenIfRequired() error {
if resp.StatusCode == 200 && token != nil {
c.Debug.Println("Connected to", c.BaseURL, "with token", token[0])
c.apiToken = token[0]
c.lastLogin = c.timeProvider.Now()
c.lastLogin = c.TimeProvider.Now()
return nil
} else if resp.StatusCode > 500 {
c.Debug.Println("General error", resp.StatusCode, resp.Status)
Expand Down
2 changes: 1 addition & 1 deletion session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestClient_refreshTokenIfRequired(t *testing.T) {
HTTPClient: &http.Client{
Timeout: time.Minute,
},
timeProvider: &ttp,
TimeProvider: &ttp,
Debug: log.Default(),
}
c.BaseURL = getEnv("TWIKEY_URL", "https://api.beta.twikey.com")
Expand Down
86 changes: 78 additions & 8 deletions twikey.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ func (tp DefaultTimeProvider) Now() time.Time {
return time.Now()
}

type Logger interface {
Println(v ...interface{})
Printf(format string, v ...interface{})
}

// nullWriter is an io.Writer that discards all writes.
type nullWriter struct{}

func (n *nullWriter) Write(p []byte) (int, error) {
return len(p), nil
}

var NullLogger = log.New(&nullWriter{}, "", log.LstdFlags)

// Client is the base class, please use a dedicated UserAgent so we can notify the emergency contact
// if weird behaviour is perceived.
type Client struct {
Expand All @@ -42,27 +56,83 @@ type Client struct {
Salt string
UserAgent string
HTTPClient HTTPClient
Debug *log.Logger
Debug Logger
apiToken string
lastLogin time.Time
timeProvider TimeProvider
TimeProvider TimeProvider
}

type ClientOption = func(*Client)

// WithLogger sets the Logger for the Client. If you don't want
// the Client to log anything you can pass in the NullLogger
func WithLogger(logger Logger) ClientOption {
return func(client *Client) {
client.Debug = logger
}
}

// WithBaseURL will set the base URL of the API used when making requests.
//
// In production, you will probably want to use the default but if you want
// to make request to some mock API in a test environment you can use this
// to make the Client make requests to a different host.
//
// The default: https://api.twikey.com
func WithBaseURL(baseURL string) ClientOption {
return func(client *Client) {
client.BaseURL = baseURL
}
}

// WithHTTPClient configures the underlying HTTP client used to make HTTP requests.
func WithHTTPClient(httpClient HTTPClient) ClientOption {
return func(client *Client) {
client.HTTPClient = httpClient
}
}

// WithTimeProvider sets the TimeProvider for this Client.
func WithTimeProvider(provider TimeProvider) ClientOption {
return func(client *Client) {
client.TimeProvider = provider
}
}

// WithSalt sets the salt used in generating one-time-passwords
func WithSalt(salt string) ClientOption {
return func(client *Client) {
client.Salt = salt
}
}

// WithUserAgent will configure the value that is passed on in the HTTP User-Agent header
// for all requests to the Twikey API made with this Client
func WithUserAgent(userAgent string) ClientOption {
return func(client *Client) {
client.UserAgent = userAgent
}
}

// NewClient is a convenience method to hit the ground running with the Twikey Rest API
func NewClient(apiKey string) *Client {
logger := log.Default()
logger.SetOutput(ioutil.Discard)
return &Client{
func NewClient(apiKey string, opts ...ClientOption) *Client {
c := &Client{
BaseURL: baseURLV1,
APIKey: apiKey,
Salt: "own",
UserAgent: twikeyBaseAgent,
HTTPClient: &http.Client{
Timeout: time.Minute,
},
Debug: logger,
timeProvider: DefaultTimeProvider{},
Debug: log.Default(),
TimeProvider: DefaultTimeProvider{},
}

for _, opt := range opts {
opt(c)
}

return c
}

type errorResponse struct {
Expand Down
35 changes: 35 additions & 0 deletions twikey_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package twikey

import (
"log"
"os"
"testing"
)
Expand All @@ -11,6 +12,40 @@ func newTestClient() *Client {
return c
}

func TestClientWithBasicFunctionalOptions(t *testing.T) {
salt := "salty"
baseUrl := "http://localtest.me"
userAgent := "someUserAgent"
apikey := "123"

c := NewClient(apikey,
WithBaseURL(baseUrl),
WithSalt(salt),
WithUserAgent(userAgent),
WithLogger(log.Default()),
)

if c.APIKey != apikey {
t.Fatalf("Expected client ApiKey to be %s but was %s", apikey, c.APIKey)
}

if c.BaseURL != baseUrl {
t.Fatalf("Expected client BaseURL to be %s but was %s", baseUrl, c.BaseURL)
}

if c.Salt != salt {
t.Fatalf("Expected client Salt to be %s but was %s", salt, c.Salt)
}

if c.UserAgent != userAgent {
t.Fatalf("Expected client UserAgent to be %s but was %s", userAgent, c.UserAgent)
}

if c.Debug != log.Default() {
t.Fatalf("Expected the default logger from log to be used")
}
}

func TestTwikeyClient_verifyWebhook(t *testing.T) {
c := NewClient("1234")
err := c.VerifyWebhook("55261CBC12BF62000DE1371412EF78C874DBC46F513B078FB9FF8643B2FD4FC2", "abc=123&name=abc")
Expand Down

0 comments on commit e675688

Please sign in to comment.