forked from Mirrors/go-iap
Merge pull request #18 from fubotv/master
Add Support for the Amazon Receipt Verification Service
This commit is contained in:
19
README.md
19
README.md
@@ -11,6 +11,9 @@ Current API Documents:
|
|||||||
* AppStore: [](https://godoc.org/github.com/dogenzaka/go-iap/appstore)
|
* AppStore: [](https://godoc.org/github.com/dogenzaka/go-iap/appstore)
|
||||||
* GooglePlay: [](https://godoc.org/github.com/dogenzaka/go-iap/playstore)
|
* GooglePlay: [](https://godoc.org/github.com/dogenzaka/go-iap/playstore)
|
||||||
|
|
||||||
|
* Amazon AppStore: [](https://godoc.org/github.com/dogenzaka/go-iap/amazon)
|
||||||
|
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
```
|
```
|
||||||
go get github.com/parnurzeal/gorequest
|
go get github.com/parnurzeal/gorequest
|
||||||
@@ -66,6 +69,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### In App Purchase (via Amazon App Store)
|
||||||
|
|
||||||
|
```
|
||||||
|
import(
|
||||||
|
"github.com/dogenzaka/go-iap/amazon"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
client := amazon.New("developerSecret")
|
||||||
|
|
||||||
|
resp, err := client.Verify("userID", "receiptID")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# ToDo
|
# ToDo
|
||||||
- [x] Validator for In App Purchase Receipt (AppStore)
|
- [x] Validator for In App Purchase Receipt (AppStore)
|
||||||
@@ -82,6 +98,9 @@ This validator supports the receipt type for iOS7 or above.
|
|||||||
### In App Billing
|
### In App Billing
|
||||||
This validator uses [Version 3 API](http://developer.android.com/google/play/billing/api.html).
|
This validator uses [Version 3 API](http://developer.android.com/google/play/billing/api.html).
|
||||||
|
|
||||||
|
### In App Purchase (Amazon)
|
||||||
|
This validator uses [RVS for IAP v2.0](https://developer.amazon.com/public/apis/earn/in-app-purchasing/docs-v2/verifying-receipts-in-iap-2.0).
|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
go-iap is licensed under the MIT.
|
go-iap is licensed under the MIT.
|
||||||
|
|||||||
114
amazon/validator.go
Normal file
114
amazon/validator.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package amazon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/parnurzeal/gorequest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SandboxURL string = "http://localhost:8080/RVSSandbox"
|
||||||
|
ProductionURL string = "https://appstore-sdk.amazon.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSandboxUrl() string {
|
||||||
|
url := os.Getenv("IAP_SANDBOX_URL")
|
||||||
|
if url == "" {
|
||||||
|
url = SandboxURL
|
||||||
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is a configuration to initialize client
|
||||||
|
type Config struct {
|
||||||
|
IsProduction bool
|
||||||
|
Secret string
|
||||||
|
TimeOut time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// The IAPResponse type has the response properties
|
||||||
|
type IAPResponse struct {
|
||||||
|
ReceiptID string `json:"receiptId"`
|
||||||
|
ProductType string `json:"productType"`
|
||||||
|
ProductID string `json:"productId"`
|
||||||
|
PurchaseDate int64 `json:"purchaseDate"`
|
||||||
|
CancelDate int64 `json:"cancelDate"`
|
||||||
|
TestTransaction bool `json:"testTransaction"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IAPResponseError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status bool `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAPClient is an interface to call validation API in Amazon App Store
|
||||||
|
type IAPClient interface {
|
||||||
|
Verify(string, string) (IAPResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client implements IAPClient
|
||||||
|
type Client struct {
|
||||||
|
URL string
|
||||||
|
Secret string
|
||||||
|
TimeOut time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a client object
|
||||||
|
func New(secret string) IAPClient {
|
||||||
|
client := Client{
|
||||||
|
URL: getSandboxUrl(),
|
||||||
|
Secret: secret,
|
||||||
|
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: getSandboxUrl(),
|
||||||
|
Secret: config.Secret,
|
||||||
|
TimeOut: config.TimeOut,
|
||||||
|
}
|
||||||
|
if config.IsProduction {
|
||||||
|
client.URL = ProductionURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify sends receipts and gets validation result
|
||||||
|
func (c Client) Verify(userID string, receiptID string) (IAPResponse, error) {
|
||||||
|
result := IAPResponse{}
|
||||||
|
url := fmt.Sprintf("%v/version/1.0/verifyReceiptId/developer/%v/user/%v/receiptId/%v", c.URL, c.Secret, userID, receiptID)
|
||||||
|
res, body, errs := gorequest.New().
|
||||||
|
Get(url).
|
||||||
|
Timeout(c.TimeOut).
|
||||||
|
End()
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
return result, fmt.Errorf("%v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
|
responseError := IAPResponseError{}
|
||||||
|
json.NewDecoder(strings.NewReader(body)).Decode(&responseError)
|
||||||
|
return result, errors.New(responseError.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.NewDecoder(strings.NewReader(body)).Decode(&result)
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
169
amazon/validator_test.go
Normal file
169
amazon/validator_test.go
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
package amazon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHandle497Error(t *testing.T) {
|
||||||
|
var expected, actual error
|
||||||
|
client := New("developerSecret")
|
||||||
|
|
||||||
|
server, client := testTools(
|
||||||
|
497,
|
||||||
|
"{\"message\":\"Purchase token/app user mismatch\",\"status\":false}",
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// status 400
|
||||||
|
expected = errors.New("Purchase token/app user mismatch")
|
||||||
|
_, actual = client.Verify(
|
||||||
|
"99FD_DL23EMhrOGDnur9-ulvqomrSg6qyLPSD3CFE=",
|
||||||
|
"q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM9JLrShIzCvOzM-LL04tiTdW0lFKASo2NDEwMjCwMDM2MTC0AIqVAsUsLd1c4l18jIxdfTOK_N1d8kqLLHVLc8oK83OLgtPNCit9AoJdjJ3dXG2BGkqUrAxrAQ",
|
||||||
|
)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandle400Error(t *testing.T) {
|
||||||
|
var expected, actual error
|
||||||
|
client := New("developerSecret")
|
||||||
|
|
||||||
|
server, client := testTools(
|
||||||
|
400,
|
||||||
|
"{\"message\":\"Failed to parse receipt Id\",\"status\":false}",
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
// status 400
|
||||||
|
expected = errors.New("Failed to parse receipt Id")
|
||||||
|
_, actual = client.Verify(
|
||||||
|
"99FD_DL23EMhrOGDnur9-ulvqomrSg6qyLPSD3CFE=",
|
||||||
|
"q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM9JLrShIzCvOzM-LL04tiTdW0lFKASo2NDEwMjCwMDM2MTC0AIqVAsUsLd1c4l18jIxdfTOK_N1d8kqLLHVLc8oK83OLgtPNCit9AoJdjJ3dXG2BGkqUrAxrAQ",
|
||||||
|
)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
expected := Client{
|
||||||
|
URL: SandboxURL,
|
||||||
|
TimeOut: time.Second * 5,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := New("developerSecret")
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithEnvironment(t *testing.T) {
|
||||||
|
expected := Client{
|
||||||
|
URL: ProductionURL,
|
||||||
|
TimeOut: time.Second * 5,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("IAP_ENVIRONMENT", "production")
|
||||||
|
actual := New("developerSecret")
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithConfig(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
IsProduction: true,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
TimeOut: time.Second * 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := Client{
|
||||||
|
URL: ProductionURL,
|
||||||
|
TimeOut: time.Second * 2,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := NewWithConfig(config)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithConfigTimeout(t *testing.T) {
|
||||||
|
config := Config{
|
||||||
|
IsProduction: true,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := Client{
|
||||||
|
URL: ProductionURL,
|
||||||
|
TimeOut: time.Second * 5,
|
||||||
|
Secret: "developerSecret",
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := NewWithConfig(config)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerify(t *testing.T) {
|
||||||
|
server, client := testTools(
|
||||||
|
200,
|
||||||
|
"{\"purchaseDate\":1402008634018,\"receiptId\":\"q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM9JLrShIzCvOzM-LL04tiTdW0lFKASo2NDEwMjCwMDM2MTC0AIqVAsUsLd1c4l18jIxdfTOK_N1d8kqLLHVLc8oK83OLgtPNCit9AoJdjJ3dXG2BGkqUrAxrAQ\",\"productId\":\"com.amazon.iapsamplev2.expansion_set_3\",\"parentProductId\":null,\"productType\":\"ENTITLED\",\"cancelDate\":null,\"quantity\":1,\"betaProduct\":false,\"testTransaction\":true}",
|
||||||
|
)
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
expected := IAPResponse{
|
||||||
|
ReceiptID: "q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM9JLrShIzCvOzM-LL04tiTdW0lFKASo2NDEwMjCwMDM2MTC0AIqVAsUsLd1c4l18jIxdfTOK_N1d8kqLLHVLc8oK83OLgtPNCit9AoJdjJ3dXG2BGkqUrAxrAQ",
|
||||||
|
ProductType: "ENTITLED",
|
||||||
|
ProductID: "com.amazon.iapsamplev2.expansion_set_3",
|
||||||
|
PurchaseDate: 1402008634018,
|
||||||
|
CancelDate: 0,
|
||||||
|
TestTransaction: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
actual, _ := client.Verify(
|
||||||
|
"99FD_DL23EMhrOGDnur9-ulvqomrSg6qyLPSD3CFE=",
|
||||||
|
"q1YqVrJSSs7P1UvMTazKz9PLTCwoTswtyEktM9JLrShIzCvOzM-LL04tiTdW0lFKASo2NDEwMjCwMDM2MTC0AIqVAsUsLd1c4l18jIxdfTOK_N1d8kqLLHVLc8oK83OLgtPNCit9AoJdjJ3dXG2BGkqUrAxrAQ",
|
||||||
|
)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyTimeout(t *testing.T) {
|
||||||
|
// HTTP 100 is "continue" so it will time out
|
||||||
|
server, client := testTools(100, "timeout response")
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
expected := errors.New("")
|
||||||
|
_, actual := client.Verify("timeout", "timeout")
|
||||||
|
if !reflect.DeepEqual(reflect.TypeOf(actual), reflect.TypeOf(expected)) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testTools(code int, body string) (*httptest.Server, *Client) {
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(code)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprintln(w, body)
|
||||||
|
}))
|
||||||
|
|
||||||
|
client := &Client{URL: server.URL, TimeOut: time.Second * 2, Secret: "developerSecret"}
|
||||||
|
return server, client
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user