forked from Mirrors/go-iap
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90f2d8ac93 | ||
|
|
cb8b807b57 | ||
|
|
442bfb3f23 | ||
|
|
2e626201fb | ||
|
|
e0fce7e253 | ||
|
|
6882b6758e | ||
|
|
bb35e9f2a8 | ||
|
|
d13032442f | ||
|
|
806cc5f85b | ||
|
|
47dd64806a | ||
|
|
5cc390497d | ||
|
|
7d9e71d01e | ||
|
|
deddee8776 | ||
|
|
a827229599 | ||
|
|
a86d8e5e46 | ||
|
|
d8e3214b87 | ||
|
|
2f225b82e1 | ||
|
|
9a16ac2219 | ||
|
|
55c7fd6ae1 | ||
|
|
acc524bc09 | ||
|
|
8f2ab57531 | ||
|
|
5d0e393a7d | ||
|
|
8f84c4951b | ||
|
|
5abc018f37 | ||
|
|
b4778a6542 | ||
|
|
28613a7b42 | ||
|
|
e948691645 | ||
|
|
1b4f9f5ba9 | ||
|
|
eb5fd1fd67 | ||
|
|
cb6856d274 | ||
|
|
c16a822c91 | ||
|
|
3e4cb3beda | ||
|
|
53f8c45c39 | ||
|
|
e39806cf34 | ||
|
|
51232bd52a | ||
|
|
5994fb99fd | ||
|
|
9666be3e03 | ||
|
|
8ddfbdb30a | ||
|
|
dd8af1ccd2 | ||
|
|
9ba6e70200 | ||
|
|
052ce72134 | ||
|
|
a3f651b46e | ||
|
|
588d81166b | ||
|
|
a61c519ac8 | ||
|
|
3c88ce1648 | ||
|
|
0aa274084a | ||
|
|
c61599bf8e | ||
|
|
e54455a635 | ||
|
|
d9ae3a1d88 | ||
|
|
b739b19032 | ||
|
|
be7b768650 | ||
|
|
98cf7b036f | ||
|
|
8359bd764f | ||
|
|
3fcc899200 | ||
|
|
31f386e220 | ||
|
|
ac84b97cd8 | ||
|
|
c4c303e812 | ||
|
|
b5222c00cf | ||
|
|
c8962b67cb | ||
|
|
627fa5e7d1 |
@@ -1,6 +1,6 @@
|
|||||||
language: go
|
language: go
|
||||||
go:
|
go:
|
||||||
- 1.12.5
|
- 1.13.x
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
- GO111MODULE=on
|
- GO111MODULE=on
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -1,15 +1,22 @@
|
|||||||
|
.PHONEY: all
|
||||||
.PHONEY: all setup test cover
|
|
||||||
|
|
||||||
all: setup cover
|
all: setup cover
|
||||||
|
|
||||||
|
.PHONEY: setup
|
||||||
setup:
|
setup:
|
||||||
go get golang.org/x/tools/cmd/cover
|
go get golang.org/x/tools/cmd/cover
|
||||||
go get google.golang.org/appengine/urlfetch
|
go get google.golang.org/appengine/urlfetch
|
||||||
go get ./...
|
go get ./...
|
||||||
|
|
||||||
|
.PHONEY: test
|
||||||
test:
|
test:
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
|
.PHONEY: cover
|
||||||
cover:
|
cover:
|
||||||
go test -coverprofile=coverage.txt ./...
|
go test -coverprofile=coverage.txt ./...
|
||||||
|
|
||||||
|
.PHONEY: generate
|
||||||
|
generate:
|
||||||
|
rm -rf ./appstore/mocks/*
|
||||||
|
rm -rf ./playstore/mocks/*
|
||||||
|
go generate ./...
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -1,7 +1,7 @@
|
|||||||
go-iap
|
go-iap
|
||||||
======
|
======
|
||||||
|
|
||||||

|

|
||||||
[](https://travis-ci.org/awa/go-iap)
|
[](https://travis-ci.org/awa/go-iap)
|
||||||
[](https://codecov.io/github/awa/go-iap?branch=master)
|
[](https://codecov.io/github/awa/go-iap?branch=master)
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ Current API Documents:
|
|||||||
* AppStore: [](https://godoc.org/github.com/awa/go-iap/appstore)
|
* AppStore: [](https://godoc.org/github.com/awa/go-iap/appstore)
|
||||||
* GooglePlay: [](https://godoc.org/github.com/awa/go-iap/playstore)
|
* GooglePlay: [](https://godoc.org/github.com/awa/go-iap/playstore)
|
||||||
* Amazon AppStore: [](https://godoc.org/github.com/awa/go-iap/amazon)
|
* Amazon AppStore: [](https://godoc.org/github.com/awa/go-iap/amazon)
|
||||||
|
* Huawei HMS: [](https://godoc.org/github.com/awa/go-iap/hms)
|
||||||
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
@@ -19,6 +20,7 @@ Current API Documents:
|
|||||||
go get github.com/awa/go-iap/appstore
|
go get github.com/awa/go-iap/appstore
|
||||||
go get github.com/awa/go-iap/playstore
|
go get github.com/awa/go-iap/playstore
|
||||||
go get github.com/awa/go-iap/amazon
|
go get github.com/awa/go-iap/amazon
|
||||||
|
go get github.com/awa/go-iap/hms
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -78,6 +80,23 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### In App Purchase (via Huawei Mobile Services)
|
||||||
|
|
||||||
|
```
|
||||||
|
import(
|
||||||
|
"github.com/awa/go-iap/hms"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// If "orderSiteURL" and/or "subscriptionSiteURL" are empty,
|
||||||
|
// they will be default to AppTouch German.
|
||||||
|
// Please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-common-statement-0000001050986127-V5 for details.
|
||||||
|
client := hms.New("clientID", "clientSecret", "orderSiteURL", "subscriptionSiteURL")
|
||||||
|
ctx := context.Background()
|
||||||
|
resp, err := client.VerifySubscription(ctx, "purchaseToken", "subscriptionID", 1)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
# ToDo
|
# ToDo
|
||||||
- [x] Validator for In App Purchase Receipt (AppStore)
|
- [x] Validator for In App Purchase Receipt (AppStore)
|
||||||
- [x] Validator for Subscription token (GooglePlay)
|
- [x] Validator for Subscription token (GooglePlay)
|
||||||
@@ -96,6 +115,9 @@ This validator uses [Version 3 API](http://developer.android.com/google/play/bil
|
|||||||
### In App Purchase (Amazon)
|
### 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).
|
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).
|
||||||
|
|
||||||
|
### In App Purchase (HMS)
|
||||||
|
This validator uses [Version 2 API](https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-common-statement-0000001050986127-V5).
|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
go-iap is licensed under the MIT.
|
go-iap is licensed under the MIT.
|
||||||
|
|||||||
49
appstore/mocks/appstore.go
Normal file
49
appstore/mocks/appstore.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/awa/go-iap/appstore (interfaces: IAPClient)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
appstore "github.com/awa/go-iap/appstore"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockIAPClient is a mock of IAPClient interface
|
||||||
|
type MockIAPClient struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockIAPClientMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIAPClientMockRecorder is the mock recorder for MockIAPClient
|
||||||
|
type MockIAPClientMockRecorder struct {
|
||||||
|
mock *MockIAPClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockIAPClient creates a new mock instance
|
||||||
|
func NewMockIAPClient(ctrl *gomock.Controller) *MockIAPClient {
|
||||||
|
mock := &MockIAPClient{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockIAPClientMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockIAPClient) EXPECT() *MockIAPClientMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify mocks base method
|
||||||
|
func (m *MockIAPClient) Verify(arg0 context.Context, arg1 appstore.IAPRequest, arg2 interface{}) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Verify", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify indicates an expected call of Verify
|
||||||
|
func (mr *MockIAPClientMockRecorder) Verify(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verify", reflect.TypeOf((*MockIAPClient)(nil).Verify), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ type (
|
|||||||
|
|
||||||
IsTrialPeriod string `json:"is_trial_period"`
|
IsTrialPeriod string `json:"is_trial_period"`
|
||||||
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period,omitempty"`
|
IsInIntroOfferPeriod string `json:"is_in_intro_offer_period,omitempty"`
|
||||||
IsUpgraded string `json:"is_upgraded"`
|
IsUpgraded string `json:"is_upgraded,omitempty"`
|
||||||
|
|
||||||
ExpiresDate
|
ExpiresDate
|
||||||
|
|
||||||
@@ -150,6 +150,19 @@ type (
|
|||||||
IsRetryable bool `json:"is-retryable,omitempty"`
|
IsRetryable bool `json:"is-retryable,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The IAPLatestResponse type has the response properties
|
||||||
|
// If you use latest_receipt as token to verify, response should be like following struct
|
||||||
|
IAPLatestResponse struct {
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
Receipt InApp `json:"receipt"`
|
||||||
|
LatestReceiptInfo InApp `json:"latest_receipt_info,omitempty"`
|
||||||
|
LatestExpiredReceiptInfo InApp `json:"latest_expired_receipt_info,omitempty"`
|
||||||
|
LatestReceipt string `json:"latest_receipt,omitempty"`
|
||||||
|
SubscriptionAutoRenewStatus interface{} `json:"auto_renew_status,omitempty"`
|
||||||
|
SubscriptionAutoRenewProductID string `json:"auto_renew_product_id,omitempty"`
|
||||||
|
SubscriptionRetryFlag string `json:"is_in_billing_retry_period,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// The HttpStatusResponse struct contains the status code returned by the store
|
// The HttpStatusResponse struct contains the status code returned by the store
|
||||||
// Used as a workaround to detect when to hit the production appstore or sandbox appstore regardless of receipt type
|
// Used as a workaround to detect when to hit the production appstore or sandbox appstore regardless of receipt type
|
||||||
StatusResponse struct {
|
StatusResponse struct {
|
||||||
@@ -163,8 +176,10 @@ type (
|
|||||||
CancellationReason string `json:"cancellation_reason,omitempty"`
|
CancellationReason string `json:"cancellation_reason,omitempty"`
|
||||||
ExpirationIntent string `json:"expiration_intent,omitempty"`
|
ExpirationIntent string `json:"expiration_intent,omitempty"`
|
||||||
IsInBillingRetryPeriod string `json:"is_in_billing_retry_period,omitempty"`
|
IsInBillingRetryPeriod string `json:"is_in_billing_retry_period,omitempty"`
|
||||||
LatestReceiptInfo ReceiptForIOS6 `json:"latest_expired_receipt_info"`
|
|
||||||
Receipt ReceiptForIOS6 `json:"receipt"`
|
Receipt ReceiptForIOS6 `json:"receipt"`
|
||||||
|
LatestExpiredReceiptInfo ReceiptForIOS6 `json:"latest_expired_receipt_info"`
|
||||||
|
LatestReceipt string `json:"latest_receipt"`
|
||||||
|
LatestReceiptInfo ReceiptForIOS6 `json:"latest_receipt_info"`
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ const (
|
|||||||
NotificationTypeDidChangeRenewalPreference NotificationType = "DID_CHANGE_RENEWAL_PREF"
|
NotificationTypeDidChangeRenewalPreference NotificationType = "DID_CHANGE_RENEWAL_PREF"
|
||||||
// Customer changed the subscription renewal status. Current active plan is not affected.
|
// Customer changed the subscription renewal status. Current active plan is not affected.
|
||||||
NotificationTypeDidChangeRenewalStatus NotificationType = "DID_CHANGE_RENEWAL_STATUS"
|
NotificationTypeDidChangeRenewalStatus NotificationType = "DID_CHANGE_RENEWAL_STATUS"
|
||||||
|
// Subscription failed to renew due to a billing issue.
|
||||||
|
NotificationTypeDidFailToRenew NotificationType = "DID_FAIL_TO_RENEW"
|
||||||
|
// Indicates that App Store successfully refunded a transaction
|
||||||
|
NotificationTypeRefund NotificationType = "REFUND"
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NotificationEnvironment string
|
type NotificationEnvironment string
|
||||||
@@ -54,10 +59,11 @@ type NotificationReceipt struct {
|
|||||||
OriginalPurchaseDate
|
OriginalPurchaseDate
|
||||||
NotificationExpiresDate
|
NotificationExpiresDate
|
||||||
CancellationDate
|
CancellationDate
|
||||||
|
CancellationReason string `json:"cancellation_reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationUnifiedReceipt struct {
|
type NotificationUnifiedReceipt struct {
|
||||||
Status string `json:"status"`
|
Status int `json:"status"`
|
||||||
Environment Environment `json:"environment"`
|
Environment Environment `json:"environment"`
|
||||||
LatestReceipt string `json:"latest_receipt"`
|
LatestReceipt string `json:"latest_receipt"`
|
||||||
LatestReceiptInfo []InApp `json:"latest_receipt_info"`
|
LatestReceiptInfo []InApp `json:"latest_receipt_info"`
|
||||||
@@ -85,19 +91,29 @@ type SubscriptionNotification struct {
|
|||||||
AutoRenewStatus string `json:"auto_renew_status"` // false or true
|
AutoRenewStatus string `json:"auto_renew_status"` // false or true
|
||||||
AutoRenewProductID string `json:"auto_renew_product_id"`
|
AutoRenewProductID string `json:"auto_renew_product_id"`
|
||||||
|
|
||||||
|
// HACK (msyrus): Separate Subscriptiton Notification from Notification verification response
|
||||||
|
Status int `json:"status,omitempty"`
|
||||||
|
Receipt NotificationReceipt `json:"receipt"`
|
||||||
|
SubscriptionRetryFlag string `json:"is_in_billing_retry_period"`
|
||||||
|
|
||||||
// Posted if the notification_type is RENEWAL or INTERACTIVE_RENEWAL, and only if the renewal is successful.
|
// Posted if the notification_type is RENEWAL or INTERACTIVE_RENEWAL, and only if the renewal is successful.
|
||||||
// Posted also if the notification_type is INITIAL_BUY.
|
// Posted also if the notification_type is INITIAL_BUY.
|
||||||
// Not posted for notification_type CANCEL.
|
// Not posted for notification_type CANCEL.
|
||||||
|
// Deprecated: use UnifiedReceipt.LatestReceipt instead. See details: https://developer.apple.com/documentation/appstoreservernotifications/ .
|
||||||
LatestReceipt string `json:"latest_receipt"`
|
LatestReceipt string `json:"latest_receipt"`
|
||||||
|
// Deprecated: use UnifiedReceipt.LatestReceiptInfo instead. See details: https://developer.apple.com/documentation/appstoreservernotifications/ .
|
||||||
LatestReceiptInfo NotificationReceipt `json:"latest_receipt_info"`
|
LatestReceiptInfo NotificationReceipt `json:"latest_receipt_info"`
|
||||||
|
|
||||||
// In the new notifications above properties latest_receipt, latest_receipt_info are moved under this one
|
// In the new notifications above properties latest_receipt, latest_receipt_info are moved under this one
|
||||||
UnifiedReceipt NotificationUnifiedReceipt `json:"unified_receipt"`
|
UnifiedReceipt NotificationUnifiedReceipt `json:"unified_receipt"`
|
||||||
|
|
||||||
// Posted only if the notification_type is RENEWAL or CANCEL or if renewal failed and subscription expired.
|
// Posted only if the notification_type is RENEWAL or CANCEL or if renewal failed and subscription expired.
|
||||||
|
// Deprecated: see details: https://developer.apple.com/documentation/appstoreservernotifications/ .
|
||||||
LatestExpiredReceipt string `json:"latest_expired_receipt"`
|
LatestExpiredReceipt string `json:"latest_expired_receipt"`
|
||||||
|
// Deprecated: see details: https://developer.apple.com/documentation/appstoreservernotifications/ .
|
||||||
LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"`
|
LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"`
|
||||||
|
|
||||||
// Posted only if the notification_type is CANCEL.
|
// Posted only if the notification_type is CANCEL.
|
||||||
CancellationDate
|
CancellationDate
|
||||||
|
CancellationReason string `json:"cancellation_reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks/appstore.go -package=mocks github.com/awa/go-iap/appstore IAPClient
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SandboxURL is the endpoint for sandbox environment.
|
// SandboxURL is the endpoint for sandbox environment.
|
||||||
SandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt"
|
SandboxURL string = "https://sandbox.itunes.apple.com/verifyReceipt"
|
||||||
@@ -31,47 +34,55 @@ type Client struct {
|
|||||||
httpCli *http.Client
|
httpCli *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAppStoreServer = errors.New("AppStore server error")
|
||||||
|
|
||||||
|
ErrInvalidJSON = errors.New("The App Store could not read the JSON object you provided.")
|
||||||
|
ErrInvalidReceiptData = errors.New("The data in the receipt-data property was malformed or missing.")
|
||||||
|
ErrReceiptUnauthenticated = errors.New("The receipt could not be authenticated.")
|
||||||
|
ErrInvalidSharedSecret = errors.New("The shared secret you provided does not match the shared secret on file for your account.")
|
||||||
|
ErrServerUnavailable = errors.New("The receipt server is not currently available.")
|
||||||
|
ErrReceiptIsForTest = 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.")
|
||||||
|
ErrReceiptIsForProduction = 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.")
|
||||||
|
ErrReceiptUnauthorized = errors.New("This receipt could not be authorized. Treat this the same as if a purchase was never made.")
|
||||||
|
|
||||||
|
ErrInternalDataAccessError = errors.New("Internal data access error.")
|
||||||
|
ErrUnknown = errors.New("An unknown error occurred")
|
||||||
|
)
|
||||||
|
|
||||||
// HandleError returns error message by status code
|
// HandleError returns error message by status code
|
||||||
func HandleError(status int) error {
|
func HandleError(status int) error {
|
||||||
var message string
|
var e error
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case 0:
|
case 0:
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
case 21000:
|
case 21000:
|
||||||
message = "The App Store could not read the JSON object you provided."
|
e = ErrInvalidJSON
|
||||||
|
|
||||||
case 21002:
|
case 21002:
|
||||||
message = "The data in the receipt-data property was malformed or missing."
|
e = ErrInvalidReceiptData
|
||||||
|
|
||||||
case 21003:
|
case 21003:
|
||||||
message = "The receipt could not be authenticated."
|
e = ErrReceiptUnauthenticated
|
||||||
|
|
||||||
case 21004:
|
case 21004:
|
||||||
message = "The shared secret you provided does not match the shared secret on file for your account."
|
e = ErrInvalidSharedSecret
|
||||||
|
|
||||||
case 21005:
|
case 21005:
|
||||||
message = "The receipt server is not currently available."
|
e = ErrServerUnavailable
|
||||||
|
|
||||||
case 21007:
|
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."
|
e = ErrReceiptIsForTest
|
||||||
|
|
||||||
case 21008:
|
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."
|
e = ErrReceiptIsForProduction
|
||||||
|
case 21009:
|
||||||
|
e = ErrInternalDataAccessError
|
||||||
case 21010:
|
case 21010:
|
||||||
message = "This receipt could not be authorized. Treat this the same as if a purchase was never made."
|
e = ErrReceiptUnauthorized
|
||||||
|
|
||||||
default:
|
default:
|
||||||
if status >= 21100 && status <= 21199 {
|
if status >= 21100 && status <= 21199 {
|
||||||
message = "Internal data access error."
|
e = ErrInternalDataAccessError
|
||||||
} else {
|
} else {
|
||||||
message = "An unknown error occurred"
|
e = ErrUnknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors.New(message)
|
return fmt.Errorf("status %d: %w", status, e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a client object
|
// New creates a client object
|
||||||
@@ -80,7 +91,7 @@ func New() *Client {
|
|||||||
ProductionURL: ProductionURL,
|
ProductionURL: ProductionURL,
|
||||||
SandboxURL: SandboxURL,
|
SandboxURL: SandboxURL,
|
||||||
httpCli: &http.Client{
|
httpCli: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
@@ -96,64 +107,70 @@ func NewWithClient(client *http.Client) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify sends receipts and gets validation result
|
// Verify sends receipts and gets validation result
|
||||||
func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) error {
|
func (c *Client) Verify(ctx context.Context, reqBody IAPRequest, result interface{}) (Environment, error) {
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
|
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", c.ProductionURL, b)
|
req, err := http.NewRequest("POST", c.ProductionURL, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", ContentType)
|
req.Header.Set("Content-Type", ContentType)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
resp, err := c.httpCli.Do(req)
|
resp, err := c.httpCli.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 500 {
|
||||||
|
return "", fmt.Errorf("Received http status code %d from the App Store: %w", resp.StatusCode, ErrAppStoreServer)
|
||||||
|
}
|
||||||
return c.parseResponse(resp, result, ctx, reqBody)
|
return c.parseResponse(resp, result, ctx, reqBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) error {
|
func (c *Client) parseResponse(resp *http.Response, result interface{}, ctx context.Context, reqBody IAPRequest) (Environment, error) {
|
||||||
// Read the body now so that we can unmarshal it twice
|
// Read the body now so that we can unmarshal it twice
|
||||||
buf, err := ioutil.ReadAll(resp.Body)
|
buf, err := ioutil.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(buf, &result)
|
err = json.Unmarshal(buf, &result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL
|
// https://developer.apple.com/library/content/technotes/tn2413/_index.html#//apple_ref/doc/uid/DTS40016228-CH1-RECEIPTURL
|
||||||
var r StatusResponse
|
var r StatusResponse
|
||||||
err = json.Unmarshal(buf, &r)
|
err = json.Unmarshal(buf, &r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
if r.Status == 21007 {
|
if r.Status == 21007 {
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
|
if err := json.NewEncoder(b).Encode(reqBody); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := http.NewRequest("POST", c.SandboxURL, b)
|
req, err := http.NewRequest("POST", c.SandboxURL, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", ContentType)
|
req.Header.Set("Content-Type", ContentType)
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
resp, err := c.httpCli.Do(req)
|
resp, err := c.httpCli.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= 500 {
|
||||||
return json.NewDecoder(resp.Body).Decode(result)
|
return Sandbox, fmt.Errorf("Received http status code %d from the App Store Sandbox: %w", resp.StatusCode, ErrAppStoreServer)
|
||||||
|
}
|
||||||
|
// 21007 is found when the receipt is from the test environment
|
||||||
|
return Sandbox, json.NewDecoder(resp.Body).Decode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return Production, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,52 +26,57 @@ func TestHandleError(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "status 21000",
|
name: "status 21000",
|
||||||
in: 21000,
|
in: 21000,
|
||||||
out: errors.New("The App Store could not read the JSON object you provided."),
|
out: ErrInvalidJSON,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21002",
|
name: "status 21002",
|
||||||
in: 21002,
|
in: 21002,
|
||||||
out: errors.New("The data in the receipt-data property was malformed or missing."),
|
out: ErrInvalidReceiptData,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21003",
|
name: "status 21003",
|
||||||
in: 21003,
|
in: 21003,
|
||||||
out: errors.New("The receipt could not be authenticated."),
|
out: ErrReceiptUnauthenticated,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21004",
|
name: "status 21004",
|
||||||
in: 21004,
|
in: 21004,
|
||||||
out: errors.New("The shared secret you provided does not match the shared secret on file for your account."),
|
out: ErrInvalidSharedSecret,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21005",
|
name: "status 21005",
|
||||||
in: 21005,
|
in: 21005,
|
||||||
out: errors.New("The receipt server is not currently available."),
|
out: ErrServerUnavailable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21007",
|
name: "status 21007",
|
||||||
in: 21007,
|
in: 21007,
|
||||||
out: 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."),
|
out: ErrReceiptIsForTest,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21008",
|
name: "status 21008",
|
||||||
in: 21008,
|
in: 21008,
|
||||||
out: 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."),
|
out: ErrReceiptIsForProduction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "status 21009",
|
||||||
|
in: 21009,
|
||||||
|
out: ErrInternalDataAccessError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21010",
|
name: "status 21010",
|
||||||
in: 21010,
|
in: 21010,
|
||||||
out: errors.New("This receipt could not be authorized. Treat this the same as if a purchase was never made."),
|
out: ErrReceiptUnauthorized,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status 21100 ~ 21199",
|
name: "status 21100 ~ 21199",
|
||||||
in: 21100,
|
in: 21100,
|
||||||
out: errors.New("Internal data access error."),
|
out: ErrInternalDataAccessError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "status unknown",
|
name: "status unknown",
|
||||||
in: 100,
|
in: 100,
|
||||||
out: errors.New("An unknown error occurred"),
|
out: ErrUnknown,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +84,7 @@ func TestHandleError(t *testing.T) {
|
|||||||
t.Run(v.name, func(t *testing.T) {
|
t.Run(v.name, func(t *testing.T) {
|
||||||
out := HandleError(v.in)
|
out := HandleError(v.in)
|
||||||
|
|
||||||
if !reflect.DeepEqual(out, v.out) {
|
if !errors.Is(out, v.out) {
|
||||||
t.Errorf("input: %d\ngot: %v\nwant: %v\n", v.in, out, v.out)
|
t.Errorf("input: %d\ngot: %v\nwant: %v\n", v.in, out, v.out)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -91,7 +96,7 @@ func TestNew(t *testing.T) {
|
|||||||
ProductionURL: ProductionURL,
|
ProductionURL: ProductionURL,
|
||||||
SandboxURL: SandboxURL,
|
SandboxURL: SandboxURL,
|
||||||
httpCli: &http.Client{
|
httpCli: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,29 +185,30 @@ func TestResponses(t *testing.T) {
|
|||||||
result := &IAPResponse{}
|
result := &IAPResponse{}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
name string
|
||||||
testServer *httptest.Server
|
testServer *httptest.Server
|
||||||
sandboxServ *httptest.Server
|
sandboxServ *httptest.Server
|
||||||
expected *IAPResponse
|
expected *IAPResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []testCase{
|
testCases := []testCase{
|
||||||
// VerifySandboxReceipt
|
|
||||||
{
|
{
|
||||||
|
name: "VerifySandboxReceipt",
|
||||||
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)),
|
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)),
|
||||||
sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)),
|
sandboxServ: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 0}`)),
|
||||||
expected: &IAPResponse{
|
expected: &IAPResponse{
|
||||||
Status: 0,
|
Status: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// VerifyBadPayload
|
|
||||||
{
|
{
|
||||||
|
name: "VerifyBadPayload",
|
||||||
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)),
|
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21002}`)),
|
||||||
expected: &IAPResponse{
|
expected: &IAPResponse{
|
||||||
Status: 21002,
|
Status: 21002,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// SuccessPayload
|
|
||||||
{
|
{
|
||||||
|
name: "SuccessPayload",
|
||||||
testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)),
|
testServer: httptest.NewServer(serverWithResponse(http.StatusBadRequest, `{"status": 0}`)),
|
||||||
expected: &IAPResponse{
|
expected: &IAPResponse{
|
||||||
Status: 0,
|
Status: 0,
|
||||||
@@ -213,7 +219,8 @@ func TestResponses(t *testing.T) {
|
|||||||
client := New()
|
client := New()
|
||||||
client.SandboxURL = "localhost"
|
client.SandboxURL = "localhost"
|
||||||
|
|
||||||
for i, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer tc.testServer.Close()
|
defer tc.testServer.Close()
|
||||||
client.ProductionURL = tc.testServer.URL
|
client.ProductionURL = tc.testServer.URL
|
||||||
if tc.sandboxServ != nil {
|
if tc.sandboxServ != nil {
|
||||||
@@ -223,47 +230,54 @@ func TestResponses(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := client.Verify(ctx, req, result)
|
err := client.Verify(ctx, req, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Test case %d - %s", i, err.Error())
|
t.Errorf("%s", err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(result, tc.expected) {
|
if !reflect.DeepEqual(result, tc.expected) {
|
||||||
t.Errorf("Test case %d - got %v\nwant %v", i, result, tc.expected)
|
t.Errorf("got %v\nwant %v", result, tc.expected)
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrors(t *testing.T) {
|
func TestHttpStatusErrors(t *testing.T) {
|
||||||
req := IAPRequest{
|
req := IAPRequest{
|
||||||
ReceiptData: "dummy data",
|
ReceiptData: "dummy data",
|
||||||
}
|
}
|
||||||
result := &IAPResponse{}
|
result := &IAPResponse{}
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
|
name string
|
||||||
testServer *httptest.Server
|
testServer *httptest.Server
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []testCase{
|
testCases := []testCase{
|
||||||
// VerifySandboxReceiptFailure
|
|
||||||
{
|
{
|
||||||
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21007}`)),
|
name: "status 200",
|
||||||
|
testServer: httptest.NewServer(serverWithResponse(http.StatusOK, `{"status": 21000}`)),
|
||||||
|
err: nil,
|
||||||
},
|
},
|
||||||
// VerifyBadResponse
|
|
||||||
{
|
{
|
||||||
|
name: "status 500",
|
||||||
testServer: httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)),
|
testServer: httptest.NewServer(serverWithResponse(http.StatusInternalServerError, `qwerty!@#$%^`)),
|
||||||
|
err: ErrAppStoreServer,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
client := New()
|
client := New()
|
||||||
client.SandboxURL = "localhost"
|
client.SandboxURL = "localhost"
|
||||||
|
|
||||||
for i, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
defer tc.testServer.Close()
|
defer tc.testServer.Close()
|
||||||
client.ProductionURL = tc.testServer.URL
|
client.ProductionURL = tc.testServer.URL
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
err := client.Verify(ctx, req, result)
|
err := client.Verify(ctx, req, result)
|
||||||
if err == nil {
|
if !errors.Is(err, tc.err) {
|
||||||
t.Errorf("Test case %d - expected error to be not nil since the sandbox is not responding", i)
|
t.Errorf("expected error to be not nil since the sandbox is not responding")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,12 +311,10 @@ func serverWithResponse(statusCode int, response string) http.Handler {
|
|||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if "POST" == r.Method {
|
if "POST" == r.Method {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
w.Write([]byte(response))
|
w.Write([]byte(response))
|
||||||
return
|
|
||||||
} else {
|
} else {
|
||||||
w.Write([]byte(`unsupported request`))
|
w.Write([]byte(`unsupported request`))
|
||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(statusCode)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -4,24 +4,13 @@ go 1.12
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.39.0 // indirect
|
cloud.google.com/go v0.39.0 // indirect
|
||||||
github.com/golang/mock v1.3.1 // indirect
|
github.com/golang/mock v1.4.0
|
||||||
github.com/google/btree v1.0.0 // indirect
|
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f // indirect
|
|
||||||
github.com/kr/pty v1.1.4 // indirect
|
|
||||||
go.opencensus.io v0.22.0 // indirect
|
go.opencensus.io v0.22.0 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 // indirect
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 // indirect
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522 // indirect
|
|
||||||
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff // indirect
|
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422 // indirect
|
|
||||||
golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171 // indirect
|
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0
|
||||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 // indirect
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
|
|
||||||
golang.org/x/tools v0.0.0-20190530215528-75312fb06703 // indirect
|
|
||||||
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f
|
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f
|
||||||
google.golang.org/appengine v1.6.0
|
google.golang.org/appengine v1.6.5
|
||||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect
|
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 // indirect
|
||||||
google.golang.org/grpc v1.21.0 // indirect
|
google.golang.org/grpc v1.21.0 // indirect
|
||||||
honnef.co/go/tools v0.0.0-20190530170028-a1efa522b896 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
56
go.sum
56
go.sum
@@ -4,71 +4,52 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
|
|||||||
cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
|
cloud.google.com/go v0.39.0 h1:UgQP9na6OTfp4dsAiz/eFpFA1C6tPdH5wiRdi19tuMw=
|
||||||
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
|
cloud.google.com/go v0.39.0/go.mod h1:rVLT6fkc8chs9sfPtFc1SBH6em7n+ZoXaG+87tDISts=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
|
github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
|
||||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
github.com/golang/mock v1.4.0 h1:Rd1kQnQu0Hq3qvJppYSG0HtP+f5LPPUiDswTLiEegLg=
|
||||||
|
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
|
||||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||||
|
github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
|
||||||
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
|
go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg=
|
||||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||||
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
|
||||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
|
||||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
|
||||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
|
||||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
|
||||||
golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
|
||||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
|
||||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07 h1:XC1K3wNjuz44KaI+cj85C9TW85w/46RH7J+DTXNH5Wk=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190517181255-950ef44c6e07/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
|
||||||
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -78,27 +59,22 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1 h1:R4dVlxdmKenVdMRS/tTspEpSTRWINYrHD8ySIU9yCIU=
|
||||||
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
|
||||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||||
golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
|
||||||
golang.org/x/tools v0.0.0-20190530215528-75312fb06703/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
|
||||||
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
|
google.golang.org/api v0.5.0 h1:lj9SyhMzyoa38fgFF0oO2T6pjs5IzkLPKfVtxpyCRMM=
|
||||||
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
google.golang.org/api v0.5.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||||
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f h1:tAgkkP6ovjCY8HraRtpXwh0CVqHwGqEAVLHwXyfFjIM=
|
google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f h1:tAgkkP6ovjCY8HraRtpXwh0CVqHwGqEAVLHwXyfFjIM=
|
||||||
@@ -106,15 +82,13 @@ google.golang.org/api v0.5.1-0.20190526001144-9f3a303b451f/go.mod h1:8k5glujaEP+
|
|||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
|
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||||
google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.6.5/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-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
|
||||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||||
google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69 h1:4rNOqY4ULrKzS6twXa619uQgI7h9PaVd4ZhjFQ7C5zs=
|
|
||||||
google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
|
||||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4=
|
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101 h1:wuGevabY6r+ivPNagjUXGGxF+GqgMd+dBhjsxW4q9u4=
|
||||||
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
|
||||||
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
|
google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
|
||||||
@@ -122,9 +96,7 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi
|
|||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
|
||||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||||
honnef.co/go/tools v0.0.0-20190530170028-a1efa522b896/go.mod h1:wtc9q0E9zm8PjdRMh29DPlTlCCHVzKDwnkT4GskQVzg=
|
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||||
|
|||||||
205
hms/client.go
Normal file
205
hms/client.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
package hms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HMS OAuth url
|
||||||
|
const tokenURL = "https://oauth-login.cloud.huawei.com/oauth2/v3/token"
|
||||||
|
|
||||||
|
// AccessToken expires grace period in seconds.
|
||||||
|
// The actural ExpiredAt will be substracted with this number to avoid boundray problems.
|
||||||
|
const accessTokenExpiresGracePeriod = 60
|
||||||
|
|
||||||
|
// global variable to store API AccessToken.
|
||||||
|
// All clients within an instance share one AccessToken grantee scalebility and to avoid rate limit.
|
||||||
|
var applicationAccessTokens = make(map[[16]byte]ApplicationAccessToken)
|
||||||
|
|
||||||
|
// lock when writing to applicationAccessTokens map
|
||||||
|
var applicationAccessTokensLock sync.Mutex
|
||||||
|
|
||||||
|
// ApplicationAccessToken model, received from HMS OAuth API
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/open-platform-oauth-0000001050123437-V5#EN-US_TOPIC_0000001050123437__section12493191334711
|
||||||
|
type ApplicationAccessToken struct {
|
||||||
|
// App-level access token.
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
|
||||||
|
// Remaining validity period of an access token, in seconds.
|
||||||
|
ExpiresIn int64 `json:"expires_in"`
|
||||||
|
// This value is always Bearer, indicating the type of the returned access token.
|
||||||
|
// TokenType string `json:"token_type"`
|
||||||
|
|
||||||
|
// Save the timestamp when AccessToken is obtained
|
||||||
|
ExpiredAt int64 `json:"-"`
|
||||||
|
|
||||||
|
// Request header string
|
||||||
|
HeaderString string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client implements VerifySignature, VerifyOrder and VerifySubscription methods
|
||||||
|
type Client struct {
|
||||||
|
clientID string
|
||||||
|
clientSecret string
|
||||||
|
clientIDSecretHash [16]byte
|
||||||
|
httpCli *http.Client
|
||||||
|
orderSiteURL string // site URL to request order information
|
||||||
|
subscriptionSiteURL string // site URL to request subscription information
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns client with credentials.
|
||||||
|
// Required client_id and client_secret which could be acquired from the HMS API Console.
|
||||||
|
// When user accountFlag is not equals to 1, orderSiteURL/subscriptionSiteURL are the site URLs that will be used to connect to HMS IAP API services.
|
||||||
|
// If orderSiteURL or subscriptionSiteURL are not set, default to AppTouch Germany site.
|
||||||
|
//
|
||||||
|
// Please refer https://developer.huawei.com/consumer/en/doc/start/api-console-guide
|
||||||
|
// and https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-common-statement-0000001050986127-V5 for details.
|
||||||
|
func New(clientID, clientSecret, orderSiteURL, subscriptionSiteURL string) *Client {
|
||||||
|
// Set default order / subscription iap site to AppTouch Germany if it is not provided
|
||||||
|
if !strings.HasPrefix(orderSiteURL, "http") {
|
||||||
|
orderSiteURL = "https://orders-at-dre.iap.dbankcloud.com"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(subscriptionSiteURL, "http") {
|
||||||
|
subscriptionSiteURL = "https://subscr-at-dre.iap.dbankcloud.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create http client
|
||||||
|
return &Client{
|
||||||
|
clientID: clientID,
|
||||||
|
clientSecret: clientSecret,
|
||||||
|
clientIDSecretHash: md5.Sum([]byte(clientID + clientSecret)),
|
||||||
|
httpCli: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
orderSiteURL: orderSiteURL,
|
||||||
|
subscriptionSiteURL: subscriptionSiteURL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetApplicationAccessTokenHeader obtain OAuth AccessToken from HMS
|
||||||
|
//
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/atdemo.go#L37
|
||||||
|
func (c *Client) GetApplicationAccessTokenHeader() (string, error) {
|
||||||
|
// To complie with the rate limit (1000/5min as of July 24th, 2020)
|
||||||
|
// new AccessTokens are requested only when it is expired.
|
||||||
|
// Please refer https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/open-platform-oauth-0000001050123437-V5 for detailes
|
||||||
|
if applicationAccessTokens[c.clientIDSecretHash].ExpiredAt > time.Now().Unix() {
|
||||||
|
return applicationAccessTokens[c.clientIDSecretHash].HeaderString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
urlValue := url.Values{"grant_type": {"client_credentials"}, "client_secret": {c.clientSecret}, "client_id": {c.clientID}}
|
||||||
|
resp, err := c.httpCli.PostForm(tokenURL, urlValue)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var atResponse ApplicationAccessToken
|
||||||
|
json.Unmarshal(bodyBytes, &atResponse)
|
||||||
|
if atResponse.AccessToken != "" {
|
||||||
|
// update expire time
|
||||||
|
atResponse.ExpiredAt = atResponse.ExpiresIn + time.Now().Unix() - accessTokenExpiresGracePeriod
|
||||||
|
// parse request header string
|
||||||
|
atResponse.HeaderString = fmt.Sprintf(
|
||||||
|
"Basic %s",
|
||||||
|
base64.StdEncoding.EncodeToString([]byte(
|
||||||
|
fmt.Sprintf("APPAT:%s",
|
||||||
|
atResponse.AccessToken,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
// save AccessToken info to global variable
|
||||||
|
applicationAccessTokensLock.Lock()
|
||||||
|
applicationAccessTokens[c.clientIDSecretHash] = atResponse
|
||||||
|
applicationAccessTokensLock.Unlock()
|
||||||
|
return atResponse.HeaderString, nil
|
||||||
|
}
|
||||||
|
return "", errors.New("Get token fail, " + string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns root order URL by flag, prefixing with "https://"
|
||||||
|
func (c *Client) getRootOrderURLByFlag(flag int64) string {
|
||||||
|
switch flag {
|
||||||
|
case 1:
|
||||||
|
return "https://orders-at-dre.iap.dbankcloud.com"
|
||||||
|
}
|
||||||
|
return c.orderSiteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns root subscription URL by flag, prefixing with "https://"
|
||||||
|
func (c *Client) getRootSubscriptionURLByFlag(flag int64) string {
|
||||||
|
switch flag {
|
||||||
|
case 1:
|
||||||
|
return "https://subscr-at-dre.iap.dbankcloud.com"
|
||||||
|
}
|
||||||
|
return c.subscriptionSiteURL
|
||||||
|
}
|
||||||
|
|
||||||
|
// get error based on result code returned from api
|
||||||
|
func (c *Client) getResponseErrorByCode(code string) error {
|
||||||
|
switch code {
|
||||||
|
case "0":
|
||||||
|
return nil
|
||||||
|
case "5":
|
||||||
|
return ErrorResponseInvalidParameter
|
||||||
|
case "6":
|
||||||
|
return ErrorResponseCritical
|
||||||
|
case "8":
|
||||||
|
return ErrorResponseProductNotBelongToUser
|
||||||
|
case "9":
|
||||||
|
return ErrorResponseConsumedProduct
|
||||||
|
case "11":
|
||||||
|
return ErrorResponseAbnormalUserAccount
|
||||||
|
default:
|
||||||
|
return ErrorResponseUnknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
|
||||||
|
// ErrorResponseUnknown error placeholder for undocumented errors
|
||||||
|
var ErrorResponseUnknown error = errors.New("Unknown error from API response")
|
||||||
|
|
||||||
|
// ErrorResponseSignatureVerifyFailed failed to verify dataSignature against the response json string.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
|
||||||
|
// var ErrorResponseSignatureVerifyFailed error = errors.New("Failed to verify dataSignature against the response json string")
|
||||||
|
|
||||||
|
// ErrorResponseInvalidParameter The parameter passed to the API is invalid.
|
||||||
|
// This error may also indicate that an agreement is not signed or parameters are not set correctly for the in-app purchase settlement in HUAWEI IAP, or the required permission is not in the list.
|
||||||
|
//
|
||||||
|
// Check whether the parameter passed to the API is correctly set. If so, check whether required settings in HUAWEI IAP are correctly configured.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
var ErrorResponseInvalidParameter error = errors.New("The parameter passed to the API is invalid")
|
||||||
|
|
||||||
|
// ErrorResponseCritical A critical error occurs during API operations.
|
||||||
|
//
|
||||||
|
// Rectify the fault based on the error information in the response. If the fault persists, contact Huawei technical support.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
var ErrorResponseCritical error = errors.New("A critical error occurs during API operations")
|
||||||
|
|
||||||
|
// ErrorResponseProductNotBelongToUser A user failed to consume or confirm a product because the user does not own the product.
|
||||||
|
//
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
var ErrorResponseProductNotBelongToUser error = errors.New("A user failed to consume or confirm a product because the user does not own the product")
|
||||||
|
|
||||||
|
// ErrorResponseConsumedProduct The product cannot be consumed or confirmed because it has been consumed or confirmed.
|
||||||
|
//
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
var ErrorResponseConsumedProduct error = errors.New("The product cannot be consumed or confirmed because it has been consumed or confirmed")
|
||||||
|
|
||||||
|
// ErrorResponseAbnormalUserAccount The user account is abnormal, for example, the user has been deregistered.
|
||||||
|
//
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
var ErrorResponseAbnormalUserAccount error = errors.New("The user account is abnormal, for example, the user has been deregistered")
|
||||||
488
hms/model.go
Normal file
488
hms/model.go
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
package hms
|
||||||
|
|
||||||
|
// InAppPurchaseData json model. Used when requesting In-App order and / or subscription verification.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
type InAppPurchaseData struct {
|
||||||
|
// App ID.
|
||||||
|
ApplicationID int64 `json:"applicationId"`
|
||||||
|
|
||||||
|
// For consumables or non-consumables, the value is always false.
|
||||||
|
// For subscriptions, possible values are:
|
||||||
|
// true: A subscription is in active state and will be automatically renewed on the next renewal date.
|
||||||
|
// false: A user has canceled the subscription. The user can access the subscribed content
|
||||||
|
// before the next renewal date but will be unable to access the content after the date
|
||||||
|
// unless the user enables automatic renewal. If a grace period is provided,
|
||||||
|
// this value remains true for the subscription as long as it is still in the grace period.
|
||||||
|
// The next settlement date is automatically extended every day until the grace period ends
|
||||||
|
// or the user changes the payment method.
|
||||||
|
AutoRenewing bool `json:"autoRenewing"`
|
||||||
|
|
||||||
|
// Order ID, which uniquely identifies a transaction and is generated by the Huawei IAP server during payment.
|
||||||
|
OrderID string `json:"orderId"`
|
||||||
|
|
||||||
|
// Product type. Possible values are:
|
||||||
|
// 0: consumable
|
||||||
|
// 1: non-consumable
|
||||||
|
// 2: subscription
|
||||||
|
Kind int64 `json:"kind"`
|
||||||
|
|
||||||
|
// App package name.
|
||||||
|
PackageName string `json:"packageName,omitempty"`
|
||||||
|
|
||||||
|
// Product ID. Each product must have a unique ID, which is maintained in the PMS or passed when the app initiates a purchase.
|
||||||
|
ProductID string `json:"productId"`
|
||||||
|
|
||||||
|
// Product name.
|
||||||
|
ProductName string `json:"productName,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp of the purchase time, which is the number of milliseconds from 00:00:00 on January 1, 1970 to the purchase time.
|
||||||
|
PurchaseTime int64 `json:"purchaseTime,omitempty"`
|
||||||
|
|
||||||
|
// Transaction status. Possible values are:
|
||||||
|
// -1: initialized
|
||||||
|
// 0: purchased
|
||||||
|
// 1: canceled
|
||||||
|
// 2: refunded
|
||||||
|
PurchaseState int64 `json:"purchaseState"`
|
||||||
|
|
||||||
|
// Information stored on the merchant side, which is passed by the app when the payment API is called.
|
||||||
|
DeveloperPayload string `json:"developerPayload,omitempty"`
|
||||||
|
|
||||||
|
// Challenge defined when an app initiates a consumption request.
|
||||||
|
// The challenge uniquely identifies the consumption request and exists only for one-off products.
|
||||||
|
DeveloperChallenge string `json:"developerChallenge,omitempty"`
|
||||||
|
|
||||||
|
// Consumption status, which exists only for one-off products. Possible values are:
|
||||||
|
// 0: not consumed
|
||||||
|
// 1: consumed
|
||||||
|
ConsumptionState string `json:"consumptionState,omitempty"`
|
||||||
|
|
||||||
|
// Purchase token, which uniquely identifies the mapping between a product and a user.
|
||||||
|
// It is generated by the Huawei IAP server when the payment is complete.
|
||||||
|
// NOTE:
|
||||||
|
// * This parameter uniquely identifies the mapping between a product and a user.
|
||||||
|
// It does not change when the subscription is renewed.
|
||||||
|
// * Currently, the value contains 92 characters and its length may be expanded.
|
||||||
|
// If the value needs to be stored, you are advised to reserve 128 characters.
|
||||||
|
PurchaseToken string `json:"purchaseToken"`
|
||||||
|
|
||||||
|
// Purchase type. Possible values are:
|
||||||
|
// 0: in the sandbox
|
||||||
|
// 1: in the promotion period (currently unsupported)
|
||||||
|
// This parameter is not returned during formal purchase.
|
||||||
|
// To avoid default value issues. check if PurchaseType != nil first, then read *PurchaseType.
|
||||||
|
PurchaseType *int64 `json:"purchaseType,omitempty"`
|
||||||
|
|
||||||
|
// Currency. The value must comply with the ISO 4217 standard.
|
||||||
|
Currency string `json:"currency,omitempty"`
|
||||||
|
|
||||||
|
// Value after the actual price of a product is multiplied by 100. The actual price is accurate to two decimal places.
|
||||||
|
// For example, if the value of this parameter is 501, the actual product price is 5.01.
|
||||||
|
Price int64 `json:"price,omitempty"`
|
||||||
|
|
||||||
|
// Country or region code, which is used to identify a country or region. The value must comply with the ISO 3166 standard.
|
||||||
|
Country string `json:"country,omitempty"`
|
||||||
|
|
||||||
|
// Payment method. Possible values are:
|
||||||
|
// 0: HUAWEI Point
|
||||||
|
// 3: credit card
|
||||||
|
// 4: Alipay
|
||||||
|
// 6: carrier billing
|
||||||
|
// 13: PayPal
|
||||||
|
// 16: debit card
|
||||||
|
// 17: WeChat Pay
|
||||||
|
// 19: gift card
|
||||||
|
// 20: balance
|
||||||
|
// 21: HUAWEI Point card
|
||||||
|
// 24: WorldPay
|
||||||
|
// 31: Huawei Pay
|
||||||
|
// 32: Ant Credit Pay
|
||||||
|
// 200: M-Pesa
|
||||||
|
PayType string `json:"payType,omitempty"`
|
||||||
|
|
||||||
|
// Transaction order ID.
|
||||||
|
PayOrderID string `json:"payOrderId,omitempty"`
|
||||||
|
|
||||||
|
// Account type. Possible values are:
|
||||||
|
// 0: HUAWEI ID
|
||||||
|
// 1: AppTouch user account
|
||||||
|
AccountFlag int64 `json:"accountFlag,omitempty"`
|
||||||
|
|
||||||
|
// ===== The following parameters are returned only in the subscription scenario. =====
|
||||||
|
|
||||||
|
// Order ID generated by the Huawei IAP server during fee deduction for the previous renewal.
|
||||||
|
// The parameter value is the same as that of orderId when a subscription is purchased for the first time.
|
||||||
|
LastOrderID string `json:"lastOrderId"`
|
||||||
|
|
||||||
|
// ID of the subscription group to which a subscription belongs.
|
||||||
|
ProductGroup string `json:"productGroup,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp of the purchase time, which is the number of milliseconds from 00:00:00 on January 1, 1970 to the purchase time.
|
||||||
|
//
|
||||||
|
// If the purchase is not complete, this parameter is left empty.
|
||||||
|
// PurchaseTime int64 `json:"purchaseTime,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp of the first fee deduction time, which is the number of
|
||||||
|
// milliseconds from 00:00:00 on January 1, 1970 to the first successful fee deduction time.
|
||||||
|
OriPurchaseTime int64 `json:"oriPurchaseTime,omitempty"`
|
||||||
|
|
||||||
|
// Subscription ID.
|
||||||
|
//
|
||||||
|
// NOTE: This parameter uniquely identifies the mapping between a product and a user. It does not change when the subscription is renewed.
|
||||||
|
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||||
|
|
||||||
|
// Original subscription ID. If this value is not empty, the current subscription is switched from another one.
|
||||||
|
// It can be linked to the original subscription information.
|
||||||
|
OriSubscriptionID string `json:"oriSubscriptionId,omitempty"`
|
||||||
|
|
||||||
|
// Purchase quantity.
|
||||||
|
Quantity int64 `json:"quantity,omitempty"`
|
||||||
|
|
||||||
|
// Days of a paid subscription, excluding the free trial period and promotion period.
|
||||||
|
DaysLasted int64 `json:"daysLasted,omitempty"`
|
||||||
|
|
||||||
|
// Number of successful renewal periods. How many time the subscription has been renewed, excludes promotion.
|
||||||
|
// If the value is 0 or does not exist, no renewal has been performed successfully.
|
||||||
|
NumOfPeriods int64 `json:"numOfPeriods,omitempty"`
|
||||||
|
|
||||||
|
// Number of successful renewal periods with promotion.
|
||||||
|
NumOfDiscount int64 `json:"numOfDiscount,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp of the subscription expiration time. In milliseconds.
|
||||||
|
//
|
||||||
|
// For an automatic renewal receipt where the fee has been deducted successfully, this time indicates the renewal date or expiration date.
|
||||||
|
// If the value is a past time, the subscription has expired.
|
||||||
|
ExpirationDate int64 `json:"expirationDate,omitempty"`
|
||||||
|
|
||||||
|
// Reason why a subscription expires. Possible values are:
|
||||||
|
// 1: canceled by a user
|
||||||
|
// 2: product being unavailable
|
||||||
|
// 3: abnormal user signing information
|
||||||
|
// 4: billing error
|
||||||
|
// 5: price increase disagreed with by a user
|
||||||
|
// 6: unknown error
|
||||||
|
// If there are multiple exceptions, a smaller number indicates a higher priority (1 > 2 > 3...).
|
||||||
|
ExpirationIntent int64 `json:"expirationIntent,omitempty"`
|
||||||
|
|
||||||
|
// Indicates whether the system is still trying to renew an expired subscription. Possible values are:
|
||||||
|
// 0: no
|
||||||
|
// 1: yes
|
||||||
|
RetryFlag int64 `json:"retryFlag,omitempty"`
|
||||||
|
|
||||||
|
// Indicates whether a subscription is in a renewal period with promotion. Possible values are:
|
||||||
|
// 0: no
|
||||||
|
// 1: yes
|
||||||
|
IntroductoryFlag int64 `json:"introductoryFlag,omitempty"`
|
||||||
|
|
||||||
|
// Indicates whether a subscription is in a free trial period. Possible values are:
|
||||||
|
// 0: no
|
||||||
|
// 1: yes
|
||||||
|
TrialFlag int64 `json:"trialFlag,omitempty"`
|
||||||
|
|
||||||
|
// Time when a subscription is revoked. A refund is made and the service is unavailable immediately.
|
||||||
|
// This value is returned when a user:
|
||||||
|
// a. makes a complaint and revokes a subscription through the customer service personnel; or
|
||||||
|
// b. performs subscription upgrade or crossgrade that immediately takes effect and revokes the previous subscription.
|
||||||
|
// Note: If a receipt is revoked, it is deemed that the purchase is not complete.
|
||||||
|
CancelTime int64 `json:"cancelTime,omitempty"`
|
||||||
|
|
||||||
|
// Cause of subscription cancellation. Possible values are:
|
||||||
|
// 0: other causes. For example, a user mistakenly purchases a subscription and has to cancel it.
|
||||||
|
// 1: A user encounters a problem within the app and cancels the subscription.
|
||||||
|
// 2: A user performs subscription upgrade or crossgrade.
|
||||||
|
// Note: If this parameter is left empty but the cancelTime parameter has a value,
|
||||||
|
// the cancellation is caused by an operation such as upgrade.
|
||||||
|
CancelReason int64 `json:"cancelReason,omitempty"`
|
||||||
|
|
||||||
|
// App information. This parameter is reserved.
|
||||||
|
AppInfo string `json:"appInfo,omitempty"`
|
||||||
|
|
||||||
|
// Indicates whether a user has disabled the subscription notification function. Possible values are:
|
||||||
|
// 0: no
|
||||||
|
// 1: yes
|
||||||
|
// If the user disables the subscription notification function, no subscription notification will be sent to this user.
|
||||||
|
NotifyClosed int64 `json:"notifyClosed,omitempty"`
|
||||||
|
|
||||||
|
// Renewal status. Possible values are:
|
||||||
|
// 1: The subscription renewal is normal.
|
||||||
|
// 0: The user cancels subscription renewal.
|
||||||
|
// For auto-renewable subscriptions, this parameter is valid for both valid and expired subscriptions. However, it does not represent users' subscription status. Generally, when the value is 0, the app can provide other subscription options for the user, for example, recommending another subscription with a lower level in the same group. The value 0 indicates that a user proactively cancels the subscription.
|
||||||
|
RenewStatus int64 `json:"renewStatus,omitempty"`
|
||||||
|
|
||||||
|
// User opinion on the price increase of a product. Possible values are:
|
||||||
|
// 1: The user has agreed with the price increase.
|
||||||
|
// 0: The user does not take any action. After the subscription expires, it becomes invalid.
|
||||||
|
PriceConsentStatus int64 `json:"priceConsentStatus,omitempty"`
|
||||||
|
|
||||||
|
// Price used upon the next renewal. It is provided as a reference for users when the priceConsentStatus parameter is returned.
|
||||||
|
RenewPrice int64 `json:"renewPrice,omitempty"`
|
||||||
|
|
||||||
|
// true: A user has been charged for a product, the product has not expired, and no refund has been made. In this case, you can provide services for the user.
|
||||||
|
// false: The purchase of a product is not finished, the product has expired, or a refund has been made for the product after its purchase.
|
||||||
|
//
|
||||||
|
// NOTE
|
||||||
|
// If a user has canceled a subscription, the subIsvalid parameter value is still true until the subscription expires.
|
||||||
|
SubIsValid bool `json:"subIsvalid,omitempty"`
|
||||||
|
|
||||||
|
// Indicates whether to postpone the settlement date.
|
||||||
|
// 1: yes
|
||||||
|
DeferFlag int64 `json:"deferFlag,omitempty"`
|
||||||
|
|
||||||
|
// Subscription cancellation initiator. Possible values are:
|
||||||
|
// 0: user
|
||||||
|
// 1: developer
|
||||||
|
// 2: Huawei
|
||||||
|
CancelWay int64 `json:"cancelWay,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp (milliseconds in UTC) when you set a subscription renewal to be stopped in the future.
|
||||||
|
// The subscription is still valid within the validity period, but the renewal will be stopped in the future. No refund is required.
|
||||||
|
// NOTE:
|
||||||
|
// cancelWay and cancellationTime are displayed when a subscription renewal stops (no refund is involved).
|
||||||
|
CancellationTime int64 `json:"cancellationTime,omitempty"`
|
||||||
|
|
||||||
|
// Number of days for retaining a subscription relationship after the subscription is canceled.
|
||||||
|
CancelledSubKeepDays int64 `json:"cancelledSubKeepDays,omitempty"`
|
||||||
|
|
||||||
|
// Confirmation status. Possible values are:
|
||||||
|
// 0: not confirmed
|
||||||
|
// 1: confirmed
|
||||||
|
// If this parameter is left empty, no confirmation is required.
|
||||||
|
Confirmed int64 `json:"confirmed,omitempty"`
|
||||||
|
|
||||||
|
// Timestamp (milliseconds in UTC) when a paused subscription is resumed.
|
||||||
|
ResumeTime int64 `json:"resumeTime,omitempty"`
|
||||||
|
|
||||||
|
// Cancellation reason. Possible values are:
|
||||||
|
// 0: others
|
||||||
|
// 1: too high fee
|
||||||
|
// 2: technical problem, for example, product not provided
|
||||||
|
// 5: in the blocklist because of fraud
|
||||||
|
// 7: subscription switchover
|
||||||
|
// 9: service being rarely used and not required
|
||||||
|
// 10: other better apps
|
||||||
|
SurveyReason int64 `json:"surveyReason,omitempty"`
|
||||||
|
|
||||||
|
// When the value of surveyReason is 0, this parameter is used to collect the cancellation reason entered by users.
|
||||||
|
SurveyDetails string `json:"surveyDetails,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanceledPurchaseList response from query canceled or refunded purchase list
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
|
||||||
|
type CanceledPurchaseList struct {
|
||||||
|
// Result code. Possible values are:
|
||||||
|
// 0: success
|
||||||
|
// Other values: failure. For details about the result codes, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
ResponseCode string `json:"responseCode"`
|
||||||
|
|
||||||
|
// Response description.
|
||||||
|
ResponseMessage string `json:"responseMessage,omitempty"`
|
||||||
|
|
||||||
|
// List of canceled or refunded purchase information, in JSON strings. Each string indicates a purchase record.
|
||||||
|
//
|
||||||
|
// For details about the purchase information format, please refer to CanceledPurchase{}
|
||||||
|
CancelledPurchaseList string `json:"cancelledPurchaseList,omitempty"`
|
||||||
|
|
||||||
|
// Token to query data on the next page. If a value is returned, pass it in the next query request to query data on the next page.
|
||||||
|
ContinuationToken string `json:"continuationToken,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanceledPurchase individual canceled purchase information, for CanceledPurchaseList.
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
|
||||||
|
type CanceledPurchase struct {
|
||||||
|
// Unique order ID of a subscription or subscription renewal.
|
||||||
|
OrderID string `json:"orderId"`
|
||||||
|
|
||||||
|
// Product ID.
|
||||||
|
ProductID string `json:"productId"`
|
||||||
|
|
||||||
|
// Purchase token.
|
||||||
|
PurchaseToken string `json:"purchaseToken"`
|
||||||
|
|
||||||
|
// Purchase timestamp in UTC.
|
||||||
|
PurchaseTime int64 `json:"purchaseTime"`
|
||||||
|
|
||||||
|
// Cancellation or refund timestamp in UTC.
|
||||||
|
CancelledTime int64 `json:"cancelledTime"`
|
||||||
|
|
||||||
|
// Cancellation or refund initiator. Possible values are:
|
||||||
|
// 0: user
|
||||||
|
// 1: developer
|
||||||
|
// 2: Huawei
|
||||||
|
CancelledSource int64 `json:"cancelledSource"`
|
||||||
|
|
||||||
|
// Cancellation or refund reason. Possible values are:
|
||||||
|
// 0: others
|
||||||
|
// 1: user repentance
|
||||||
|
// 2: product not provided
|
||||||
|
// 3: abnormal app service
|
||||||
|
// 4: accidental purchase
|
||||||
|
// 5: fraud
|
||||||
|
// 6: chargeback
|
||||||
|
// 7: upgrade or downgrade
|
||||||
|
// 8: user service area change
|
||||||
|
CancelledReason int64 `json:"cancelledReason"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Below are all constants that can be used to determine different parameter states in IAP Purchase Data
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.Kind product type
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataKindConsumable int64 = 0
|
||||||
|
InAppPurchaseDataKindNonConsumable int64 = 1
|
||||||
|
InAppPurchaseDataKindSubscription int64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.PurchaseState
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataPurchaseStateInitialized int64 = -1
|
||||||
|
InAppPurchaseDataPurchaseStatePurchased int64 = 0
|
||||||
|
InAppPurchaseDataPurchaseStateCanceled int64 = 1
|
||||||
|
InAppPurchaseDataPurchaseStateRefunded int64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.ConsumptionState
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataConsumptionStateNotConsumed string = "0"
|
||||||
|
InAppPurchaseDataConsumptionStateConsumed string = "0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.PurchaseType indicates if the product is purchased through sandbox environment or promotion.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
//
|
||||||
|
// Note: You should always check InAppPurchaseData.PurchaseType != nil first.
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataPurchaseTypeSandbox int64 = 0
|
||||||
|
InAppPurchaseDataPurchaseTypePromotion int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.PayType payment methods when buying the product.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5#EN-US_TOPIC_0000001050986133__section135412662210
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataPayTypeHuaweiPoint string = "0"
|
||||||
|
InAppPurchaseDataPayTypeCreditCard string = "3"
|
||||||
|
InAppPurchaseDataPayTypeAlipay string = "4"
|
||||||
|
InAppPurchaseDataPayTypeCarrier string = "6"
|
||||||
|
InAppPurchaseDataPayTypePayPal string = "13"
|
||||||
|
InAppPurchaseDataPayTypeDebitCard string = "16"
|
||||||
|
InAppPurchaseDataPayTypeWeChatPay string = "17"
|
||||||
|
InAppPurchaseDataPayTypeGiftCard string = "19"
|
||||||
|
InAppPurchaseDataPayTypeBalance string = "20"
|
||||||
|
InAppPurchaseDataPayTypeHuaweiPointCard string = "21"
|
||||||
|
InAppPurchaseDataPayTypeWorldPay string = "24"
|
||||||
|
InAppPurchaseDataPayTypeHuaweiPay string = "31"
|
||||||
|
InAppPurchaseDataPayTypeAntCreditPay string = "32"
|
||||||
|
InAppPurchaseDataPayTypeMPesa string = "200" // M-Pesa
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.AccountFlag Account type.
|
||||||
|
// See https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-common-statement-0000001050986127-V5#EN-US_TOPIC_0000001050986127__section1741234185817 for details.
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataAccountFlagHuaweiID int64 = 0
|
||||||
|
InAppPurchaseDataAccountFlagAppTouch int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.ExpirationIntent Reasons why a subscription expires.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataExpirationIntentCanceledByUser int64 = 1
|
||||||
|
InAppPurchaseDataExpirationIntentProductUnavaliable int64 = 2
|
||||||
|
InAppPurchaseDataExpirationIntentAbnormalUserSigning int64 = 3
|
||||||
|
InAppPurchaseDataExpirationIntentBillingError int64 = 4
|
||||||
|
InAppPurchaseDataExpirationIntentPriceIncreaseDisagreed int64 = 5
|
||||||
|
InAppPurchaseDataExpirationIntentUnknownError int64 = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.RetryFlag Indicates whether the system still tries to renew an expired subscription.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataRetryFlagNo int64 = 0
|
||||||
|
InAppPurchaseDataRetryFlagYes int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.IntroductoryFlag Indicates whether a subscription is in the renewal period with promotion or not.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataIntroductoryFlagNo int64 = 0
|
||||||
|
InAppPurchaseDataIntroductoryFlagYes int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.TrialFlag Indicates whether a subscription is in the free trial period or not.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataTrialFlagNo int64 = 0
|
||||||
|
InAppPurchaseDataTrialFlagYes int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.CancelReason Causes of subscription cancellation.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
//
|
||||||
|
// Note: You should check SubIsValid first.
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataCancelReasonOther int64 = 0 // Other causes. For example, a user mistakenly purchases a subscription and has to cancel it.
|
||||||
|
InAppPurchaseDataCancelReasonUserIssue int64 = 1 // A user encounters a problem within the app and cancels the subscription.
|
||||||
|
InAppPurchaseDataCancelReasonUpgradeOrCrossgrade int64 = 2 // A user performs subscription upgrade or crossgrade.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.NotifyClosed Indicates whether a user has disabled the subscription notification function.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataNotifyClosedNo int64 = 0
|
||||||
|
InAppPurchaseDataNotifyClosedYes int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.RenewStatus Indecates whether the auto-renewal is canceled or not.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataRenewStatusCanceledByUser int64 = 0 // A user proactively canceled the subscription auto-renewal.
|
||||||
|
InAppPurchaseDataRenewStatusNormal int64 = 1 // The subscription will be auto-renewed.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.PriceConsentStatus User opinions collected when the product price increased.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataPriceConsentStatusNoResponse int64 = 0 // The user did not give any comfirmation. Subscription will be terminated without renewal after expires.
|
||||||
|
InAppPurchaseDataPriceConsentStatusAgreed int64 = 1 // The user has agreed with the price increase.
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.DeferFlag Indicates whether to postpone the settlement date.
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataDeferFlagYes int64 = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for InAppPurchaseData.CancelWay how does the subscription be canceled
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5
|
||||||
|
//
|
||||||
|
// Note: You should check SubIsValid first.
|
||||||
|
const (
|
||||||
|
InAppPurchaseDataCancelWayByUser int64 = 0
|
||||||
|
InAppPurchaseDataCancelWayByDeveloper int64 = 1
|
||||||
|
InAppPurchaseDataCancelWayByHuawei int64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for CanceledPurchase.CancelledSource
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
|
||||||
|
const (
|
||||||
|
CanceledPurchaseCancelledSourceByUser int64 = 0
|
||||||
|
CanceledPurchaseCancelledSourceByDeveloper int64 = 1
|
||||||
|
CanceledPurchaseCancelledSourceByHuawei int64 = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// Constants for CanceledPurchase.CancelledReason
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
|
||||||
|
const (
|
||||||
|
CanceledPurchaseCancelledReasonOther int64 = 0
|
||||||
|
CanceledPurchaseCancelledReasonUserRepentance int64 = 1
|
||||||
|
CanceledPurchaseCancelledReasonProductNotProvided int64 = 2
|
||||||
|
CanceledPurchaseCancelledReasonAbnormal int64 = 3
|
||||||
|
CanceledPurchaseCancelledReasonAccidental int64 = 4
|
||||||
|
CanceledPurchaseCancelledReasonFraud int64 = 5
|
||||||
|
CanceledPurchaseCancelledReasonChargeback int64 = 6
|
||||||
|
CanceledPurchaseCancelledReasonUpgradeOrDowngrade int64 = 7
|
||||||
|
CanceledPurchaseCancelledReasonServiceAreaChanged int64 = 8
|
||||||
|
)
|
||||||
104
hms/modifier.go
Normal file
104
hms/modifier.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package hms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CancelSubscriptionRenewal Cancel a aubscription from auto-renew when expired.
|
||||||
|
// Note that this does not cancel the current subscription.
|
||||||
|
// If you want to revoke a subscription, use Client.RevokeSubscription() instead.
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L54
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-subscription-0000001050746115-V5
|
||||||
|
func (c *Client) CancelSubscriptionRenewal(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (success bool, responseMessage string, err error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"subscriptionId": subscriptionID,
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
}
|
||||||
|
var resp ModifySubscriptionResponse
|
||||||
|
success, resp, err = c.modifySubscriptionQuery(ctx, bodyMap, accountFlag, "/sub/applications/v2/purchases/stop")
|
||||||
|
responseMessage = resp.ResponseMessage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtendSubscription extend the current subscription expiration date without chanrging the customer.
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L68
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-refund-subscription-fee-0000001050986131-V5
|
||||||
|
func (c *Client) ExtendSubscription(ctx context.Context, purchaseToken, subscriptionID string, currentExpirationTime, desiredExpirationTime int64, accountFlag int64) (success bool, responseMessage string, newExpirationTime int64, err error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"subscriptionId": subscriptionID,
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
"currentExpirationTime": fmt.Sprintf("%v", currentExpirationTime),
|
||||||
|
"desiredExpirationTime": fmt.Sprintf("%v", desiredExpirationTime),
|
||||||
|
}
|
||||||
|
var resp ModifySubscriptionResponse
|
||||||
|
success, resp, err = c.modifySubscriptionQuery(ctx, bodyMap, accountFlag, "/sub/applications/v2/purchases/delay")
|
||||||
|
responseMessage = resp.ResponseMessage
|
||||||
|
newExpirationTime = resp.NewExpirationTime
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefundSubscription refund a subscription payment.
|
||||||
|
// Note that this does not cancel the current subscription.
|
||||||
|
// If you want to revoke a subscription, use Client.RevokeSubscription() instead.
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L84
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-refund-subscription-fee-0000001050986131-V5
|
||||||
|
func (c *Client) RefundSubscription(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (success bool, responseMessage string, err error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"subscriptionId": subscriptionID,
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
}
|
||||||
|
var resp ModifySubscriptionResponse
|
||||||
|
success, resp, err = c.modifySubscriptionQuery(ctx, bodyMap, accountFlag, "/sub/applications/v2/purchases/returnFee")
|
||||||
|
responseMessage = resp.ResponseMessage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeSubscription will revoke and issue a refund on a subscription immediately.
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L99
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-unsubscribe-0000001051066056-V5
|
||||||
|
func (c *Client) RevokeSubscription(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (success bool, responseMessage string, err error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"subscriptionId": subscriptionID,
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
}
|
||||||
|
var resp ModifySubscriptionResponse
|
||||||
|
success, resp, err = c.modifySubscriptionQuery(ctx, bodyMap, accountFlag, "/sub/applications/v2/purchases/withdrawal")
|
||||||
|
responseMessage = resp.ResponseMessage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModifySubscriptionResponse JSON response from {rootUrl}/sub/applications/v2/purchases/stop|delay|returnFee|withdrawal
|
||||||
|
type ModifySubscriptionResponse struct {
|
||||||
|
ResponseCode string `json:"responseCode"`
|
||||||
|
ResponseMessage string `json:"responseMessage;omitempty"`
|
||||||
|
NewExpirationTime int64 `json:"newExpirationTime;omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// public method to query {rootUrl}/sub/applications/v2/purchases/stop|delay|returnFee|withdrawal
|
||||||
|
func (c *Client) modifySubscriptionQuery(ctx context.Context, requestBodyMap map[string]string, accountFlag int64, uri string) (success bool, response ModifySubscriptionResponse, err error) {
|
||||||
|
url := c.getRootSubscriptionURLByFlag(accountFlag) + uri
|
||||||
|
|
||||||
|
bodyBytes, err := c.sendJSONRequest(ctx, url, requestBodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return false, response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug
|
||||||
|
log.Println("url:", url)
|
||||||
|
log.Println("request:", requestBodyMap)
|
||||||
|
log.Printf("%s", bodyBytes)
|
||||||
|
|
||||||
|
if err := json.Unmarshal(bodyBytes, &response); err != nil {
|
||||||
|
return false, response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch response.ResponseCode {
|
||||||
|
case "0":
|
||||||
|
return true, response, nil
|
||||||
|
default:
|
||||||
|
return false, response, c.getResponseErrorByCode(response.ResponseCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
103
hms/notification.go
Normal file
103
hms/notification.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package hms
|
||||||
|
|
||||||
|
// SubscriptionNotification Request parameters when a developer server is called by HMS API.
|
||||||
|
//
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-notifications-about-subscription-events-0000001050706084-V5
|
||||||
|
type SubscriptionNotification struct {
|
||||||
|
// Notification message, which is a JSON string. For details, please refer to statusUpdateNotification.
|
||||||
|
StatusUpdateNotification string `json:"statusUpdateNotification"`
|
||||||
|
|
||||||
|
// Signature string for the StatusUpdateNotification parameter. The signature algorithm is SHA256withRSA.
|
||||||
|
//
|
||||||
|
// After your server receives the signature string, you need to use the public payment key to verify the signature of StatusUpdateNotification in JSON format.
|
||||||
|
// For details, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
|
||||||
|
//
|
||||||
|
// For details about how to obtain the public key, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/query-payment-info-0000001050166299-V5
|
||||||
|
NotifycationSignature string `json:"notifycationSignature"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusUpdateNotification JSON content when unmarshal NotificationRequest.StatusUpdateNotification
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-notifications-about-subscription-events-0000001050706084-V5#EN-US_TOPIC_0000001050706084__section18290165220716
|
||||||
|
type StatusUpdateNotification struct {
|
||||||
|
// Environment for sending a notification. Value could be one of either:
|
||||||
|
// "PROD": general production environment
|
||||||
|
// "SandBox": sandbox testing environment
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
|
||||||
|
// Notification event type. For details, please refer to const NotificationTypeInitialBuy etc.
|
||||||
|
NotificationType int64 `json:"notificationType"`
|
||||||
|
|
||||||
|
// Subscription ID
|
||||||
|
SubscriptionID string `json:"subscriptionId"`
|
||||||
|
|
||||||
|
// Timestamp, which is passed only when notificationType is CANCEL(1).
|
||||||
|
CancellationDate int64 `json:"cancellationDate,omitempty"`
|
||||||
|
|
||||||
|
// Order ID used for payment during subscription renewal.
|
||||||
|
OrderID string `json:"orderId"`
|
||||||
|
|
||||||
|
// PurchaseToken of the latest receipt, which is passed only when notificationType is INITIAL_BUY(0), RENEWAL(2), or INTERACTIVE_RENEWAL(3) and the renewal is successful.
|
||||||
|
LatestReceipt string `json:"latestReceipt,omitempty"`
|
||||||
|
|
||||||
|
// Latest receipt, which is a JSON string. This parameter is left empty when notificationType is CANCEL(1).
|
||||||
|
// For details about the parameters contained, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-data-model-0000001050986133-V5#EN-US_TOPIC_0000001050986133__section264617465219
|
||||||
|
LatestReceiptInfo string `json:"latestReceiptInfo,omitempty"`
|
||||||
|
|
||||||
|
// Signature string for the LatestReceiptInfo parameter. The signature algorithm is SHA256withRSA.
|
||||||
|
//
|
||||||
|
// After your server receives the signature string, you need to use the public payment key to verify the signature of LatestReceiptInfo in JSON format.
|
||||||
|
// For details, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
|
||||||
|
//
|
||||||
|
// For details about how to obtain the public key, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/query-payment-info-0000001050166299-V5
|
||||||
|
LatestReceiptInfoSignature string `json:"latestReceiptInfoSignature,omitempty"`
|
||||||
|
|
||||||
|
// Token of the latest expired receipt. This parameter has a value only when NotificationType is RENEWAL(2) or INTERACTIVE_RENEWAL(3).
|
||||||
|
LatestExpiredReceipt string `json:"latestExpiredReceipt,omitempty"`
|
||||||
|
|
||||||
|
// Latest expired receipt, which is a JSON string. This parameter has a value only when NotificationType is RENEWAL(2) or INTERACTIVE_RENEWAL(3).
|
||||||
|
LatestExpiredReceiptInfo string `json:"latestExpiredReceiptInfo,omitempty"`
|
||||||
|
|
||||||
|
// Signature string for the LatestExpiredReceiptInfo parameter. The signature algorithm is SHA256withRSA.
|
||||||
|
//
|
||||||
|
// After your server receives the signature string, you need to use the public payment key to verify the signature of LatestExpiredReceiptInfo in JSON format.
|
||||||
|
// For details, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
|
||||||
|
//
|
||||||
|
// For details about how to obtain the public key, please refer to https://developer.huawei.com/consumer/en/doc/HMSCore-Guides-V5/query-payment-info-0000001050166299-V5
|
||||||
|
LatestExpiredReceiptInfoSignature string `json:"latestExpiredReceiptInfoSignature,omitempty"`
|
||||||
|
|
||||||
|
// Renewal status. Value could be one of either:
|
||||||
|
// 1: The subscription renewal is normal.
|
||||||
|
// 0: The user has canceled subscription renewal.
|
||||||
|
AutoRenewStatus int64 `json:"autoRenewStatus"`
|
||||||
|
|
||||||
|
// Refund order ID. This parameter has a value only when NotificationType is CANCEL(1).
|
||||||
|
RefundPayOrderID string `json:"refundPayOrderId,omitempty"`
|
||||||
|
|
||||||
|
// Product ID.
|
||||||
|
ProductID string `json:"productId"`
|
||||||
|
|
||||||
|
// App ID.
|
||||||
|
ApplicationID string `json:"applicationId,omitempty"`
|
||||||
|
|
||||||
|
// Expiration reason. This parameter has a value only when NotificationType is RENEWAL(2) or INTERACTIVE_RENEWAL(3), and the renewal is successful.
|
||||||
|
ExpirationIntent int64 `json:"expirationIntent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constants for StatusUpdateNotification.NotificationType
|
||||||
|
// https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-notifications-about-subscription-events-0000001050706084-V5#EN-US_TOPIC_0000001050706084__section18290165220716
|
||||||
|
const (
|
||||||
|
NotificationTypeInitialBuy int64 = 0
|
||||||
|
NotificationTypeCancel int64 = 1
|
||||||
|
NotificationTypeRenewal int64 = 2
|
||||||
|
NotificationTypeInteractiveRenewal int64 = 3
|
||||||
|
NotificationTypeNewRenewalPref int64 = 4
|
||||||
|
NotificationTypeRenewalStopped int64 = 5
|
||||||
|
NotificationTypeRenewalRestored int64 = 6
|
||||||
|
NotificationTypeRenewalRecurring int64 = 7
|
||||||
|
NotificationTypeInGracePeriod int64 = 8
|
||||||
|
NotificationTypeOnHold int64 = 9
|
||||||
|
NotificationTypePaused int64 = 10
|
||||||
|
NotificationTypePausePlanChanged int64 = 11
|
||||||
|
NotificationTypePriceChangeConfirmed int64 = 12
|
||||||
|
NotificationTypeDeferred int64 = 13
|
||||||
|
)
|
||||||
257
hms/validator.go
Normal file
257
hms/validator.go
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
package hms
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VerifySignature validate inapp order or subscription data signature. Returns nil if pass.
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-Guides-V5/verifying-signature-returned-result-0000001050033088-V5
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/demo.go#L60
|
||||||
|
func VerifySignature(base64EncodedPublicKey string, data string, signature string) (err error) {
|
||||||
|
publicKeyByte, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub, err := x509.ParsePKIXPublicKey(publicKeyByte)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hashed := sha256.Sum256([]byte(data))
|
||||||
|
signatureByte, err := base64.StdEncoding.DecodeString(signature)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], signatureByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscriptionVerifyResponse JSON response after requested {rootUrl}/sub/applications/v2/purchases/get
|
||||||
|
type SubscriptionVerifyResponse struct {
|
||||||
|
ResponseCode string `json:"responseCode"` // Response code, if = "0" means succeed, for others see https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
ResponseMessage string `json:"responseMessage,omitempty"` // Response descriptions, especially when error
|
||||||
|
InappPurchaseData string `json:"inappPurchaseData,omitempty"` // InappPurchaseData JSON string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySubscription gets subscriptions info with subscriptionId and purchaseToken.
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/api-subscription-verify-purchase-token-0000001050706080-V5
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L40
|
||||||
|
func (c *Client) VerifySubscription(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (InAppPurchaseData, error) {
|
||||||
|
var iap InAppPurchaseData
|
||||||
|
|
||||||
|
dataString, err := c.GetSubscriptionDataString(ctx, purchaseToken, subscriptionID, accountFlag)
|
||||||
|
if err != nil {
|
||||||
|
return iap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(dataString), &iap); err != nil {
|
||||||
|
return iap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return iap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSubscriptionDataString gets subscriptions response data string.
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/development/HMSCore-References-V5/api-subscription-verify-purchase-token-0000001050706080-V5
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/subscription.go#L40
|
||||||
|
func (c *Client) GetSubscriptionDataString(ctx context.Context, purchaseToken, subscriptionID string, accountFlag int64) (string, error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"subscriptionId": subscriptionID,
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
}
|
||||||
|
url := c.getRootSubscriptionURLByFlag(accountFlag) + "/sub/applications/v2/purchases/get"
|
||||||
|
|
||||||
|
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("GetSubscriptionDataString(): Encounter error: %s", err)
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp SubscriptionVerifyResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &resp); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := c.getResponseErrorByCode(resp.ResponseCode); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.InappPurchaseData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OrderVerifyResponse JSON response from {rootUrl}/applications/purchases/tokens/verify
|
||||||
|
type OrderVerifyResponse struct {
|
||||||
|
ResponseCode string `json:"responseCode"` // Response code, if = "0" means succeed, for others see https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/server-error-code-0000001050166248-V5
|
||||||
|
ResponseMessage string `json:"responseMessage,omitempty"` // Response descriptions, especially when error
|
||||||
|
PurchaseTokenData string `json:"purchaseTokenData,omitempty"` // InappPurchaseData JSON string
|
||||||
|
DataSignature string `json:"dataSignature,omitempty"` // Signature to verify PurchaseTokenData string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyOrder gets order (single item purchase) info with productId and purchaseToken.
|
||||||
|
//
|
||||||
|
// Note that this method does not verify the DataSignature, thus security is relied on HTTPS solely.
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-order-verify-purchase-token-0000001050746113-V5
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L41
|
||||||
|
func (c *Client) VerifyOrder(ctx context.Context, purchaseToken, productID string, accountFlag int64) (InAppPurchaseData, error) {
|
||||||
|
var iap InAppPurchaseData
|
||||||
|
|
||||||
|
dataString, _, err := c.GetOrderDataString(ctx, purchaseToken, productID, accountFlag)
|
||||||
|
if err != nil {
|
||||||
|
return iap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(dataString), &iap); err != nil {
|
||||||
|
return iap, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return iap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOrderDataString gets order (single item purchase) response data as json string and dataSignature
|
||||||
|
//
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-order-verify-purchase-token-0000001050746113-V5
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L41
|
||||||
|
func (c *Client) GetOrderDataString(ctx context.Context, purchaseToken, productID string, accountFlag int64) (purchaseTokenData, dataSignature string, err error) {
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"purchaseToken": purchaseToken,
|
||||||
|
"productId": productID,
|
||||||
|
}
|
||||||
|
url := c.getRootOrderURLByFlag(accountFlag) + "/applications/purchases/tokens/verify"
|
||||||
|
|
||||||
|
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("GetOrderDataString(): Encounter error: %s", err)
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp OrderVerifyResponse
|
||||||
|
if err := json.Unmarshal(bodyBytes, &resp); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if err := c.getResponseErrorByCode(resp.ResponseCode); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.PurchaseTokenData, resp.DataSignature, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to send http json request and get response bodyBytes.
|
||||||
|
//
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/demo.go#L33
|
||||||
|
func (c *Client) sendJSONRequest(ctx context.Context, url string, bodyMap map[string]string) (bodyBytes []byte, err error) {
|
||||||
|
bodyString, err := json.Marshal(bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewReader(bodyString))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||||
|
atHeader, err := c.GetApplicationAccessTokenHeader()
|
||||||
|
if err == nil {
|
||||||
|
req.Header.Set("Authorization", atHeader)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.httpCli.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err = ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCanceledOrRefundedPurchases gets all revoked purchases in CanceledPurchaseList{}.
|
||||||
|
// This method allow fetch over 1000 results regardles the cap implied by HMS API. Though you should still limit maxRows to a certain number to increate preformance.
|
||||||
|
//
|
||||||
|
// In case of an error, this method might return some fetch results if maxRows greater than 1000 or equals 0.
|
||||||
|
//
|
||||||
|
// Source code originated from https://github.com/HMS-Core/hms-iap-serverdemo/blob/92241f97fed1b68ddeb7cb37ea4ca6e6d33d2a87/demo/order.go#L52
|
||||||
|
// Document: https://developer.huawei.com/consumer/en/doc/HMSCore-References-V5/api-cancel-or-refund-record-0000001050746117-V5
|
||||||
|
func (c *Client) GetCanceledOrRefundedPurchases(
|
||||||
|
// context of request
|
||||||
|
ctx context.Context,
|
||||||
|
|
||||||
|
// start time timestamp in milliseconds, if =0, will default to 1 month ago.
|
||||||
|
startAt int64,
|
||||||
|
|
||||||
|
// end time timestamp in milliseconds, if =0, will default to now.
|
||||||
|
endAt int64,
|
||||||
|
|
||||||
|
// rows to return. default to 1000 if maxRows>1000 or equals to 0.
|
||||||
|
maxRows int,
|
||||||
|
|
||||||
|
// Token returned in the last query to query the data on the next page.
|
||||||
|
continuationToken string,
|
||||||
|
|
||||||
|
// Query type. Ignore this parameter when continuationToken is passed. The options are as follows:
|
||||||
|
// 0: Queries purchase information about consumables and non-consumables. This is the default value.
|
||||||
|
// 1: Queries all purchase information about consumables, non-consumables, and subscriptions.
|
||||||
|
productType int64,
|
||||||
|
|
||||||
|
// Account flag to determine which API URL to use.
|
||||||
|
accountFlag int64,
|
||||||
|
) (canceledPurchases []CanceledPurchase, newContinuationToken string, responseCode string, responseMessage string, err error) {
|
||||||
|
// default values
|
||||||
|
if maxRows > 1000 || maxRows < 1 {
|
||||||
|
maxRows = 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
switch endAt {
|
||||||
|
case 0:
|
||||||
|
endAt = time.Now().UnixNano() / 1000000
|
||||||
|
case startAt:
|
||||||
|
endAt++
|
||||||
|
}
|
||||||
|
|
||||||
|
bodyMap := map[string]string{
|
||||||
|
"startAt": fmt.Sprintf("%v", startAt),
|
||||||
|
"endAt": fmt.Sprintf("%v", endAt),
|
||||||
|
"maxRows": fmt.Sprintf("%v", maxRows),
|
||||||
|
"continuationToken": continuationToken,
|
||||||
|
"type": fmt.Sprintf("%v", productType),
|
||||||
|
}
|
||||||
|
|
||||||
|
url := c.getRootOrderURLByFlag(accountFlag) + "/applications/v2/purchases/cancelledList"
|
||||||
|
bodyBytes, err := c.sendJSONRequest(ctx, url, bodyMap)
|
||||||
|
if err != nil {
|
||||||
|
// log.Printf("GetCanceledOrRefundedPurchases(): Encounter error: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cpl CanceledPurchaseList // temporary variable to store api query result
|
||||||
|
err = json.Unmarshal(bodyBytes, &cpl)
|
||||||
|
if err != nil {
|
||||||
|
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, err
|
||||||
|
}
|
||||||
|
if cpl.ResponseCode != "0" {
|
||||||
|
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, c.getResponseErrorByCode(cpl.ResponseCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(cpl.CancelledPurchaseList), &canceledPurchases)
|
||||||
|
if err != nil {
|
||||||
|
return canceledPurchases, continuationToken, cpl.ResponseCode, cpl.ResponseMessage, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return canceledPurchases, cpl.ContinuationToken, cpl.ResponseCode, cpl.ResponseMessage, nil
|
||||||
|
}
|
||||||
158
playstore/mocks/playstore.go
Normal file
158
playstore/mocks/playstore.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
|
// Source: github.com/awa/go-iap/playstore (interfaces: IABProduct,IABSubscription)
|
||||||
|
|
||||||
|
// Package mocks is a generated GoMock package.
|
||||||
|
package mocks
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
v3 "google.golang.org/api/androidpublisher/v3"
|
||||||
|
reflect "reflect"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockIABProduct is a mock of IABProduct interface
|
||||||
|
type MockIABProduct struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockIABProductMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIABProductMockRecorder is the mock recorder for MockIABProduct
|
||||||
|
type MockIABProductMockRecorder struct {
|
||||||
|
mock *MockIABProduct
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockIABProduct creates a new mock instance
|
||||||
|
func NewMockIABProduct(ctrl *gomock.Controller) *MockIABProduct {
|
||||||
|
mock := &MockIABProduct{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockIABProductMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockIABProduct) EXPECT() *MockIABProductMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcknowledgeProduct mocks base method
|
||||||
|
func (m *MockIABProduct) AcknowledgeProduct(arg0 context.Context, arg1, arg2, arg3, arg4 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "AcknowledgeProduct", arg0, arg1, arg2, arg3, arg4)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcknowledgeProduct indicates an expected call of AcknowledgeProduct
|
||||||
|
func (mr *MockIABProductMockRecorder) AcknowledgeProduct(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcknowledgeProduct", reflect.TypeOf((*MockIABProduct)(nil).AcknowledgeProduct), arg0, arg1, arg2, arg3, arg4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyProduct mocks base method
|
||||||
|
func (m *MockIABProduct) VerifyProduct(arg0 context.Context, arg1, arg2, arg3 string) (*v3.ProductPurchase, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "VerifyProduct", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].(*v3.ProductPurchase)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyProduct indicates an expected call of VerifyProduct
|
||||||
|
func (mr *MockIABProductMockRecorder) VerifyProduct(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifyProduct", reflect.TypeOf((*MockIABProduct)(nil).VerifyProduct), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIABSubscription is a mock of IABSubscription interface
|
||||||
|
type MockIABSubscription struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockIABSubscriptionMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIABSubscriptionMockRecorder is the mock recorder for MockIABSubscription
|
||||||
|
type MockIABSubscriptionMockRecorder struct {
|
||||||
|
mock *MockIABSubscription
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockIABSubscription creates a new mock instance
|
||||||
|
func NewMockIABSubscription(ctrl *gomock.Controller) *MockIABSubscription {
|
||||||
|
mock := &MockIABSubscription{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockIABSubscriptionMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockIABSubscription) EXPECT() *MockIABSubscriptionMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcknowledgeSubscription mocks base method
|
||||||
|
func (m *MockIABSubscription) AcknowledgeSubscription(arg0 context.Context, arg1, arg2, arg3 string, arg4 *v3.SubscriptionPurchasesAcknowledgeRequest) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "AcknowledgeSubscription", arg0, arg1, arg2, arg3, arg4)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcknowledgeSubscription indicates an expected call of AcknowledgeSubscription
|
||||||
|
func (mr *MockIABSubscriptionMockRecorder) AcknowledgeSubscription(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcknowledgeSubscription", reflect.TypeOf((*MockIABSubscription)(nil).AcknowledgeSubscription), arg0, arg1, arg2, arg3, arg4)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSubscription mocks base method
|
||||||
|
func (m *MockIABSubscription) CancelSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "CancelSubscription", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSubscription indicates an expected call of CancelSubscription
|
||||||
|
func (mr *MockIABSubscriptionMockRecorder) CancelSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelSubscription", reflect.TypeOf((*MockIABSubscription)(nil).CancelSubscription), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefundSubscription mocks base method
|
||||||
|
func (m *MockIABSubscription) RefundSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "RefundSubscription", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RefundSubscription indicates an expected call of RefundSubscription
|
||||||
|
func (mr *MockIABSubscriptionMockRecorder) RefundSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RefundSubscription", reflect.TypeOf((*MockIABSubscription)(nil).RefundSubscription), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeSubscription mocks base method
|
||||||
|
func (m *MockIABSubscription) RevokeSubscription(arg0 context.Context, arg1, arg2, arg3 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "RevokeSubscription", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// RevokeSubscription indicates an expected call of RevokeSubscription
|
||||||
|
func (mr *MockIABSubscriptionMockRecorder) RevokeSubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RevokeSubscription", reflect.TypeOf((*MockIABSubscription)(nil).RevokeSubscription), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySubscription mocks base method
|
||||||
|
func (m *MockIABSubscription) VerifySubscription(arg0 context.Context, arg1, arg2, arg3 string) (*v3.SubscriptionPurchase, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "VerifySubscription", arg0, arg1, arg2, arg3)
|
||||||
|
ret0, _ := ret[0].(*v3.SubscriptionPurchase)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifySubscription indicates an expected call of VerifySubscription
|
||||||
|
func (mr *MockIABSubscriptionMockRecorder) VerifySubscription(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VerifySubscription", reflect.TypeOf((*MockIABSubscription)(nil).VerifySubscription), arg0, arg1, arg2, arg3)
|
||||||
|
}
|
||||||
@@ -1,33 +1,63 @@
|
|||||||
package playstore
|
package playstore
|
||||||
|
|
||||||
|
// https://developer.android.com/google/play/billing/rtdn-reference#sub
|
||||||
|
type SubscriptionNotificationType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubscriptionNotificationTypeRecovered SubscriptionNotificationType = iota + 1
|
||||||
|
SubscriptionNotificationTypeRenewed
|
||||||
|
SubscriptionNotificationTypeCanceled
|
||||||
|
SubscriptionNotificationTypePurchased
|
||||||
|
SubscriptionNotificationTypeAccountHold
|
||||||
|
SubscriptionNotificationTypeGracePeriod
|
||||||
|
SubscriptionNotificationTypeRestarted
|
||||||
|
SubscriptionNotificationTypePriceChangeConfirmed
|
||||||
|
SubscriptionNotificationTypeDeferred
|
||||||
|
SubscriptionNotificationTypePaused
|
||||||
|
SubscriptionNotificationTypePauseScheduleChanged
|
||||||
|
SubscriptionNotificationTypeRevoked
|
||||||
|
SubscriptionNotificationTypeExpired
|
||||||
|
)
|
||||||
|
|
||||||
|
// https://developer.android.com/google/play/billing/rtdn-reference#one-time
|
||||||
|
type OneTimeProductNotificationType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
OneTimeProductNotificationTypePurchased OneTimeProductNotificationType = iota + 1
|
||||||
|
OneTimeProductNotificationTypeCanceled
|
||||||
|
)
|
||||||
|
|
||||||
// DeveloperNotification is sent by a Pub/Sub topic.
|
// DeveloperNotification is sent by a Pub/Sub topic.
|
||||||
// Detailed description is following.
|
// Detailed description is following.
|
||||||
// https://developer.android.com/google/play/billing/realtime_developer_notifications.html#json_specification
|
// https://developer.android.com/google/play/billing/rtdn-reference#json_specification
|
||||||
type DeveloperNotification struct {
|
type DeveloperNotification struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
PackageName string `json:"packageName"`
|
PackageName string `json:"packageName"`
|
||||||
EventTimeMillis string `json:"eventTimeMillis"`
|
EventTimeMillis string `json:"eventTimeMillis"`
|
||||||
SubscriptionNotification SubscriptionNotification `json:"subscriptionNotification,omitempty"`
|
SubscriptionNotification SubscriptionNotification `json:"subscriptionNotification,omitempty"`
|
||||||
TestNotification SubscriptionNotification `json:"testNotification,omitempty"`
|
OneTimeProductNotification OneTimeProductNotification `json:"oneTimeProductNotification,omitempty"`
|
||||||
|
TestNotification TestNotification `json:"testNotification,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscriptionNotification has subscription status as notificationType, toke and subscription id
|
// SubscriptionNotification has subscription status as notificationType, token and subscription id
|
||||||
// to confirm status by calling Google Android Publisher API.
|
// to confirm status by calling Google Android Publisher API.
|
||||||
type SubscriptionNotification struct {
|
type SubscriptionNotification struct {
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
NotificationType NotificationType `json:"notificationType,omitempty"`
|
NotificationType SubscriptionNotificationType `json:"notificationType,omitempty"`
|
||||||
PurchaseToken string `json:"purchaseToken,omitempty"`
|
PurchaseToken string `json:"purchaseToken,omitempty"`
|
||||||
SubscriptionID string `json:"subscriptionId,omitempty"`
|
SubscriptionID string `json:"subscriptionId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NotificationType int
|
// OneTimeProductNotification has one-time product status as notificationType, token and sku (product id)
|
||||||
|
// to confirm status by calling Google Android Publisher API.
|
||||||
|
type OneTimeProductNotification struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
NotificationType OneTimeProductNotificationType `json:"notificationType,omitempty"`
|
||||||
|
PurchaseToken string `json:"purchaseToken,omitempty"`
|
||||||
|
SKU string `json:"sku,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
// TestNotification is the test publish that are sent only through the Google Play Developer Console
|
||||||
NotificationTypeRecovered NotificationType = iota + 1
|
type TestNotification struct {
|
||||||
NotificationTypeRenewed
|
Version string `json:"version"`
|
||||||
NotificationTypeCanceled
|
}
|
||||||
NotificationTypePurchased
|
|
||||||
NotificationTypeAccountHold
|
|
||||||
NotificationTypeGracePeriod
|
|
||||||
NotificationTypeReactivated
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -11,14 +11,19 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/api/option"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
androidpublisher "google.golang.org/api/androidpublisher/v3"
|
androidpublisher "google.golang.org/api/androidpublisher/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate mockgen -destination=mocks/playstore.go -package=mocks github.com/awa/go-iap/playstore IABProduct,IABSubscription
|
||||||
|
|
||||||
// The IABProduct type is an interface for product service
|
// The IABProduct type is an interface for product service
|
||||||
type IABProduct interface {
|
type IABProduct interface {
|
||||||
VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error)
|
VerifyProduct(context.Context, string, string, string) (*androidpublisher.ProductPurchase, error)
|
||||||
|
AcknowledgeProduct(context.Context, string, string, string, string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// The IABSubscription type is an interface for subscription service
|
// The IABSubscription type is an interface for subscription service
|
||||||
@@ -32,22 +37,41 @@ type IABSubscription interface {
|
|||||||
|
|
||||||
// The Client type implements VerifySubscription method
|
// The Client type implements VerifySubscription method
|
||||||
type Client struct {
|
type Client struct {
|
||||||
httpCli *http.Client
|
service *androidpublisher.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns http client which includes the credentials to access androidpublisher API.
|
// New returns http client which includes the credentials to access androidpublisher API.
|
||||||
// You should create a service account for your project at
|
// You should create a service account for your project at
|
||||||
// https://console.developers.google.com and download a JSON key file to set this argument.
|
// https://console.developers.google.com and download a JSON key file to set this argument.
|
||||||
func New(jsonKey []byte) (*Client, error) {
|
func New(jsonKey []byte) (*Client, error) {
|
||||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, &http.Client{Timeout: 10 * time.Second})
|
c := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
|
||||||
|
|
||||||
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
|
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Client{conf.Client(ctx)}, err
|
val := conf.Client(ctx).Transport.(*oauth2.Transport)
|
||||||
|
_, err = val.Source.Token()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{service}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWithClient returns http client which includes the custom http client.
|
// NewWithClient returns http client which includes the custom http client.
|
||||||
func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
|
func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
|
||||||
|
if cli == nil {
|
||||||
|
return nil, fmt.Errorf("client is nil")
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
|
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
|
||||||
|
|
||||||
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
|
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
|
||||||
@@ -55,7 +79,12 @@ func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Client{conf.Client(ctx)}, err
|
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{service}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcknowledgeSubscription acknowledges a subscription purchase.
|
// AcknowledgeSubscription acknowledges a subscription purchase.
|
||||||
@@ -66,13 +95,8 @@ func (c *Client) AcknowledgeSubscription(
|
|||||||
token string,
|
token string,
|
||||||
req *androidpublisher.SubscriptionPurchasesAcknowledgeRequest,
|
req *androidpublisher.SubscriptionPurchasesAcknowledgeRequest,
|
||||||
) error {
|
) error {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
|
||||||
if err != nil {
|
err := ps.Acknowledge(packageName, subscriptionID, token, req).Context(ctx).Do()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
|
|
||||||
err = ps.Acknowledge(packageName, subscriptionID, token, req).Context(ctx).Do()
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -84,12 +108,7 @@ func (c *Client) VerifySubscription(
|
|||||||
subscriptionID string,
|
subscriptionID string,
|
||||||
token string,
|
token string,
|
||||||
) (*androidpublisher.SubscriptionPurchase, error) {
|
) (*androidpublisher.SubscriptionPurchase, error) {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
|
|
||||||
result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do()
|
result, err := ps.Get(packageName, subscriptionID, token).Context(ctx).Do()
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
@@ -102,26 +121,24 @@ func (c *Client) VerifyProduct(
|
|||||||
productID string,
|
productID string,
|
||||||
token string,
|
token string,
|
||||||
) (*androidpublisher.ProductPurchase, error) {
|
) (*androidpublisher.ProductPurchase, error) {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesProductsService(c.service)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesProductsService(service)
|
|
||||||
result, err := ps.Get(packageName, productID, token).Context(ctx).Do()
|
result, err := ps.Get(packageName, productID, token).Context(ctx).Do()
|
||||||
|
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) AcknowledgeProduct(ctx context.Context, packageName, productID, token, developerPayload string) error {
|
||||||
|
ps := androidpublisher.NewPurchasesProductsService(c.service)
|
||||||
|
acknowledgeRequest := &androidpublisher.ProductPurchasesAcknowledgeRequest{DeveloperPayload: developerPayload}
|
||||||
|
err := ps.Acknowledge(packageName, productID, token, acknowledgeRequest).Context(ctx).Do()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// CancelSubscription cancels a user's subscription purchase.
|
// CancelSubscription cancels a user's subscription purchase.
|
||||||
func (c *Client) CancelSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
func (c *Client) CancelSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
|
||||||
if err != nil {
|
err := ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
|
|
||||||
err = ps.Cancel(packageName, subscriptionID, token).Context(ctx).Do()
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -129,13 +146,8 @@ func (c *Client) CancelSubscription(ctx context.Context, packageName string, sub
|
|||||||
// RefundSubscription refunds a user's subscription purchase, but the subscription remains valid
|
// RefundSubscription refunds a user's subscription purchase, but the subscription remains valid
|
||||||
// until its expiration time and it will continue to recur.
|
// until its expiration time and it will continue to recur.
|
||||||
func (c *Client) RefundSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
func (c *Client) RefundSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
|
||||||
if err != nil {
|
err := ps.Refund(packageName, subscriptionID, token).Context(ctx).Do()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
|
|
||||||
err = ps.Refund(packageName, subscriptionID, token).Context(ctx).Do()
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -143,13 +155,8 @@ func (c *Client) RefundSubscription(ctx context.Context, packageName string, sub
|
|||||||
// RevokeSubscription refunds and immediately revokes a user's subscription purchase.
|
// RevokeSubscription refunds and immediately revokes a user's subscription purchase.
|
||||||
// Access to the subscription will be terminated immediately and it will stop recurring.
|
// Access to the subscription will be terminated immediately and it will stop recurring.
|
||||||
func (c *Client) RevokeSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
func (c *Client) RevokeSubscription(ctx context.Context, packageName string, subscriptionID string, token string) error {
|
||||||
service, err := androidpublisher.New(c.httpCli)
|
ps := androidpublisher.NewPurchasesSubscriptionsService(c.service)
|
||||||
if err != nil {
|
err := ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do()
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ps := androidpublisher.NewPurchasesSubscriptionsService(service)
|
|
||||||
err = ps.Revoke(packageName, subscriptionID, token).Context(ctx).Do()
|
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import (
|
|||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"google.golang.org/api/androidpublisher/v3"
|
||||||
androidpublisher "google.golang.org/api/androidpublisher/v3"
|
|
||||||
"google.golang.org/appengine/urlfetch"
|
"google.golang.org/appengine/urlfetch"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,22 +35,19 @@ func TestNew(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
expected := "oauth2: cannot fetch token: 400 Bad Request\nResponse: {\n \"error\": \"invalid_grant\",\n \"error_description\": \"Invalid issuer: Not a service account.\"\n}"
|
expected := "oauth2: cannot fetch token: 400 Bad Request\nResponse: {\"error\":\"invalid_grant\",\"error_description\":\"Invalid JWT: iss field missing.\"}"
|
||||||
|
|
||||||
actual, _ := New(dummyKey)
|
_, err := New(dummyKey)
|
||||||
val := actual.httpCli.Transport.(*oauth2.Transport)
|
if err == nil || err.Error() != expected {
|
||||||
token, err := val.Source.Token()
|
|
||||||
if token != nil {
|
|
||||||
t.Errorf("got %#v", token)
|
|
||||||
}
|
|
||||||
if err.Error() != expected {
|
|
||||||
t.Errorf("got %v\nwant %v", err, expected)
|
t.Errorf("got %v\nwant %v", err, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Normal scenario
|
_, actual := New(nil)
|
||||||
actual, _ = New(jsonKey)
|
if actual == nil || actual.Error() != "unexpected end of JSON input" {
|
||||||
val = actual.httpCli.Transport.(*oauth2.Transport)
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
token, err = val.Source.Token()
|
}
|
||||||
|
|
||||||
|
_, err = New(jsonKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("got %#v", err)
|
t.Errorf("got %#v", err)
|
||||||
}
|
}
|
||||||
@@ -63,14 +59,31 @@ func TestNewWithClient(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
httpClient := urlfetch.Client(ctx)
|
httpClient := urlfetch.Client(ctx)
|
||||||
|
|
||||||
cli, _ := NewWithClient(dummyKey, httpClient)
|
_, err := NewWithClient(dummyKey, httpClient)
|
||||||
tr, _ := cli.httpCli.Transport.(*oauth2.Transport)
|
if err != nil {
|
||||||
|
|
||||||
if !reflect.DeepEqual(tr.Base, httpClient.Transport) {
|
|
||||||
t.Errorf("transport should be urlfetch's one")
|
t.Errorf("transport should be urlfetch's one")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewWithClientErrors(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
expected := errors.New("client is nil")
|
||||||
|
|
||||||
|
_, actual := NewWithClient(dummyKey, nil)
|
||||||
|
if !reflect.DeepEqual(actual, expected) {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
httpClient := urlfetch.Client(ctx)
|
||||||
|
|
||||||
|
_, actual = NewWithClient(nil, httpClient)
|
||||||
|
if actual == nil || actual.Error() != "unexpected end of JSON input" {
|
||||||
|
t.Errorf("got %v\nwant %v", actual, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestAcknowledgeSubscription(t *testing.T) {
|
func TestAcknowledgeSubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
@@ -83,7 +96,7 @@ func TestAcknowledgeSubscription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
err := client.AcknowledgeSubscription(ctx, "package", "subscriptionID", "purchaseToken", req)
|
err := client.AcknowledgeSubscription(ctx, "package", "subscriptionID", "purchaseToken", req)
|
||||||
|
|
||||||
if err.Error() != expected {
|
if err == nil || err.Error() != expected {
|
||||||
t.Errorf("got %v\nwant %v", err, expected)
|
t.Errorf("got %v\nwant %v", err, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,25 +112,13 @@ func TestVerifySubscription(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken")
|
_, err := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken")
|
||||||
|
|
||||||
if err.Error() != expected {
|
if err == nil || err.Error() != expected {
|
||||||
t.Errorf("got %v\nwant %v", err, expected)
|
t.Errorf("got %v\nwant %v", err, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Normal scenario
|
// TODO Normal scenario
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifySubscriptionAndroidPublisherError(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
client := Client{nil}
|
|
||||||
expected := errors.New("client is nil")
|
|
||||||
ctx := context.Background()
|
|
||||||
_, actual := client.VerifySubscription(ctx, "package", "subscriptionID", "purchaseToken")
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
|
||||||
t.Errorf("got %v\nwant %v", actual, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVerifyProduct(t *testing.T) {
|
func TestVerifyProduct(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
@@ -127,42 +128,37 @@ func TestVerifyProduct(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, err := client.VerifyProduct(ctx, "package", "productID", "purchaseToken")
|
_, err := client.VerifyProduct(ctx, "package", "productID", "purchaseToken")
|
||||||
|
|
||||||
if err.Error() != expected {
|
if err == nil || err.Error() != expected {
|
||||||
t.Errorf("got %v", err)
|
t.Errorf("got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Normal scenario
|
// TODO Normal scenario
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyProductAndroidPublisherError(t *testing.T) {
|
func TestAcknowledgeProduct(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
client := Client{nil}
|
// Exception scenario
|
||||||
expected := errors.New("client is nil")
|
expected := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
ctx := context.Background()
|
|
||||||
_, actual := client.VerifyProduct(ctx, "package", "productID", "purchaseToken")
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
client, _ := New(jsonKey)
|
||||||
t.Errorf("got %v\nwant %v", actual, expected)
|
ctx := context.Background()
|
||||||
|
err := client.AcknowledgeProduct(ctx, "package", "productID", "purchaseToken", "")
|
||||||
|
|
||||||
|
if err == nil || err.Error() != expected {
|
||||||
|
t.Errorf("got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO Normal scenario
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCancelSubscription(t *testing.T) {
|
func TestCancelSubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
|
||||||
client := &Client{nil}
|
|
||||||
expected := errors.New("client is nil")
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
client, _ := New(jsonKey)
|
||||||
|
expectedStr := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
|
actual := client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if actual == nil || actual.Error() != expectedStr {
|
||||||
t.Errorf("got %v\nwant %v", actual, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _ = New(jsonKey)
|
|
||||||
expectedStr := "googleapi: Error 400: Invalid Value, invalid"
|
|
||||||
actual = client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
|
|
||||||
|
|
||||||
if actual.Error() != expectedStr {
|
|
||||||
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,21 +167,13 @@ func TestCancelSubscription(t *testing.T) {
|
|||||||
|
|
||||||
func TestRefundSubscription(t *testing.T) {
|
func TestRefundSubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
|
||||||
client := &Client{nil}
|
|
||||||
expected := errors.New("client is nil")
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
client, _ := New(jsonKey)
|
||||||
|
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
||||||
actual := client.RefundSubscription(ctx, "package", "productID", "purchaseToken")
|
actual := client.RefundSubscription(ctx, "package", "productID", "purchaseToken")
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if actual == nil || actual.Error() != expectedStr {
|
||||||
t.Errorf("got %v\nwant %v", actual, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _ = New(jsonKey)
|
|
||||||
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
|
||||||
actual = client.RefundSubscription(ctx, "package", "productID", "purchaseToken")
|
|
||||||
|
|
||||||
if actual.Error() != expectedStr {
|
|
||||||
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,21 +182,13 @@ func TestRefundSubscription(t *testing.T) {
|
|||||||
|
|
||||||
func TestRevokeSubscription(t *testing.T) {
|
func TestRevokeSubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
|
||||||
client := &Client{nil}
|
|
||||||
expected := errors.New("client is nil")
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
client, _ := New(jsonKey)
|
||||||
|
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
||||||
actual := client.RevokeSubscription(ctx, "package", "productID", "purchaseToken")
|
actual := client.RevokeSubscription(ctx, "package", "productID", "purchaseToken")
|
||||||
|
|
||||||
if !reflect.DeepEqual(actual, expected) {
|
if actual == nil || actual.Error() != expectedStr {
|
||||||
t.Errorf("got %v\nwant %v", actual, expected)
|
|
||||||
}
|
|
||||||
|
|
||||||
client, _ = New(jsonKey)
|
|
||||||
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
|
||||||
actual = client.RevokeSubscription(ctx, "package", "productID", "purchaseToken")
|
|
||||||
|
|
||||||
if actual.Error() != expectedStr {
|
|
||||||
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
t.Errorf("got %v\nwant %v", actual, expectedStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user