34 Commits

Author SHA1 Message Date
sanjid133
5d167ac1a5 Merge branch 'master' of https://github.com/awa/go-iap 2019-12-05 10:53:06 +06:00
Junpei Tsuji
b31646baf4 Merge pull request #101 from mlesar/unified_receipt
Adding unified receipt to the Apple S2S notification
2019-11-26 00:20:18 +09:00
Matija Lesar
1bd99243d7 Changed environment property type 2019-11-25 12:42:41 +01:00
Matija Lesar
7d624ad068 Updated order of properties 2019-11-25 11:26:42 +01:00
Matija Lesar
31a625b71e CR changes 2019-11-25 11:25:31 +01:00
Matija Lesar
4209b06a64 Added unified receipt 2019-11-25 10:00:13 +01:00
Junpei Tsuji
3d3bd2b6cf Merge pull request #99 from jun06t/error-message
Fixed test error messages
2019-10-10 11:56:32 +09:00
Junpei Tsuji
934d4ffbbd Fixed test error messages 2019-10-10 10:58:16 +09:00
Junpei Tsuji
9c0e16f820 Merge pull request #96 from jun06t/appstore-grace-period
Added grace period fields
2019-09-17 13:47:52 +09:00
Junpei Tsuji
e895c80eb0 Added grace period fields 2019-09-17 13:23:05 +09:00
Hasan
4b88aefda5 Merge pull request #4 from Tapfury/noti
updated notificaiton
2019-08-18 17:07:26 +06:00
sanjid133
62fa34fffb Merge branch 'master' of github.com:Tapfury/go-iap into noti
# Conflicts:
#	appstore/model.go
#	appstore/notification.go
2019-08-18 17:06:30 +06:00
sanjid133
8414aff344 status field not present in initial_buy 2019-08-18 16:56:32 +06:00
sanjid133
e0f6e608e7 auto renew status string 2019-08-18 16:56:32 +06:00
sanjid133
11e072e211 Add renewal field 2019-08-18 16:56:32 +06:00
sanjid133
b60c954624 Verfiy latest receipt 2019-08-18 16:56:32 +06:00
sanjid133
c92634081e Add missing is_upgrade field 2019-08-18 16:56:32 +06:00
Minhaz Ahmed Syrus
fbfe02b5f6 Add missing subscription billing retry flag 2019-08-18 16:56:32 +06:00
Minhaz Ahmed Syrus
bac6b5676b Add missing notification verification fields 2019-08-18 16:56:09 +06:00
Minhaz Ahmed Syrus
94f276769f Add missing subscription billing retry flag 2019-08-18 16:54:57 +06:00
Minhaz Ahmed Syrus
4ed348bb1b Add missing notification verification fields 2019-08-18 16:54:57 +06:00
Hasan
712b3f7da2 Merge pull request #3 from Tapfury/renew
Add renewal field
2019-08-07 15:37:04 +06:00
sanjid133
5354251ea5 Add renewal field 2019-08-07 15:16:30 +06:00
Hasan
ec53640acb Merge pull request #2 from Tapfury/latestReceipt
Verfiy latest receipt
2019-08-06 18:51:28 +06:00
sanjid133
ee59170931 Verfiy latest receipt 2019-08-06 17:22:41 +06:00
Hasan
c992b9705b Merge pull request #1 from Tapfury/isupgrade
Add missing is_upgrade field
2019-08-01 17:55:11 +06:00
sanjid133
ff9fd778a3 Add missing is_upgrade field 2019-08-01 17:53:26 +06:00
Minhaz Ahmed Syrus
1877c0ae24 Add missing subscription billing retry flag 2019-08-01 17:34:39 +06:00
Minhaz Ahmed Syrus
7ef252fde0 Add missing notification verification fields 2019-08-01 17:34:39 +06:00
kitakitabauer
062102b0f3 Merge pull request #93 from awa/androidpublisher-v3
Update androidpublisher api v3
2019-06-19 16:36:38 +09:00
Minhaz Syrus
12a8101bb0 Merge branch 'master' of github.com:awa/go-iap 2019-03-23 19:28:46 +06:00
Minhaz Syrus
f94fdb06d8 Merge branch 'master' of github.com:awa/go-iap 2019-02-02 12:17:08 +06:00
Minhaz Ahmed Syrus
c371d6eb78 Add missing subscription billing retry flag 2018-11-06 16:58:05 +06:00
Minhaz Ahmed Syrus
6d5e856650 Add missing notification verification fields 2018-11-06 14:25:38 +06:00
4 changed files with 69 additions and 24 deletions

View File

@@ -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"`
ExpiresDate ExpiresDate
PurchaseDate PurchaseDate
@@ -93,6 +104,7 @@ type (
CancellationDate CancellationDate
CancellationReason string `json:"cancellation_reason,omitempty"` CancellationReason string `json:"cancellation_reason,omitempty"`
IsUpgraded string `json:"is_upgraded,omitempty"`
} }
// The Receipt type has whole data of receipt // The Receipt type has whole data of receipt
@@ -120,6 +132,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 +150,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 string `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

View File

@@ -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"`

View File

@@ -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
} }

View File

@@ -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 {