forked from Mirrors/go-iap
Compare commits
37 Commits
androidpub
...
test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b53c8c58e | ||
|
|
75ba2f30ea | ||
|
|
d6cb5f5767 | ||
|
|
5d167ac1a5 | ||
|
|
b31646baf4 | ||
|
|
1bd99243d7 | ||
|
|
7d624ad068 | ||
|
|
31a625b71e | ||
|
|
4209b06a64 | ||
|
|
3d3bd2b6cf | ||
|
|
934d4ffbbd | ||
|
|
9c0e16f820 | ||
|
|
e895c80eb0 | ||
|
|
4b88aefda5 | ||
|
|
62fa34fffb | ||
|
|
8414aff344 | ||
|
|
e0f6e608e7 | ||
|
|
11e072e211 | ||
|
|
b60c954624 | ||
|
|
c92634081e | ||
|
|
fbfe02b5f6 | ||
|
|
bac6b5676b | ||
|
|
94f276769f | ||
|
|
4ed348bb1b | ||
|
|
712b3f7da2 | ||
|
|
5354251ea5 | ||
|
|
ec53640acb | ||
|
|
ee59170931 | ||
|
|
c992b9705b | ||
|
|
ff9fd778a3 | ||
|
|
1877c0ae24 | ||
|
|
7ef252fde0 | ||
|
|
062102b0f3 | ||
|
|
12a8101bb0 | ||
|
|
f94fdb06d8 | ||
|
|
c371d6eb78 | ||
|
|
6d5e856650 |
@@ -76,16 +76,27 @@ type (
|
|||||||
CancellationDatePST string `json:"cancellation_date_pst,omitempty"`
|
CancellationDatePST string `json:"cancellation_date_pst,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The GracePeriodDate type indicates the grace period date for the subscription
|
||||||
|
GracePeriodDate struct {
|
||||||
|
GracePeriodDate string `json:"grace_period_expires_date,omitempty"`
|
||||||
|
GracePeriodDateMS string `json:"grace_period_expires_date_ms,omitempty"`
|
||||||
|
GracePeriodDatePST string `json:"grace_period_expires_date_pst,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// The InApp type has the receipt attributes
|
// The InApp type has the receipt attributes
|
||||||
InApp struct {
|
InApp struct {
|
||||||
Quantity string `json:"quantity"`
|
Quantity string `json:"quantity"`
|
||||||
ProductID string `json:"product_id"`
|
ProductID string `json:"product_id"`
|
||||||
TransactionID string `json:"transaction_id"`
|
TransactionID string `json:"transaction_id"`
|
||||||
OriginalTransactionID string `json:"original_transaction_id"`
|
OriginalTransactionID string `json:"original_transaction_id"`
|
||||||
WebOrderLineItemID string `json:"web_order_line_item_id,omitempty"`
|
WebOrderLineItemID string `json:"web_order_line_item_id,omitempty"`
|
||||||
|
PromotionalOfferID string `json:"promotional_offer_id"`
|
||||||
|
SubscriptionGroupIdentifier string `json:"subscription_group_identifier"`
|
||||||
|
|
||||||
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,omitempty"`
|
||||||
|
|
||||||
ExpiresDate
|
ExpiresDate
|
||||||
|
|
||||||
PurchaseDate
|
PurchaseDate
|
||||||
@@ -120,6 +131,8 @@ type (
|
|||||||
SubscriptionPriceConsentStatus string `json:"price_consent_status"`
|
SubscriptionPriceConsentStatus string `json:"price_consent_status"`
|
||||||
ProductID string `json:"product_id"`
|
ProductID string `json:"product_id"`
|
||||||
OriginalTransactionID string `json:"original_transaction_id"`
|
OriginalTransactionID string `json:"original_transaction_id"`
|
||||||
|
|
||||||
|
GracePeriodDate
|
||||||
}
|
}
|
||||||
|
|
||||||
// The IAPResponse type has the response properties
|
// The IAPResponse type has the response properties
|
||||||
@@ -136,6 +149,18 @@ type (
|
|||||||
PendingRenewalInfo []PendingRenewalInfo `json:"pending_renewal_info,omitempty"`
|
PendingRenewalInfo []PendingRenewalInfo `json:"pending_renewal_info,omitempty"`
|
||||||
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
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ const (
|
|||||||
// Subscription was canceled by Apple customer support.
|
// Subscription was canceled by Apple customer support.
|
||||||
NotificationTypeCancel NotificationType = "CANCEL"
|
NotificationTypeCancel NotificationType = "CANCEL"
|
||||||
// Automatic renewal was successful for an expired subscription.
|
// Automatic renewal was successful for an expired subscription.
|
||||||
|
// Deprecated: DID_RECOVER should be used instead of RENEWAL
|
||||||
NotificationTypeRenewal NotificationType = "RENEWAL"
|
NotificationTypeRenewal NotificationType = "RENEWAL"
|
||||||
|
// Expired subscription recovered through a billing retry.
|
||||||
|
NotificationTypeDidRecover NotificationType = "DID_RECOVER"
|
||||||
// Customer renewed a subscription interactively after it lapsed.
|
// Customer renewed a subscription interactively after it lapsed.
|
||||||
NotificationTypeInteractiveRenewal NotificationType = "INTERACTIVE_RENEWAL"
|
NotificationTypeInteractiveRenewal NotificationType = "INTERACTIVE_RENEWAL"
|
||||||
// Customer changed the plan that takes affect at the next subscription renewal. Current active plan is not affected.
|
// Customer changed the plan that takes affect at the next subscription renewal. Current active plan is not affected.
|
||||||
@@ -53,6 +56,14 @@ type NotificationReceipt struct {
|
|||||||
CancellationDate
|
CancellationDate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NotificationUnifiedReceipt struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Environment Environment `json:"environment"`
|
||||||
|
LatestReceipt string `json:"latest_receipt"`
|
||||||
|
LatestReceiptInfo []InApp `json:"latest_receipt_info"`
|
||||||
|
PendingRenewalInfo []PendingRenewalInfo `json:"pending_renewal_info,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type SubscriptionNotification struct {
|
type SubscriptionNotification struct {
|
||||||
Environment NotificationEnvironment `json:"environment"`
|
Environment NotificationEnvironment `json:"environment"`
|
||||||
NotificationType NotificationType `json:"notification_type"`
|
NotificationType NotificationType `json:"notification_type"`
|
||||||
@@ -71,15 +82,23 @@ type SubscriptionNotification struct {
|
|||||||
ExpirationIntent string `json:"expiration_intent"`
|
ExpirationIntent string `json:"expiration_intent"`
|
||||||
|
|
||||||
// Auto renew info
|
// Auto renew info
|
||||||
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:"recipt"`
|
||||||
|
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.
|
||||||
LatestReceipt string `json:"latest_receipt"`
|
LatestReceipt string `json:"latest_receipt"`
|
||||||
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
|
||||||
|
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.
|
||||||
LatestExpiredReceipt string `json:"latest_expired_receipt"`
|
LatestExpiredReceipt string `json:"latest_expired_receipt"`
|
||||||
LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"`
|
LatestExpiredReceiptInfo NotificationReceipt `json:"latest_expired_receipt_info"`
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
|
|
||||||
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://boron2.ngrok.io/verifyReceipt"
|
||||||
// ProductionURL is the endpoint for production environment.
|
// ProductionURL is the endpoint for production environment.
|
||||||
ProductionURL string = "https://buy.itunes.apple.com/verifyReceipt"
|
ProductionURL string = "https://buy.itunes.apple.com/verifyReceipt"
|
||||||
// ContentType is the request content-type for apple store.
|
// ContentType is the request content-type for apple store.
|
||||||
@@ -96,64 +96,64 @@ 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()
|
||||||
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()
|
||||||
|
// 21007 is found when the receipt is from the test environment
|
||||||
return json.NewDecoder(resp.Body).Decode(result)
|
return Sandbox, json.NewDecoder(resp.Body).Decode(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return Production, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ func TestNewWithClient(t *testing.T) {
|
|||||||
func TestAcknowledgeSubscription(t *testing.T) {
|
func TestAcknowledgeSubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
expected := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
|
|
||||||
client, _ := New(jsonKey)
|
client, _ := New(jsonKey)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -93,7 +93,7 @@ func TestAcknowledgeSubscription(t *testing.T) {
|
|||||||
func TestVerifySubscription(t *testing.T) {
|
func TestVerifySubscription(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
expected := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
|
|
||||||
client, _ := New(jsonKey)
|
client, _ := New(jsonKey)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -121,7 +121,7 @@ func TestVerifySubscriptionAndroidPublisherError(t *testing.T) {
|
|||||||
func TestVerifyProduct(t *testing.T) {
|
func TestVerifyProduct(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Exception scenario
|
// Exception scenario
|
||||||
expected := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
expected := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
|
|
||||||
client, _ := New(jsonKey)
|
client, _ := New(jsonKey)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
@@ -159,7 +159,7 @@ func TestCancelSubscription(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client, _ = New(jsonKey)
|
client, _ = New(jsonKey)
|
||||||
expectedStr := "googleapi: Error 404: No application was found for the given package name., applicationNotFound"
|
expectedStr := "googleapi: Error 400: Invalid Value, invalid"
|
||||||
actual = client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
|
actual = client.CancelSubscription(ctx, "package", "productID", "purchaseToken")
|
||||||
|
|
||||||
if actual.Error() != expectedStr {
|
if actual.Error() != expectedStr {
|
||||||
|
|||||||
Reference in New Issue
Block a user