Skip to content

Commit

Permalink
Merge pull request #45 from customerio/txnl-push
Browse files Browse the repository at this point in the history
Transactional push support.
  • Loading branch information
clabland authored May 8, 2023
2 parents 97b92a7 + 727b691 commit ccb97f8
Show file tree
Hide file tree
Showing 11 changed files with 379 additions and 107 deletions.
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,11 +258,11 @@ In order to send push notifications, we need customer device information.
// platform (required) - the platform of the device, currently only accepts 'ios' and 'android'
// data (optional) - a ```map[string]interface{}``` of information about the device.
// You can pass any key/value pairs that would be useful in your triggers.
// We currently only save 'last_used'.
// Your interface{} should be parseable as Json by 'encoding/json'.Marshal

if err := track.AddDevice("5", "messaging token", "android", map[string]interface{}{
"last_used": time.Now().Unix(),
"attribute_name": "attribute_value",
}); err != nil {
// handle error
}
Expand All @@ -288,6 +288,7 @@ if err := track.DeleteDevice("5", "messaging-token"); err != nil {

To use the Customer.io [Transactional API](https://customer.io/docs/transactional-api), create an instance of the API client using an [App API key](https://customer.io/docs/managing-credentials#app-api-keys).

## Email
Create a `customerio.SendEmailRequest` instance, and then use `(c *customerio.APIClient).SendEmail` to send your message. [Learn more about transactional messages and optional `SendEmailRequest` properties](https://customer.io/docs/transactional-api).

You can also send attachments with your message. Use `customerio.SendEmailRequest.Attach` to encode attachments.
Expand Down Expand Up @@ -335,6 +336,42 @@ if err != nil {
fmt.Println(body)
```

## Push
Create a `customerio.SendPushRequest` instance, and then use `(c *customerio.APIClient).SendPush` to send your message. [Learn more about transactional messages and optional `SendPush` properties](https://customer.io/docs/transactional-api).

```go
client := customerio.NewAPIClient("<extapikey>", customerio.WithRegion(customerio.RegionUS));

request := customerio.SendPushRequest{
TransactionalMessageID: "3",
MessageData: map[string]interface{}{
"name": "Person",
"items": map[string]interface{}{
"name": "shoes",
"price": "59.99",
},
"products": []interface{}{},
},
Identifiers: map[string]string{
"id": "example1",
},
}

// (optional) upsert a particular device for the profile the push is being sent to.
device, err := customerio.NewDevice("device-id", "android", map[string]interface{}{"optional_attr": "value"})
if err != nil {
// handle error, invalid device params.
}
request.Device = device

body, err := client.SendPush(context.Background(), &request)
if err != nil {
// handle error
}

fmt.Println(body)
```

## Context Support
There are additional API methods that support passing a context that satisfies the `context.Context` interface to allow better control over dispatched requests. For example with sending an event:
```go
Expand Down
54 changes: 0 additions & 54 deletions customerio.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,60 +141,6 @@ func (c *CustomerIO) DeleteCtx(ctx context.Context, customerID string) error {
nil)
}

// Delete deletes a customer
func (c *CustomerIO) Delete(customerID string) error {
return c.DeleteCtx(context.Background(), customerID)
}

// AddDeviceCtx adds a device for a customer
func (c *CustomerIO) AddDeviceCtx(ctx context.Context, customerID string, deviceID string, platform string, data map[string]interface{}) error {
if customerID == "" {
return ParamError{Param: "customerID"}
}
if deviceID == "" {
return ParamError{Param: "deviceID"}
}
if platform == "" {
return ParamError{Param: "platform"}
}

body := map[string]map[string]interface{}{
"device": {
"id": deviceID,
"platform": platform,
},
}
for k, v := range data {
body["device"][k] = v
}
return c.request(ctx, "PUT",
fmt.Sprintf("%s/api/v1/customers/%s/devices", c.URL, url.PathEscape(customerID)),
body)
}

// AddDevice adds a device for a customer
func (c *CustomerIO) AddDevice(customerID string, deviceID string, platform string, data map[string]interface{}) error {
return c.AddDeviceCtx(context.Background(), customerID, deviceID, platform, data)
}

// DeleteDeviceCtx deletes a device for a customer
func (c *CustomerIO) DeleteDeviceCtx(ctx context.Context, customerID string, deviceID string) error {
if customerID == "" {
return ParamError{Param: "customerID"}
}
if deviceID == "" {
return ParamError{Param: "deviceID"}
}
return c.request(ctx, "DELETE",
fmt.Sprintf("%s/api/v1/customers/%s/devices/%s", c.URL, url.PathEscape(customerID), url.PathEscape(deviceID)),
nil)
}

// DeleteDevice deletes a device for a customer
func (c *CustomerIO) DeleteDevice(customerID string, deviceID string) error {
return c.DeleteDeviceCtx(context.Background(), customerID, deviceID)
}

func (c *CustomerIO) auth() string {
return base64.URLEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", c.siteID, c.apiKey)))
}
Expand Down
109 changes: 109 additions & 0 deletions device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package customerio

import (
"context"
"fmt"
"net/url"
)

type deviceV1 struct {
ID string `json:"id"`
Platform string `json:"platform"`
LastUsed string `json:"last_used,omitempty"`
Attributes map[string]interface{} `json:"attributes"`
}

type deviceV2 struct {
ID string `json:"token"`
Platform string `json:"platform"`
LastUsed string `json:"last_used,omitempty"`
Attributes map[string]interface{} `json:"attributes"`
}

func newDeviceV1(deviceID, platform string, data map[string]interface{}) (*deviceV1, error) {
if deviceID == "" {
return nil, ParamError{Param: "deviceID"}
}
if platform == "" {
return nil, ParamError{Param: "platform"}
}
d := &deviceV1{
ID: deviceID,
Platform: platform,
}

if len(data) > 0 {
d.Attributes = make(map[string]interface{})
}

for k, v := range data {
if k == "last_used" {
d.LastUsed = fmt.Sprintf("%v", v)
continue
}
d.Attributes[k] = v
}

return d, nil
}

func NewDevice(deviceID, platform string, data map[string]interface{}) (*deviceV2, error) {
d, err := newDeviceV1(deviceID, platform, data)
if err != nil {
return nil, err
}
return &deviceV2{
ID: d.ID,
Platform: d.Platform,
Attributes: d.Attributes,
LastUsed: d.LastUsed,
}, nil
}

// Delete deletes a customer
func (c *CustomerIO) Delete(customerID string) error {
return c.DeleteCtx(context.Background(), customerID)
}

// AddDeviceCtx adds a device for a customer
func (c *CustomerIO) AddDeviceCtx(ctx context.Context, customerID string, deviceID string, platform string, data map[string]interface{}) error {
if customerID == "" {
return ParamError{Param: "customerID"}
}

d, err := newDeviceV1(deviceID, platform, data)
if err != nil {
return err
}

body := map[string]interface{}{
"device": d,
}

return c.request(ctx, "PUT",
fmt.Sprintf("%s/api/v1/customers/%s/devices", c.URL, url.PathEscape(customerID)),
body)
}

// AddDevice adds a device for a customer
func (c *CustomerIO) AddDevice(customerID string, deviceID string, platform string, data map[string]interface{}) error {
return c.AddDeviceCtx(context.Background(), customerID, deviceID, platform, data)
}

// DeleteDeviceCtx deletes a device for a customer
func (c *CustomerIO) DeleteDeviceCtx(ctx context.Context, customerID string, deviceID string) error {
if customerID == "" {
return ParamError{Param: "customerID"}
}
if deviceID == "" {
return ParamError{Param: "deviceID"}
}
return c.request(ctx, "DELETE",
fmt.Sprintf("%s/api/v1/customers/%s/devices/%s", c.URL, url.PathEscape(customerID), url.PathEscape(deviceID)),
nil)
}

// DeleteDevice deletes a device for a customer
func (c *CustomerIO) DeleteDevice(customerID string, deviceID string) error {
return c.DeleteDeviceCtx(context.Background(), customerID, deviceID)
}
27 changes: 23 additions & 4 deletions examples/transactional.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func main() {

client := customerio.NewAPIClient("<your-key-here>", customerio.WithRegion(customerio.RegionUS))

req := customerio.SendEmailRequest{
emailReq := customerio.SendEmailRequest{
Identifiers: map[string]string{
"id": "customer_1",
},
Expand All @@ -34,14 +34,33 @@ func main() {
}
defer f.Close()

if err := req.Attach("sample.pdf", f); err != nil {
if err := emailReq.Attach("sample.pdf", f); err != nil {
panic(err)
}

resp, err := client.SendEmail(ctx, &req)
emailResp, err := client.SendEmail(ctx, &emailReq)
if err != nil {
panic(err)
}

fmt.Println(resp)
fmt.Println(emailResp)

// Create and configure your transactional messages in the Customer.io UI.
transactionalMessageID := "push_message_id"

pushReq := customerio.SendPushRequest{
TransactionalMessageID: transactionalMessageID,
Identifiers: map[string]string{
"id": "customer_1",
},
Title: "hello, {{ trigger.name }}",
Message: "hello from the Customer.io {{ trigger.client }} client",
}

pushResp, err := client.SendPush(ctx, &pushReq)
if err != nil {
panic(err)
}

fmt.Println(pushResp)
}
31 changes: 4 additions & 27 deletions send_email.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
"net/http"
)

type SendEmailRequest struct {
Expand Down Expand Up @@ -64,33 +62,12 @@ type SendEmailResponse struct {

// SendEmail sends a single transactional email using the Customer.io transactional API
func (c *APIClient) SendEmail(ctx context.Context, req *SendEmailRequest) (*SendEmailResponse, error) {
body, statusCode, err := c.doRequest(ctx, "POST", "/v1/send/email", req)
resp, err := c.sendTransactional(ctx, TransactionalTypeEmail, req)
if err != nil {
return nil, err
}

if statusCode != http.StatusOK {
var meta struct {
Meta struct {
Err string `json:"error"`
} `json:"meta"`
}
if err := json.Unmarshal(body, &meta); err != nil {
return nil, &TransactionalError{
StatusCode: statusCode,
Err: string(body),
}
}
return nil, &TransactionalError{
StatusCode: statusCode,
Err: meta.Meta.Err,
}
}

var result SendEmailResponse
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}

return &result, nil
return &SendEmailResponse{
*resp,
}, nil
}
27 changes: 7 additions & 20 deletions send_email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package customerio_test
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"reflect"
Expand All @@ -28,41 +27,29 @@ func TestSendEmail(t *testing.T) {
},
}

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
b, err := ioutil.ReadAll(req.Body)
if err != nil {
t.Error(err)
}
defer req.Body.Close()

var verify = func(request []byte) {
var body customerio.SendEmailRequest
if err := json.Unmarshal(b, &body); err != nil {
if err := json.Unmarshal(request, &body); err != nil {
t.Error(err)
}

if !reflect.DeepEqual(&body, emailRequest) {
t.Errorf("Request differed, want: %#v, got: %#v", emailRequest, body)
t.Errorf("Request differed, want: %#v, got: %#v", request, body)
}
}

w.Write([]byte(`{
"delivery_id": "ABCDEFG",
"queued_at": 1500111111
}`))
}))
api, srv := transactionalServer(t, verify)
defer srv.Close()

api := customerio.NewAPIClient("myKey")
api.URL = srv.URL

resp, err := api.SendEmail(context.Background(), emailRequest)
if err != nil {
t.Error(err)
}

expect := &customerio.SendEmailResponse{
TransactionalResponse: customerio.TransactionalResponse{
DeliveryID: "ABCDEFG",
QueuedAt: time.Unix(1500111111, 0),
DeliveryID: testDeliveryID,
QueuedAt: time.Unix(int64(testQueuedAt), 0),
},
}

Expand Down
Loading

0 comments on commit ccb97f8

Please sign in to comment.