From 1f1601cd3c27c484f508e5d2c7fb6204437f68a6 Mon Sep 17 00:00:00 2001 From: tsuji_jumpei Date: Thu, 4 Dec 2014 21:08:46 +0900 Subject: [PATCH] Initial push --- .travis.yml | 13 ++++ Makefile | 15 +++++ README.md | 37 ++++++++++++ appstore/model.go | 62 +++++++++++++++++++ appstore/validator.go | 118 +++++++++++++++++++++++++++++++++++++ appstore/validator_test.go | 112 +++++++++++++++++++++++++++++++++++ 6 files changed, 357 insertions(+) create mode 100644 .travis.yml create mode 100644 Makefile create mode 100644 appstore/model.go create mode 100644 appstore/validator.go create mode 100644 appstore/validator_test.go diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5339cb9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,13 @@ +language: go +go: +- 1.3 +before_install: + sudo pip install codecov +install: +- go get code.google.com/p/go.tools/cmd/cover +- go get ./... +script: +- make cover +after_success: + codecov + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..5d9b45c --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ + +.PHONEY: all setup test cover + +all: setup cover + +setup: + go get code.google.com/p/go.tools/cmd/cover + go get ./... + +test: + go test -v ./... + +cover: + go test -v -coverprofile=coverage.txt -covermode=count ./appstore + diff --git a/README.md b/README.md index e3ff76f..372c92a 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,40 @@ go-iap ====== go-iap verifies the purchase receipt via AppStore or GooglePlayStore + + +# Installation +``` +go get github.com/dogenzaka/go-iap +``` + + +# Quick Start + +### In App Purchase (via App Store) + +``` +import( + "github.com/dogenzaka/go-iap/appstore" +) + +func main() { + client := appstore.New() + req := appstore.IAPRequest{ + ReceiptData: "your receipt data encoded by base64", + } + resp, err := client.Verify(&req) +} +``` + +# ToDo +- [x] App Store Client +- [ ] Google Play Store Client + + +# Support +iOS7 or above + + +# License +Gorv is licensed under the MIT. diff --git a/appstore/model.go b/appstore/model.go new file mode 100644 index 0000000..a958159 --- /dev/null +++ b/appstore/model.go @@ -0,0 +1,62 @@ +package appstore + +type ( + // The IAPRequest type has the request parameter + IAPRequest struct { + ReceiptData string `json:"receipt-data"` + } + + // The RequestDate type indicates the date and time that the request was sent + RequestDate struct { + RequestDate string `json:"request_date"` + RequestDateMS string `json:"request_date_ms"` + RequestDatePST string `json:"request_date_pst"` + } + + // The PurchaseDate type indicates the date and time that the item was purchased + PurchaseDate struct { + PurchaseDate string `json:"purchase_date"` + PurchaseDateMS string `json:"purchase_date_ms"` + PurchaseDatePST string `json:"purchase_date_pst"` + } + + // The OriginalPurchaseDate type indicates the beginning of the subscription period + OriginalPurchaseDate struct { + OriginalPurchaseDate string `json:"original_purchase_date"` + OriginalPurchaseDateMS string `json:"original_purchase_date_ms"` + OriginalPurchaseDatePST string `json:"original_purchase_date_pst"` + } + + // The InApp type has the receipt attributes + InApp struct { + Quantity string `json:"quantity"` + ProductID string `json:"product_id"` + TransactionID string `json:"transaction_id"` + OriginalTransactionID string `json:"original_transaction_id"` + IsTrialPeriod string `json:"is_trial_period"` + ExpiresDate string `json:"expires_date"` + CancellationDate string `json:"cancellation_date"` + AppItemID string `json:"app_item_id"` + VersionExternalIdentifier string `json:"version_external_identifier"` + WebOrderLineItemID string `json:"web_order_line_item_id"` + PurchaseDate + OriginalPurchaseDate + } + + // The Receipt type has whole data of receipt + Receipt struct { + ReceiptType string `json:"receipt_type"` + BundleID string `json:"bundle_id"` + ApplicationVersion string `json:"application_version"` + OriginalApplicationVersion string `json:"original_application_version"` + InApp []InApp `json:"in_app"` + RequestDate + } + + // The IAPResponse type has the response properties + IAPResponse struct { + Status int `json:"status"` + Environment string `json:"environment"` + Receipt Receipt `json:"receipt"` + } +) diff --git a/appstore/validator.go b/appstore/validator.go new file mode 100644 index 0000000..e9c26af --- /dev/null +++ b/appstore/validator.go @@ -0,0 +1,118 @@ +package appstore + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "strconv" + "strings" + "time" + + "github.com/parnurzeal/gorequest" +) + +const ( + sandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt" + productionURL string = "https://buy.itunes.apple.com/verifyReceipt" +) + +// Config is a configuration to initialize client +type Config struct { + IsProduction bool + TimeOut time.Duration +} + +// IAPClient is an interface to call validation API in App Store +type IAPClient interface { + Verify(IAPRequest) (IAPResponse, error) +} + +// Client implements IAPClient +type Client struct { + URL string + TimeOut time.Duration +} + +// HandleError returns error message by status code +func HandleError(status int) error { + var message string + + switch status { + case 0: + return nil + + case 21000: + message = "The App Store could not read the JSON object you provided." + + case 21002: + message = "The data in the receipt-data property was malformed or missing." + + case 21003: + message = "The receipt could not be authenticated." + + case 21005: + message = "The receipt server is not currently available." + + case 21007: + message = "This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead." + + case 21008: + message = "This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead." + + default: + message = "An unknown error ocurred" + } + + return errors.New(message) +} + +// New creates a client object +func New() *Client { + client := &Client{ + URL: sandboxURL, + TimeOut: time.Second * 5, + } + if os.Getenv("IAP_ENVIRONMENT") == "production" { + client.URL = productionURL + } + return client +} + +// NewWithConfig creates a client with configuration +func NewWithConfig(config Config) *Client { + if config.TimeOut == 0 { + config.TimeOut = time.Second * 5 + } + + client := &Client{ + URL: sandboxURL, + TimeOut: config.TimeOut, + } + if config.IsProduction { + client.URL = productionURL + } + + return client +} + +// Verify sends receipts and gets validation result +func (c *Client) Verify(req *IAPRequest) (IAPResponse, error) { + result := IAPResponse{} + res, body, errs := gorequest.New(). + Post(c.URL). + Send(req). + Timeout(c.TimeOut). + End() + + if errs != nil { + return result, fmt.Errorf("%v", errs) + } + if res.StatusCode < 200 || res.StatusCode >= 300 { + return result, errors.New("An error occurred in IAP - code:" + strconv.Itoa(res.StatusCode)) + } + + err := json.NewDecoder(strings.NewReader(body)).Decode(&result) + + return result, err +} diff --git a/appstore/validator_test.go b/appstore/validator_test.go new file mode 100644 index 0000000..db67415 --- /dev/null +++ b/appstore/validator_test.go @@ -0,0 +1,112 @@ +package appstore + +import ( + "errors" + "reflect" + "testing" + "time" +) + +func TestHandleError(t *testing.T) { + var expected, actual error + + // status 0 + expected = nil + actual = HandleError(0) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21000 + expected = errors.New("The App Store could not read the JSON object you provided.") + actual = HandleError(21000) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21002 + expected = errors.New("The data in the receipt-data property was malformed or missing.") + actual = HandleError(21002) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21003 + expected = errors.New("The receipt could not be authenticated.") + actual = HandleError(21003) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21005 + expected = errors.New("The receipt server is not currently available.") + actual = HandleError(21005) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21007 + expected = errors.New("This receipt is from the test environment, but it was sent to the production environment for verification. Send it to the test environment instead.") + actual = HandleError(21007) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status 21008 + expected = errors.New("This receipt is from the production environment, but it was sent to the test environment for verification. Send it to the production environment instead.") + actual = HandleError(21008) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } + + // status unkown + expected = errors.New("An unknown error ocurred") + actual = HandleError(100) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } +} + +func TestNew(t *testing.T) { + expected := &Client{ + URL: "https://sandbox.itunes.apple.com/verifyReceipt", + TimeOut: time.Second * 5, + } + + actual := New() + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } +} + +func TestNewWithConfig(t *testing.T) { + config := Config{ + IsProduction: true, + TimeOut: time.Second * 2, + } + + expected := &Client{ + URL: "https://buy.itunes.apple.com/verifyReceipt", + TimeOut: time.Second * 2, + } + + actual := NewWithConfig(config) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } +} + +func TestVerify(t *testing.T) { + client := New() + + expected := IAPResponse{ + Status: 21002, + } + req := IAPRequest{ + ReceiptData: "dummy data", + } + actual, _ := client.Verify(&req) + if !reflect.DeepEqual(actual, expected) { + t.Errorf("got %v\nwant %v", actual, expected) + } +}