├── .gitignore ├── go.mod ├── order.go ├── refund.go ├── internal ├── auth │ ├── rsa.go │ └── type.go └── storekit │ ├── token.go │ └── claims.go ├── auth_type.go ├── order_type.go ├── subscription.go ├── LICENSE ├── notification.go ├── transaction.go ├── refund_type.go ├── auth_test.go ├── subscription_type.go ├── receipt.go ├── apple.go ├── go.sum ├── README.md ├── transaction_type.go ├── auth.go ├── refund_test.go ├── order_test.go ├── apple_type.go ├── transaction_test.go ├── notification_type.go ├── subscription_test.go ├── receipt_test.go ├── receipt_type.go └── notification_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | .idea 17 | *.iml 18 | .DS_Store -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/smartwalle/apple 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/golang-jwt/jwt/v5 v5.0.0 7 | github.com/google/uuid v1.3.1 8 | github.com/smartwalle/dbc v0.0.20 9 | github.com/smartwalle/ncrypto v1.0.4 10 | github.com/smartwalle/ngx v1.0.9 11 | github.com/smartwalle/nsync v0.0.7 12 | ) 13 | 14 | require github.com/smartwalle/queue v0.0.4 // indirect 15 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import "net/http" 4 | 5 | const ( 6 | kOrderLookup = "/v1/lookup/" 7 | ) 8 | 9 | // OrderLookup https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id 10 | func (c *Client) OrderLookup(orderId string) (result *OrderLookupResponse, err error) { 11 | err = c.request(http.MethodGet, c.BuildAPI(kOrderLookup, orderId), nil, nil, &result) 12 | return result, err 13 | } 14 | -------------------------------------------------------------------------------- /refund.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import "net/http" 4 | 5 | const ( 6 | kRefundLookup = "/v2/refund/lookup/" 7 | ) 8 | 9 | // RefundLookup https://developer.apple.com/documentation/appstoreserverapi/get_refund_history 10 | func (c *Client) RefundLookup(transactionId string, param RefundLookupParam) (result *RefundLookupResponse, err error) { 11 | err = c.request(http.MethodGet, c.BuildAPI(kRefundLookup, transactionId), param, nil, &result) 12 | return result, err 13 | } 14 | -------------------------------------------------------------------------------- /internal/auth/rsa.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rsa" 5 | "encoding/base64" 6 | "math/big" 7 | ) 8 | 9 | func DecodePublicKey(n, e string) (*rsa.PublicKey, error) { 10 | nByte, err := base64.RawURLEncoding.DecodeString(n) 11 | if err != nil { 12 | return nil, err 13 | } 14 | eByte, err := base64.RawURLEncoding.DecodeString(e) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | var pKey rsa.PublicKey 20 | 21 | pKey.N = big.NewInt(0).SetBytes(nByte) 22 | pKey.E = int(big.NewInt(0).SetBytes(eByte).Uint64()) 23 | 24 | return &pKey, nil 25 | } 26 | -------------------------------------------------------------------------------- /auth_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | type User struct { 4 | Id string `json:"id"` 5 | Issuer string `json:"issuer"` 6 | Sub string `json:"sub"` 7 | BundleId string `json:"bundle_id"` 8 | Email string `json:"email"` 9 | EmailVerified bool `json:"email_verified"` 10 | IsPrivateEmail bool `json:"is_private_email"` 11 | RealUserStatus int `json:"real_user_status"` 12 | Nonce string `json:"nonce"` 13 | AuthTime int64 `json:"auth_time"` 14 | IssuedAt int64 `json:"issued_at"` 15 | ExpiresAt int64 `json:"expires_at"` 16 | TransferSub string `json:"transfer_sub"` 17 | } 18 | -------------------------------------------------------------------------------- /internal/auth/type.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import "github.com/golang-jwt/jwt/v5" 4 | 5 | type Header struct { 6 | Kid string `json:"kid"` 7 | Alg string `json:"alg"` 8 | } 9 | 10 | type Bool bool 11 | 12 | type Claims struct { 13 | jwt.RegisteredClaims 14 | CHash string `json:"c_hash"` 15 | AuthTime int `json:"auth_time"` 16 | Nonce string `json:"nonce"` 17 | NonceSupported Bool `json:"nonce_supported"` 18 | Email string `json:"email"` 19 | EmailVerified Bool `json:"email_verified"` 20 | IsPrivateEmail Bool `json:"is_private_email"` 21 | RealUserStatus int `json:"real_user_status"` 22 | TransferSub string `json:"transfer_sub"` 23 | } 24 | 25 | type Key struct { 26 | Kty string `json:"kty"` 27 | Kid string `json:"kid"` 28 | Use string `json:"use"` 29 | Alg string `json:"alg"` 30 | N string `json:"n"` 31 | E string `json:"e"` 32 | } 33 | -------------------------------------------------------------------------------- /order_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import "encoding/json" 4 | 5 | // OrderLookupResponse https://developer.apple.com/documentation/appstoreserverapi/orderlookupresponse 6 | type OrderLookupResponse struct { 7 | Status int `json:"status"` 8 | Transactions []*Transaction `json:"transactions"` 9 | } 10 | 11 | type OrderLookupResponseAlias OrderLookupResponse 12 | 13 | func (o *OrderLookupResponse) UnmarshalJSON(data []byte) (err error) { 14 | var aux = struct { 15 | *OrderLookupResponseAlias 16 | SignedTransactions []SignedTransaction `json:"signedTransactions"` 17 | }{ 18 | OrderLookupResponseAlias: (*OrderLookupResponseAlias)(o), 19 | } 20 | 21 | if err = json.Unmarshal(data, &aux); err != nil { 22 | return err 23 | } 24 | 25 | for _, item := range aux.SignedTransactions { 26 | var transaction *Transaction 27 | transaction, err = item.Decode() 28 | if err != nil { 29 | return err 30 | } 31 | o.Transactions = append(o.Transactions, transaction) 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /subscription.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | kGetSubscriptions = "/v1/subscriptions/" 11 | kExtendSubscription = "/v1/subscriptions/extend/" 12 | ) 13 | 14 | // GetSubscriptionsStatuses https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses 15 | func (c *Client) GetSubscriptionsStatuses(transactionId string) (result *SubscriptionsStatusResponse, err error) { 16 | err = c.request(http.MethodGet, c.BuildAPI(kGetSubscriptions, transactionId), nil, nil, &result) 17 | return result, err 18 | } 19 | 20 | // ExtendSubscription https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date 21 | func (c *Client) ExtendSubscription(transactionId string, param ExtendRenewalDateParam) (result *ExtendRenewalDateResponse, err error) { 22 | data, err := json.Marshal(param) 23 | if err != nil { 24 | return nil, err 25 | } 26 | err = c.request(http.MethodPut, c.BuildAPI(kExtendSubscription, transactionId), nil, bytes.NewReader(data), &result) 27 | return result, err 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 SmartWalle. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /notification.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/smartwalle/apple/internal/storekit" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | kTestNotification = "/v1/notifications/test" 11 | ) 12 | 13 | // RequestTestNotification https://developer.apple.com/documentation/appstoreserverapi/request_a_test_notification 14 | func (c *Client) RequestTestNotification() (result *TestNotificationResponse, err error) { 15 | err = c.request(http.MethodPost, c.BuildAPI(kTestNotification), nil, nil, &result) 16 | return result, err 17 | } 18 | 19 | // DecodeNotification 用于解析通知数据 https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2 20 | // 21 | // 关于接收到苹果服务器推送的通知之后,业务服务器如何响应参照: 22 | // https://developer.apple.com/documentation/appstoreservernotifications/responding_to_app_store_server_notifications 23 | func (c *Client) DecodeNotification(data []byte) (*Notification, error) { 24 | return DecodeNotification(data) 25 | } 26 | 27 | func DecodeNotification(data []byte) (*Notification, error) { 28 | var aux = struct { 29 | SignedPayload string `json:"signedPayload"` 30 | }{} 31 | 32 | if err := json.Unmarshal(data, &aux); err != nil { 33 | return nil, err 34 | } 35 | 36 | var notification = &Notification{} 37 | if err := storekit.DecodeClaims(aux.SignedPayload, notification); err != nil { 38 | return nil, err 39 | } 40 | return notification, nil 41 | } 42 | -------------------------------------------------------------------------------- /transaction.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/http" 7 | ) 8 | 9 | const ( 10 | kTransaction = "/v1/transactions/" 11 | kTransactionHistory = "/v1/history/" 12 | kTransactionConsumption = "/v1/transactions/consumption/" 13 | ) 14 | 15 | // GetTransaction https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info 16 | func (c *Client) GetTransaction(transactionId string) (result *TransactionResponse, err error) { 17 | err = c.request(http.MethodGet, c.BuildAPI(kTransaction, transactionId), nil, nil, &result) 18 | return result, err 19 | } 20 | 21 | // GetTransactionHistory https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history 22 | func (c *Client) GetTransactionHistory(transactionId string, param TransactionHistoryParam) (result *TransactionHistoryResponse, err error) { 23 | err = c.request(http.MethodGet, c.BuildAPI(kTransactionHistory, transactionId), param, nil, &result) 24 | return result, err 25 | } 26 | 27 | // SendConsumptionInformation https://developer.apple.com/documentation/appstoreserverapi/send_consumption_information 28 | func (c *Client) SendConsumptionInformation(transactionId string, param ConsumptionParam) (err error) { 29 | data, err := json.Marshal(param) 30 | if err != nil { 31 | return err 32 | } 33 | err = c.request(http.MethodPut, c.BuildAPI(kTransactionConsumption, transactionId), nil, bytes.NewReader(data), nil) 34 | return err 35 | } 36 | -------------------------------------------------------------------------------- /refund_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "encoding/json" 5 | "net/url" 6 | ) 7 | 8 | // RefundLookupParam https://developer.apple.com/documentation/appstoreserverapi/get_refund_history#query-parameters 9 | type RefundLookupParam struct { 10 | Revision string 11 | } 12 | 13 | func (r RefundLookupParam) Values() url.Values { 14 | var values = url.Values{} 15 | if r.Revision != "" { 16 | values.Set("revision", r.Revision) 17 | } 18 | return values 19 | } 20 | 21 | // RefundLookupResponse https://developer.apple.com/documentation/appstoreserverapi/refundhistoryresponse 22 | type RefundLookupResponse struct { 23 | HasMore bool `json:"hasMore"` 24 | Revision string `json:"revision"` 25 | Transactions []*Transaction `json:"transactions"` 26 | } 27 | 28 | type RefundLookupResponseAlias RefundLookupResponse 29 | 30 | func (r *RefundLookupResponse) UnmarshalJSON(data []byte) (err error) { 31 | var aux = struct { 32 | *RefundLookupResponseAlias 33 | SignedTransactions []SignedTransaction `json:"signedTransactions"` 34 | }{ 35 | RefundLookupResponseAlias: (*RefundLookupResponseAlias)(r), 36 | } 37 | 38 | if err = json.Unmarshal(data, &aux); err != nil { 39 | return err 40 | } 41 | 42 | for _, item := range aux.SignedTransactions { 43 | var transaction *Transaction 44 | transaction, err = item.Decode() 45 | if err != nil { 46 | return err 47 | } 48 | r.Transactions = append(r.Transactions, transaction) 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/storekit/token.go: -------------------------------------------------------------------------------- 1 | package storekit 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "fmt" 6 | "github.com/golang-jwt/jwt/v5" 7 | "github.com/google/uuid" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const ( 13 | kTokenTimeout = 3000 14 | ) 15 | 16 | type Token struct { 17 | sync.Mutex 18 | 19 | bearer string 20 | issuedAt int64 21 | keyId string 22 | issuer string 23 | bundleId string 24 | privateKey *ecdsa.PrivateKey 25 | } 26 | 27 | func NewToken(privateKey *ecdsa.PrivateKey, keyId, issuer, bundleId string) *Token { 28 | return &Token{ 29 | keyId: keyId, 30 | issuer: issuer, 31 | bundleId: bundleId, 32 | privateKey: privateKey, 33 | } 34 | } 35 | 36 | func (t *Token) generate() (string, int64, error) { 37 | var issuedAt = time.Now().Unix() 38 | var expiredAt = issuedAt + kTokenTimeout 39 | var nToken = jwt.New(jwt.SigningMethodES256) 40 | nToken.Header["kid"] = t.keyId 41 | nToken.Claims = jwt.MapClaims{ 42 | "iss": t.issuer, 43 | "iat": issuedAt, 44 | "exp": expiredAt, 45 | "aud": "appstoreconnect-v1", 46 | "nonce": uuid.NewString(), 47 | "bid": t.bundleId, 48 | } 49 | var bearer, err = nToken.SignedString(t.privateKey) 50 | if err != nil { 51 | return "", 0, err 52 | } 53 | return fmt.Sprintf("Bearer %s", bearer), issuedAt, nil 54 | } 55 | 56 | func (t *Token) Bearer() string { 57 | t.Lock() 58 | defer t.Unlock() 59 | 60 | if t.expired() { 61 | t.bearer, t.issuedAt, _ = t.generate() 62 | } 63 | return t.bearer 64 | } 65 | 66 | func (t *Token) expired() bool { 67 | return time.Now().Unix() >= (t.issuedAt + kTokenTimeout) 68 | } 69 | -------------------------------------------------------------------------------- /auth_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "github.com/smartwalle/apple" 5 | "testing" 6 | ) 7 | 8 | func TestAuthClient_DecodeToken(t *testing.T) { 9 | var c = apple.NewAuthClient() 10 | t.Log(c.DecodeToken("eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNjeW91bGUuYmFsbHUzZCIsImV4cCI6MTY4MjU3MDMxOCwiaWF0IjoxNjgyNDgzOTE4LCJzdWIiOiIwMDEwOTIuYTg3MzU4NDA0ZjExNDUwMThjMTYxMmVkZWZkOTliN2QuMDU1MyIsImNfaGFzaCI6IjgtTWtQcDZPSlNrUjlvcnEzLVB0RWciLCJlbWFpbCI6Ijdzcnd4OWpidjRAcHJpdmF0ZXJlbGF5LmFwcGxlaWQuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiaXNfcHJpdmF0ZV9lbWFpbCI6InRydWUiLCJhdXRoX3RpbWUiOjE2ODI0ODM5MTgsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.1jPI8zPVi4tqdW3rdDM67Jao6vGZSK3KdOk8mQlq_u21W6rrc9qQNKwi_rNnFWzHlpu6Z_ffU24zio9192jXSJeqPnixB-bqkH9PM5QfTja4XFsPzR9LVaQIPzwTrvVPPaz87vLXDi7YbjKoMLFbAvAVqvJcwbg0J9e0dDhcSUPPtnfkd0wymtzyBONBHqRls38vkUBwPSadddBvRDCXZP64bb_LEIdxvJWasO-G55QlmDI2QBfsIYaO99wUkVlsgRpyzuYVYpWRaKj3x-lnEDEbePK_M1eW1P_VH6vdMb1cRwvIhlayI9363U3tlljqKWbYfINLxE_sGUVfPDmvLw")) 11 | } 12 | 13 | func TestAuthClient_VerifyToken(t *testing.T) { 14 | var c = apple.NewAuthClient(apple.WithBundleId("com.scyoule.ballu3d")) 15 | t.Log(c.VerifyToken("eyJraWQiOiJXNldjT0tCIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLnNjeW91bGUuYmFsbHUzZCIsImV4cCI6MTY4MjU3MDMxOCwiaWF0IjoxNjgyNDgzOTE4LCJzdWIiOiIwMDEwOTIuYTg3MzU4NDA0ZjExNDUwMThjMTYxMmVkZWZkOTliN2QuMDU1MyIsImNfaGFzaCI6IjgtTWtQcDZPSlNrUjlvcnEzLVB0RWciLCJlbWFpbCI6Ijdzcnd4OWpidjRAcHJpdmF0ZXJlbGF5LmFwcGxlaWQuY29tIiwiZW1haWxfdmVyaWZpZWQiOiJ0cnVlIiwiaXNfcHJpdmF0ZV9lbWFpbCI6InRydWUiLCJhdXRoX3RpbWUiOjE2ODI0ODM5MTgsIm5vbmNlX3N1cHBvcnRlZCI6dHJ1ZX0.1jPI8zPVi4tqdW3rdDM67Jao6vGZSK3KdOk8mQlq_u21W6rrc9qQNKwi_rNnFWzHlpu6Z_ffU24zio9192jXSJeqPnixB-bqkH9PM5QfTja4XFsPzR9LVaQIPzwTrvVPPaz87vLXDi7YbjKoMLFbAvAVqvJcwbg0J9e0dDhcSUPPtnfkd0wymtzyBONBHqRls38vkUBwPSadddBvRDCXZP64bb_LEIdxvJWasO-G55QlmDI2QBfsIYaO99wUkVlsgRpyzuYVYpWRaKj3x-lnEDEbePK_M1eW1P_VH6vdMb1cRwvIhlayI9363U3tlljqKWbYfINLxE_sGUVfPDmvLw")) 16 | } 17 | -------------------------------------------------------------------------------- /subscription_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import "encoding/json" 4 | 5 | // SubscriptionsStatusResponse https://developer.apple.com/documentation/appstoreserverapi/statusresponse 6 | type SubscriptionsStatusResponse struct { 7 | Data []*SubscriptionGroupIdentifier `json:"data"` 8 | Environment Environment `json:"environment"` 9 | AppAppleId int `json:"appAppleId"` 10 | BundleId string `json:"bundleId"` 11 | } 12 | 13 | type SubscriptionGroupIdentifier struct { 14 | SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"` 15 | LastTransactions []*LastTransaction `json:"lastTransactions"` 16 | } 17 | 18 | type LastTransaction struct { 19 | OriginalTransactionId string `json:"originalTransactionId"` 20 | Status int `json:"status"` 21 | Renewal *Renewal `json:"renewal"` 22 | Transaction *Transaction `json:"transaction"` 23 | } 24 | 25 | type LastTransactionAlias LastTransaction 26 | 27 | func (t *LastTransaction) UnmarshalJSON(data []byte) (err error) { 28 | var aux = struct { 29 | *LastTransactionAlias 30 | SignedRenewal SignedRenewal `json:"signedRenewalInfo"` 31 | SignedTransaction SignedTransaction `json:"signedTransactionInfo"` 32 | }{ 33 | LastTransactionAlias: (*LastTransactionAlias)(t), 34 | } 35 | 36 | if err = json.Unmarshal(data, &aux); err != nil { 37 | return err 38 | } 39 | 40 | if t.Renewal, err = aux.SignedRenewal.Decode(); err != nil { 41 | return err 42 | } 43 | if t.Transaction, err = aux.SignedTransaction.Decode(); err != nil { 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | // ExtendRenewalDateParam https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldaterequest 50 | type ExtendRenewalDateParam struct { 51 | ExtendByDays int `json:"extendByDays"` 52 | ExtendReasonCode int `json:"extendReasonCode"` 53 | RequestIdentifier string `json:"requestIdentifier"` 54 | } 55 | 56 | // ExtendRenewalDateResponse https://developer.apple.com/documentation/appstoreserverapi/extendrenewaldateresponse 57 | type ExtendRenewalDateResponse struct { 58 | EffectiveDate int64 `json:"effectiveDate"` 59 | OriginalTransactionId string `json:"originalTransactionId"` 60 | Success bool `json:"success"` 61 | WebOrderLineItemId string `json:"webOrderLineItemId"` 62 | } 63 | -------------------------------------------------------------------------------- /receipt.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/http" 9 | ) 10 | 11 | const ( 12 | kVerifyReceiptProduction = "https://buy.itunes.apple.com/verifyReceipt" 13 | kVerifyReceiptSandbox = "https://sandbox.itunes.apple.com/verifyReceipt" 14 | ) 15 | 16 | var ( 17 | ErrBadReceipt = errors.New("bad receipt") 18 | ) 19 | 20 | // VerifyReceipt 验证苹果内购交易是否有效 https://developer.apple.com/documentation/appstorereceipts/verifyreceipt 21 | // 首先请求苹果的服务器,获取票据(receipt)的详细信息,然后验证交易信息(transactionId)是否属于该票据, 22 | // 如果交易信息在票据中,则返回详细的交易信息。 23 | // 注意:本方法会先调用苹果生产环境接口进行票据查询,如果返回票据信息为测试环境中的信息时,则调用测试环境接口进行查询。 24 | func VerifyReceipt(transactionId, receipt string, opts ...ReceiptOption) (*ReceiptSummary, *InApp, error) { 25 | var summary, err = GetReceipt(receipt, opts...) 26 | if err != nil { 27 | return nil, nil, err 28 | } 29 | 30 | // 没有交易信息 31 | if summary == nil { 32 | return nil, nil, ErrBadReceipt 33 | } 34 | 35 | // 票据查询失败 36 | if summary.Status != 0 { 37 | return nil, nil, fmt.Errorf("bad receipt: %d", summary.Status) 38 | } 39 | 40 | // 验证 transactionId 和 receipt 是否匹配 41 | if summary.Receipt != nil { 42 | for _, info := range summary.Receipt.InApp { 43 | if info.TransactionId == transactionId { 44 | return summary, info, nil 45 | } 46 | } 47 | } 48 | return nil, nil, ErrBadReceipt 49 | } 50 | 51 | // GetReceipt 获取票据信息 52 | // 53 | // 注意:本方法会先调用苹果生产环境接口进行票据查询,如果返回票据信息为测试环境中的信息时,则调用测试环境接口进行查询。 54 | func GetReceipt(receipt string, opts ...ReceiptOption) (*ReceiptSummary, error) { 55 | var nOpt = &ReceiptOptions{} 56 | nOpt.Client = http.DefaultClient 57 | nOpt.Receipt = receipt 58 | for _, opt := range opts { 59 | if opt != nil { 60 | opt(nOpt) 61 | } 62 | } 63 | 64 | // 从生产环境查询 65 | var summary, err = getReceipt(kVerifyReceiptProduction, nOpt) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | // 如果返回票据信息为测试环境中的信息时,则调用测试环境接口进行查询 71 | if summary != nil && summary.Status == 21007 { 72 | summary, err = getReceipt(kVerifyReceiptSandbox, nOpt) 73 | if err != nil { 74 | return nil, err 75 | } 76 | } 77 | return summary, nil 78 | } 79 | 80 | func getReceipt(url string, opts *ReceiptOptions) (*ReceiptSummary, error) { 81 | var data, err = json.Marshal(opts) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | rsp, err := opts.Client.Do(req) 92 | if err != nil { 93 | return nil, err 94 | } 95 | defer rsp.Body.Close() 96 | 97 | var summary *ReceiptSummary 98 | var decoder = json.NewDecoder(rsp.Body) 99 | if err = decoder.Decode(&summary); err != nil { 100 | return nil, err 101 | } 102 | 103 | return summary, nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/storekit/claims.go: -------------------------------------------------------------------------------- 1 | package storekit 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/x509" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "github.com/golang-jwt/jwt/v5" 10 | "github.com/smartwalle/ncrypto" 11 | "strings" 12 | ) 13 | 14 | const kRootPEM = `-----BEGIN CERTIFICATE----- 15 | MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwS 16 | QXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0aW9u 17 | IEF1dGhvcml0eTETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UEBhMCVVMwHhcN 18 | MTQwNDMwMTgxOTA2WhcNMzkwNDMwMTgxOTA2WjBnMRswGQYDVQQDDBJBcHBsZSBS 19 | b290IENBIC0gRzMxJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9y 20 | aXR5MRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQGEwJVUzB2MBAGByqGSM49 21 | AgEGBSuBBAAiA2IABJjpLz1AcqTtkyJygRMc3RCV8cWjTnHcFBbZDuWmBSp3ZHtf 22 | TjjTuxxEtX/1H7YyYl3J6YRbTzBPEVoA/VhYDKX1DyxNB0cTddqXl5dvMVztK517 23 | IDvYuVTZXpmkOlEKMaNCMEAwHQYDVR0OBBYEFLuw3qFYM4iapIqZ3r6966/ayySr 24 | MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gA 25 | MGUCMQCD6cHEFl4aXTQY2e3v9GwOAEZLuN+yRhHFD/3meoyhpmvOwgPUnPWTxnS4 26 | at+qIxUCMG1mihDK1A3UT82NQz60imOlM27jbdoXt2QfyFMm+YhidDkLF1vLUagM 27 | 6BgD56KyKA== 28 | -----END CERTIFICATE----- 29 | ` 30 | 31 | type Header struct { 32 | Alg string `json:"alg"` 33 | X5C []string `json:"x5c"` 34 | } 35 | 36 | func DecodeClaims(payload string, claims jwt.Claims) error { 37 | headerBytes, err := base64.RawStdEncoding.DecodeString(strings.Split(payload, ".")[0]) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | //var data, _ = base64.RawStdEncoding.DecodeString(strings.Split(payload, ".")[1]) 43 | //fmt.Println(string(data)) 44 | 45 | var header *Header 46 | if err = json.Unmarshal(headerBytes, &header); err != nil { 47 | return err 48 | } 49 | 50 | rootCert, err := ncrypto.DecodeCertificate([]byte(kRootPEM)) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | intermediateCert, err := ncrypto.DecodeCertificate([]byte(header.X5C[1])) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | cert, err := ncrypto.DecodeCertificate([]byte(header.X5C[0])) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | if err = verifyCert(rootCert, intermediateCert, cert); err != nil { 66 | return err 67 | } 68 | 69 | if _, err = jwt.ParseWithClaims(payload, claims, func(token *jwt.Token) (interface{}, error) { 70 | switch publicKey := cert.PublicKey.(type) { 71 | case *ecdsa.PublicKey: 72 | return publicKey, nil 73 | default: 74 | return nil, errors.New("key is not a valid *ecdsa.PublicKey") 75 | } 76 | }); err != nil { 77 | return err 78 | } 79 | return nil 80 | } 81 | 82 | func verifyCert(rootCert, intermediateCert, cert *x509.Certificate) error { 83 | var roots = x509.NewCertPool() 84 | roots.AddCert(rootCert) 85 | 86 | var intermediates = x509.NewCertPool() 87 | intermediates.AddCert(intermediateCert) 88 | intermediates.AddCert(rootCert) 89 | 90 | var opts = x509.VerifyOptions{ 91 | Roots: roots, 92 | Intermediates: intermediates, 93 | } 94 | if _, err := cert.Verify(opts); err != nil { 95 | return err 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /apple.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/smartwalle/apple/internal/storekit" 7 | "github.com/smartwalle/ncrypto" 8 | "github.com/smartwalle/ngx" 9 | "io" 10 | "net/http" 11 | "os" 12 | "strings" 13 | ) 14 | 15 | const ( 16 | kStoreKitSandbox = "https://api.storekit-sandbox.itunes.apple.com/inApps" 17 | kStoreKitProduction = "https://api.storekit.itunes.apple.com/inApps" 18 | ) 19 | 20 | // Client 苹果 App Store Server API 21 | type Client struct { 22 | Client *http.Client 23 | token *storekit.Token 24 | host string 25 | } 26 | 27 | func New(p8key []byte, keyId, issuer, bundleId string, isProduction bool) (*Client, error) { 28 | var pKey, err = ncrypto.DecodePrivateKey(p8key).PKCS8().ECDSAPrivateKey() 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | var nClient = &Client{} 34 | nClient.Client = http.DefaultClient 35 | nClient.token = storekit.NewToken(pKey, keyId, issuer, bundleId) 36 | 37 | if isProduction { 38 | nClient.host = kStoreKitProduction 39 | } else { 40 | nClient.host = kStoreKitSandbox 41 | } 42 | 43 | return nClient, nil 44 | } 45 | 46 | func NewWithKeyFile(filename, keyId, issuer, bundleId string, isProduction bool) (*Client, error) { 47 | data, err := os.ReadFile(filename) 48 | if err != nil { 49 | return nil, err 50 | } 51 | return New(data, keyId, issuer, bundleId, isProduction) 52 | } 53 | 54 | func (c *Client) BuildAPI(paths ...string) string { 55 | var path = c.host 56 | for _, p := range paths { 57 | p = strings.TrimSpace(p) 58 | if len(p) > 0 { 59 | if strings.HasSuffix(path, "/") { 60 | path = path + p 61 | } else { 62 | if strings.HasPrefix(p, "/") { 63 | path = path + p 64 | } else { 65 | path = path + "/" + p 66 | } 67 | } 68 | } 69 | } 70 | return path 71 | } 72 | 73 | func (c *Client) request(method, url string, param Param, body io.Reader, result interface{}) (err error) { 74 | var req = ngx.NewRequest(method, url, ngx.WithClient(c.Client)) 75 | if param != nil { 76 | req.SetForm(param.Values()) 77 | } 78 | if body != nil { 79 | req.SetBody(body) 80 | req.SetContentType(ngx.ContentTypeJSON) 81 | } 82 | req.Header().Set("Authorization", c.token.Bearer()) 83 | 84 | rsp, err := req.Do(context.Background()) 85 | if err != nil { 86 | return err 87 | } 88 | defer rsp.Body.Close() 89 | 90 | data, err := io.ReadAll(rsp.Body) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | switch rsp.StatusCode { 96 | case http.StatusOK: 97 | return json.Unmarshal(data, result) 98 | case http.StatusAccepted: 99 | return nil 100 | case http.StatusUnauthorized: 101 | return &Error{Code: http.StatusUnauthorized, Message: http.StatusText(http.StatusUnauthorized)} 102 | default: 103 | if len(data) == 0 { 104 | return &Error{Code: rsp.StatusCode, Message: http.StatusText(rsp.StatusCode)} 105 | } 106 | 107 | var rErr *Error 108 | if err = json.Unmarshal(data, &rErr); err != nil { 109 | return err 110 | } 111 | return rErr 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= 2 | github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 6 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/smartwalle/dbc v0.0.18 h1:HeAebZJDCuATkPLfIZnFFyK+vMqblL0vacoCWvSyCgk= 8 | github.com/smartwalle/dbc v0.0.18/go.mod h1:JsFDfdJxgPbT/6X4kUw/mDumKIH/f3vKpV5EPIuDfqg= 9 | github.com/smartwalle/dbc v0.0.19 h1:ywyxJVFVgycp2bWTFyOf/OiyA8UB4Pn+p/RfNuN7rmk= 10 | github.com/smartwalle/dbc v0.0.19/go.mod h1:mO9WdNrdrwUW2Ty2H6hF17aEFXySxQwKmmoJa9wO+AQ= 11 | github.com/smartwalle/dbc v0.0.20 h1:hGg5Dr7sDwfqWukv/EQ7Q7pxQ0l3bXRE9vy1W4mDEkk= 12 | github.com/smartwalle/dbc v0.0.20/go.mod h1:mO9WdNrdrwUW2Ty2H6hF17aEFXySxQwKmmoJa9wO+AQ= 13 | github.com/smartwalle/ncrypto v1.0.0 h1:nQFxIS3fRgr8V0xRkhnfNQOrcJGPNF6d5XzFwVm79KU= 14 | github.com/smartwalle/ncrypto v1.0.0/go.mod h1:NmCbG0nLnSDnMImEDrjptFKs0PiLThnFkjQSMtGYgs4= 15 | github.com/smartwalle/ncrypto v1.0.1 h1:CVe/h/srt6knLiF/9V5OnDDhguskWR801meHizq7KmU= 16 | github.com/smartwalle/ncrypto v1.0.1/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= 17 | github.com/smartwalle/ncrypto v1.0.2 h1:pTAhCqtPCMhpOwFXX+EcMdR6PNzruBNoGQrN2S1GbGI= 18 | github.com/smartwalle/ncrypto v1.0.2/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= 19 | github.com/smartwalle/ncrypto v1.0.3 h1:fnzjoriZt2LZeD8ljEtRe2eU33Au7i8vIF4Gafz5RuI= 20 | github.com/smartwalle/ncrypto v1.0.3/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= 21 | github.com/smartwalle/ncrypto v1.0.4 h1:P2rqQxDepJwgeO5ShoC+wGcK2wNJDmcdBOWAksuIgx8= 22 | github.com/smartwalle/ncrypto v1.0.4/go.mod h1:Dwlp6sfeNaPMnOxMNayMTacvC5JGEVln3CVdiVDgbBk= 23 | github.com/smartwalle/ngx v1.0.5 h1:L31bGSybA/h9jh2/Aj6+oqjgKl4PU5Vzt6Kh+ewCEqI= 24 | github.com/smartwalle/ngx v1.0.5/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= 25 | github.com/smartwalle/ngx v1.0.6 h1:JPNqNOIj+2nxxFtrSkJO+vKJfeNUSEQueck/Wworjps= 26 | github.com/smartwalle/ngx v1.0.6/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= 27 | github.com/smartwalle/ngx v1.0.7 h1:BIQo6wmAnERehogNKUnthoxwBavTWxbR9oLFcGjWXKQ= 28 | github.com/smartwalle/ngx v1.0.7/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= 29 | github.com/smartwalle/ngx v1.0.8 h1:FIQ0OPtQ7BNEhPutB8JWPU/nD1RJZEyPD++uDb5SZws= 30 | github.com/smartwalle/ngx v1.0.8/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= 31 | github.com/smartwalle/ngx v1.0.9 h1:pUXDvWRZJIHVrCKA1uZ15YwNti+5P4GuJGbpJ4WvpMw= 32 | github.com/smartwalle/ngx v1.0.9/go.mod h1:mx/nz2Pk5j+RBs7t6u6k22MPiBG/8CtOMpCnALIG8Y0= 33 | github.com/smartwalle/nsync v0.0.4 h1:QEiw/THw0oiyxMVvUhcoTKPN6r4v3oBVqiYQoHIMAy0= 34 | github.com/smartwalle/nsync v0.0.4/go.mod h1:nMMP9E4eXlpDrSW8amkScDfrwyQt7VKpG2PJzeS6U+I= 35 | github.com/smartwalle/nsync v0.0.6 h1:rESfGKm3rzWIrmj3uGaNOX0qDZ8x1V/zOCf/rfs8FMY= 36 | github.com/smartwalle/nsync v0.0.6/go.mod h1:nMMP9E4eXlpDrSW8amkScDfrwyQt7VKpG2PJzeS6U+I= 37 | github.com/smartwalle/nsync v0.0.7 h1:yyTe3T4zZ/S+H8ojtrtanS/ur5gMyfV0SbZ4KRdm8yI= 38 | github.com/smartwalle/nsync v0.0.7/go.mod h1:nMMP9E4eXlpDrSW8amkScDfrwyQt7VKpG2PJzeS6U+I= 39 | github.com/smartwalle/queue v0.0.3 h1:6QpTliu/gS2vdk/vXXDU8WEcC0/d/M/Ft+xXxkv9kRw= 40 | github.com/smartwalle/queue v0.0.3/go.mod h1:zMEHt/7zLtVwATDUQi3OdBUlbQ2XUzWd7OXP8p/684c= 41 | github.com/smartwalle/queue v0.0.4 h1:h2YfM/I1yjPOQjMkhPOVuN5cSk62fRPqzqhlyZ/5zDA= 42 | github.com/smartwalle/queue v0.0.4/go.mod h1:zMEHt/7zLtVwATDUQi3OdBUlbQ2XUzWd7OXP8p/684c= 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 苹果支付 2 | 3 | ## 鸣谢 4 | 5 | [![jetbrains.svg](jetbrains.svg)](https://www.jetbrains.com/?from=AliPay%20SDK%20for%20Go) 6 | 7 | ## 安装 8 | 9 | ```go 10 | go get github.com/smartwalle/apple 11 | ``` 12 | 13 | ```go 14 | import github.com/smartwalle/apple 15 | ``` 16 | 17 | ## 帮助 18 | 19 | 在集成的过程中有遇到问题,欢迎加 QQ 群 203357977 讨论。 20 | 21 | ## 其它支付 22 | 23 | 支付宝 [https://github.com/smartwalle/alipay](https://github.com/smartwalle/alipay) 24 | 25 | PayPal [https://github.com/smartwalle/paypal](https://github.com/smartwalle/paypal) 26 | 27 | 银联支付 [https://github.com/smartwalle/unionpay](https://github.com/smartwalle/unionpay) 28 | 29 | ## 苹果内购验证(旧) 30 | 31 | ```go 32 | var summary, info, err = apple.VerifyReceipt(transactionId, receipt) 33 | ``` 34 | 35 | 苹果内购验证支持**生产环境**和**沙箱环境**,**VerifyReceipt()** 函数内部会优先向苹果生产环境进行验证,然后根据获取到的数据判断是否要向沙箱环境进行验证。 36 | 37 | 可以从 **VerifyReceipt()** 函数返回的数据中判断该支付所属的环境信息。 38 | 39 | ## 苹果内购验证(新) 40 | 41 | 根据苹果[官方文档](https://developer.apple.com/documentation/appstorereceipts/verifyreceipt)所示,原 verifyReceipt 接口已经标记为 Deprecated,新的接口已经整合到 [App Store Server API](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info)。 42 | 43 | 新的验证需要参考本文档[其它接口](https://github.com/smartwalle/apple#%E5%85%B6%E5%AE%83%E6%8E%A5%E5%8F%A3)部分对 apple.Client 进行实例化,然后调用其 GetTransaction() 方法进行查询。 44 | 45 | ## 苹果登录数据解析 46 | 47 | ```go 48 | var client = apple.NewAuthClient() 49 | var user, err = client.DecodeToken("从客户端获取到的 IdentityToken") 50 | ``` 51 | 52 | ## 苹果登录数据验证 53 | 54 | 如果要验证 Token 的合法性,在初始化 IdentityClient 的时候,需要设置 BundleId。 55 | 56 | ```go 57 | var client = apple.NewAuthClient(apple.WithBundleId("bundle id")) 58 | var user, err = client.VerifyToken("从客户端获取到的 IdentityToken") 59 | ``` 60 | 61 | ## 通知数据解析 62 | 63 | ```go 64 | var notification, err = apple.DecodeNotification([]byte(data)) 65 | ``` 66 | 67 | 业务服务器提供一个请求方法为 **POST** 的 HTTP 接口给苹果,苹果会在需要的时候推送一些通知消息到该接口。 68 | 69 | ```go 70 | var s = gin.Default() 71 | s.POST("/apple", apple) 72 | 73 | func apple(c *gin.Context) { 74 | var data, _ = io.ReadAll(c.Request.Body) 75 | var notification, err = apple.DecodeNotification([]byte(data)) 76 | // 关于这里如何返回数据参考 https://developer.apple.com/documentation/appstoreservernotifications/responding_to_app_store_server_notifications 77 | // 简单来讲,返回 HTTP Status Code 200 表示我们成功处理该通知 78 | // 如:c.Status(http.StatusOK) 79 | 80 | // 返回 HTTP Status Code 50x 或者 40x 表示我们没有成功处理该通知,苹果会在一定时间后重新推送该通知 81 | // 如:c.Status(http.StatusBadRequest) 82 | } 83 | 84 | ``` 85 | 86 | ## 其它接口 87 | 88 | * **[Get Transaction Info](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_info)** 89 | * **[Look Up Order ID](https://developer.apple.com/documentation/appstoreserverapi/look_up_order_id)** 90 | * **[Get Refund History](https://developer.apple.com/documentation/appstoreserverapi/get_refund_history)** 91 | * **[Get All Subscription Statuses](https://developer.apple.com/documentation/appstoreserverapi/get_all_subscription_statuses)** 92 | * **[Extend a Subscription Renewal Date](https://developer.apple.com/documentation/appstoreserverapi/extend_a_subscription_renewal_date)** 93 | * **[Get Transaction History](https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history)** 94 | * **[Send Consumption Information](https://developer.apple.com/documentation/appstoreserverapi/send_consumption_information)** 95 | 96 | 以上接口需要先初始化 apple.Client 97 | 98 | ```go 99 | var client, _ = apple.New(keyfile, keyId, issuer, bundleId, isProduction) 100 | ``` 101 | 102 | #### 关于 keyfile, keyId, issuer 如何获取? 103 | 104 | [Creating API Keys to Use With the App Store Server API 105 | ](https://developer.apple.com/documentation/appstoreserverapi/creating_api_keys_to_use_with_the_app_store_server_api) 106 | 107 | ## License 108 | 109 | This project is licensed under the MIT License. 110 | -------------------------------------------------------------------------------- /transaction_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/url" 7 | ) 8 | 9 | // TransactionResponse https://developer.apple.com/documentation/appstoreserverapi/transactioninforesponse 10 | type TransactionResponse struct { 11 | *Transaction `json:"transaction"` 12 | } 13 | 14 | type TransactionResponseAlias TransactionResponse 15 | 16 | func (t *TransactionResponse) UnmarshalJSON(data []byte) (err error) { 17 | var aux = struct { 18 | *TransactionResponseAlias 19 | SignedTransactions SignedTransaction `json:"signedTransactionInfo"` 20 | }{ 21 | TransactionResponseAlias: (*TransactionResponseAlias)(t), 22 | } 23 | 24 | if err = json.Unmarshal(data, &aux); err != nil { 25 | return err 26 | } 27 | 28 | var transaction *Transaction 29 | transaction, err = aux.SignedTransactions.Decode() 30 | if err != nil { 31 | return err 32 | } 33 | t.Transaction = transaction 34 | return nil 35 | } 36 | 37 | // TransactionHistoryParam https://developer.apple.com/documentation/appstoreserverapi/get_transaction_history#query-parameters 38 | type TransactionHistoryParam struct { 39 | Revision string 40 | StartDate string 41 | EndDate string 42 | ProductId string 43 | ProductType string 44 | Sort string 45 | SubscriptionGroupIdentifier string 46 | InAppOwnershipType string 47 | Revoked bool 48 | } 49 | 50 | func (t TransactionHistoryParam) Values() url.Values { 51 | var values = url.Values{} 52 | if t.Revision != "" { 53 | values.Set("revision", t.Revision) 54 | } 55 | if t.StartDate != "" { 56 | values.Set("startDate", t.StartDate) 57 | } 58 | if t.EndDate != "" { 59 | values.Set("endDate", t.EndDate) 60 | } 61 | if t.ProductId != "" { 62 | values.Set("productId", t.ProductId) 63 | } 64 | if t.ProductType != "" { 65 | values.Set("productType", t.ProductType) 66 | } 67 | if t.Sort != "" { 68 | values.Set("sort", t.Sort) 69 | } 70 | if t.SubscriptionGroupIdentifier != "" { 71 | values.Set("subscriptionGroupIdentifier", t.SubscriptionGroupIdentifier) 72 | } 73 | if t.InAppOwnershipType != "" { 74 | values.Set("inAppOwnershipType", t.InAppOwnershipType) 75 | } 76 | values.Set("revoked", fmt.Sprintf("%v", t.Revoked)) 77 | return values 78 | } 79 | 80 | // TransactionHistoryResponse https://developer.apple.com/documentation/appstoreserverapi/historyresponse 81 | type TransactionHistoryResponse struct { 82 | AppAppleId int `json:"appAppleId"` 83 | BundleId string `json:"bundleId"` 84 | Environment Environment `json:"environment"` 85 | HasMore bool `json:"hasMore"` 86 | Revision string `json:"revision"` 87 | Transactions []*Transaction `json:"transactions"` 88 | } 89 | 90 | type TransactionHistoryResponseAlias TransactionHistoryResponse 91 | 92 | func (t *TransactionHistoryResponse) UnmarshalJSON(data []byte) (err error) { 93 | var aux = struct { 94 | *TransactionHistoryResponseAlias 95 | SignedTransactions []SignedTransaction `json:"signedTransactions"` 96 | }{ 97 | TransactionHistoryResponseAlias: (*TransactionHistoryResponseAlias)(t), 98 | } 99 | 100 | if err = json.Unmarshal(data, &aux); err != nil { 101 | return err 102 | } 103 | 104 | for _, item := range aux.SignedTransactions { 105 | var transaction *Transaction 106 | transaction, err = item.Decode() 107 | if err != nil { 108 | return err 109 | } 110 | t.Transactions = append(t.Transactions, transaction) 111 | } 112 | return nil 113 | } 114 | 115 | // ConsumptionParam https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest 116 | type ConsumptionParam struct { 117 | AccountTenure int `json:"accountTenure"` 118 | AppAccountToken string `json:"appAccountToken"` 119 | ConsumptionStatus int `json:"consumptionStatus"` 120 | CustomerConsented bool `json:"customerConsented"` 121 | DeliveryStatus int `json:"deliveryStatus"` 122 | LifetimeDollarsPurchased int `json:"lifetimeDollarsPurchased"` 123 | LifetimeDollarsRefunded int `json:"lifetimeDollarsRefunded"` 124 | Platform int `json:"platform"` 125 | PlayTime int `json:"playTime"` 126 | SampleContentProvided bool `json:"sampleContentProvided"` 127 | UserStatus int `json:"userStatus"` 128 | } 129 | -------------------------------------------------------------------------------- /auth.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "context" 5 | "crypto/rsa" 6 | "encoding/base64" 7 | "encoding/json" 8 | "errors" 9 | "github.com/golang-jwt/jwt/v5" 10 | "github.com/smartwalle/apple/internal/auth" 11 | "github.com/smartwalle/dbc" 12 | "github.com/smartwalle/ngx" 13 | "github.com/smartwalle/nsync/singleflight" 14 | "net/http" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | const ( 20 | kFetchAuthKeys = "https://appleid.apple.com/auth/keys" 21 | kIssuer = "https://appleid.apple.com" 22 | ) 23 | 24 | var ( 25 | ErrInvalidToken = errors.New("invalid token") 26 | ErrInvalidIssuer = errors.New("invalid issuer") 27 | ErrInvalidBundleId = errors.New("invalid bundle id") 28 | ErrTokenExpired = errors.New("token is expired") 29 | ) 30 | 31 | type AuthClientOption func(c *AuthClient) 32 | 33 | // WithKeyExpiration 用于设置从 https://appleid.apple.com/auth/keys 获取的公钥在本地的缓存时间,单位为秒 34 | func WithKeyExpiration(expiration int64) AuthClientOption { 35 | return func(c *AuthClient) { 36 | c.expiration = expiration 37 | } 38 | } 39 | 40 | // WithBundleId 用于设置 VerifyToken() 方法需要的 BundleId 信息 41 | func WithBundleId(bundleId string) AuthClientOption { 42 | return func(c *AuthClient) { 43 | c.bundleId = bundleId 44 | } 45 | } 46 | 47 | // AuthClient 苹果登录验证 48 | type AuthClient struct { 49 | Client *http.Client 50 | keys dbc.Cache[string, *rsa.PublicKey] 51 | group singleflight.Group[string, interface{}] 52 | expiration int64 53 | bundleId string 54 | } 55 | 56 | func NewAuthClient(opts ...AuthClientOption) *AuthClient { 57 | var nClient = &AuthClient{} 58 | nClient.Client = http.DefaultClient 59 | nClient.keys = dbc.New[*rsa.PublicKey]() 60 | nClient.group = singleflight.New[interface{}]() 61 | for _, opt := range opts { 62 | if opt != nil { 63 | opt(nClient) 64 | } 65 | } 66 | if nClient.expiration <= 0 { 67 | nClient.expiration = 300 // 默认 300 秒 68 | } 69 | return nClient 70 | } 71 | 72 | // DecodeToken 解析 Token 73 | // 74 | // 只对 Token 进行解析,不会验证合法性 75 | func (c *AuthClient) DecodeToken(token string) (*User, error) { 76 | var payloads = strings.Split(token, ".") 77 | if len(payloads) < 3 { 78 | return nil, ErrInvalidToken 79 | } 80 | 81 | headerBytes, err := base64.RawStdEncoding.DecodeString(payloads[0]) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | var header *auth.Header 87 | if err = json.Unmarshal(headerBytes, &header); err != nil { 88 | return nil, err 89 | } 90 | 91 | var key = c.GetAuthKey(header.Kid) 92 | if key == nil { 93 | return nil, ErrInvalidToken 94 | } 95 | 96 | var claims = &auth.Claims{} 97 | jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { 98 | return key, nil 99 | }) 100 | 101 | var user = &User{} 102 | user.Id = claims.Subject 103 | user.Issuer = claims.Issuer 104 | user.BundleId = strings.Join(claims.Audience, ";") 105 | user.Email = claims.Email 106 | user.EmailVerified = bool(claims.EmailVerified) 107 | user.IsPrivateEmail = bool(claims.IsPrivateEmail) 108 | user.RealUserStatus = claims.RealUserStatus 109 | user.TransferSub = claims.TransferSub 110 | user.Nonce = claims.Nonce 111 | user.AuthTime = int64(claims.AuthTime) 112 | user.IssuedAt = claims.IssuedAt.Unix() 113 | user.ExpiresAt = claims.ExpiresAt.Unix() 114 | return user, nil 115 | } 116 | 117 | // VerifyToken 解析并验证 Token https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/verifying_a_user#3383769 118 | // 119 | // 会对 Token 的合法性进行验证,主要判断 BundleId 和 Issuer 是否正确以及 Token 是否在有效期内 120 | func (c *AuthClient) VerifyToken(token string) (*User, error) { 121 | var user, err = c.DecodeToken(token) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | if user.BundleId != c.bundleId { 127 | return nil, ErrInvalidBundleId 128 | } 129 | 130 | if user.Issuer != kIssuer { 131 | return nil, ErrInvalidIssuer 132 | } 133 | 134 | if user.ExpiresAt <= time.Now().Unix() { 135 | return nil, ErrTokenExpired 136 | } 137 | 138 | return user, nil 139 | } 140 | 141 | func (c *AuthClient) GetAuthKey(kid string) *rsa.PublicKey { 142 | // 从本地缓存中查询 key 信息,存在则直接返回 143 | if key, _ := c.keys.Get(kid); key != nil { 144 | return key 145 | } 146 | 147 | c.group.Do(kFetchAuthKeys, func(_ string) (interface{}, error) { 148 | // 从苹果服务器请求 key 数据 149 | var nKeys, _ = c.requestAuthKeys() 150 | 151 | for _, key := range nKeys { 152 | var nKey, _ = auth.DecodePublicKey(key.N, key.E) 153 | if nKey != nil { 154 | c.keys.SetEx(key.Kid, nKey, c.expiration) 155 | } 156 | } 157 | return nil, nil 158 | }) 159 | 160 | key, _ := c.keys.Get(kid) 161 | return key 162 | } 163 | 164 | // requestAuthKeys https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature 165 | func (c *AuthClient) requestAuthKeys() ([]auth.Key, error) { 166 | var req = ngx.NewRequest(http.MethodGet, kFetchAuthKeys, ngx.WithClient(c.Client)) 167 | 168 | var rsp, err = req.Do(context.Background()) 169 | if err != nil { 170 | return nil, err 171 | } 172 | defer rsp.Body.Close() 173 | 174 | var aux = struct { 175 | Keys []auth.Key `json:"keys"` 176 | }{} 177 | if err = json.NewDecoder(rsp.Body).Decode(&aux); err != nil { 178 | return nil, err 179 | } 180 | 181 | return aux.Keys, nil 182 | } 183 | -------------------------------------------------------------------------------- /refund_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/smartwalle/apple" 6 | "testing" 7 | ) 8 | 9 | func TestClient_RefundLookupResponse(t *testing.T) { 10 | var data = []byte(`{"signedTransactions":["eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDA1Mzc3NzkzMSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDAwNTI1NzU2MDciLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMDA0MDI1Njk4IiwiYnVuZGxlSWQiOiJjb20ucHJvamVjdDVlLm15bG9ncy5tZWlqaS5kZWJ1ZyIsInByb2R1Y3RJZCI6ImNvbS5wcm9qZWN0NWUubWVpamkubXlsb2dzLk1vbnRobHkuUHJlbWl1bS50ZXN0Iiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjA5MjAwNDUiLCJwdXJjaGFzZURhdGUiOjE2NTI0MzIxODEwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNjUyMzQzNDgyMDAwLCJleHBpcmVzRGF0ZSI6MTY1MjQzMjM2MTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJhcHBBY2NvdW50VG9rZW4iOiJjNDRkM2EyNC0zN2U5LTQxM2UtYWY1Yy1hNzNhNTNkOGY2OTciLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNjUyNDMyMTQ0NTYwLCJlbnZpcm9ubWVudCI6IlNhbmRib3gifQ.WlDvZlNCU4prMRi97mFTKz7p8jJTGSUCGjcs_RUS_LkBZDfmxszg_S1pzdMsWlvsStXAOboEgHuQbbHzLb-NNQ"]}`) 11 | var nRsp *apple.RefundLookupResponse 12 | if err := json.Unmarshal(data, &nRsp); err != nil { 13 | t.Fatal(err) 14 | } 15 | t.Log(nRsp.Transactions[0]) 16 | } 17 | -------------------------------------------------------------------------------- /order_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/smartwalle/apple" 6 | "testing" 7 | ) 8 | 9 | func TestClient_OrderLookupResponse(t *testing.T) { 10 | var data = []byte(`{"status":0,"signedTransactions":["eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDA1Mzc3NzkzMSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDAwNTI1NzU2MDciLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMDA0MDI1Njk4IiwiYnVuZGxlSWQiOiJjb20ucHJvamVjdDVlLm15bG9ncy5tZWlqaS5kZWJ1ZyIsInByb2R1Y3RJZCI6ImNvbS5wcm9qZWN0NWUubWVpamkubXlsb2dzLk1vbnRobHkuUHJlbWl1bS50ZXN0Iiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjA5MjAwNDUiLCJwdXJjaGFzZURhdGUiOjE2NTI0MzIxODEwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNjUyMzQzNDgyMDAwLCJleHBpcmVzRGF0ZSI6MTY1MjQzMjM2MTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJhcHBBY2NvdW50VG9rZW4iOiJjNDRkM2EyNC0zN2U5LTQxM2UtYWY1Yy1hNzNhNTNkOGY2OTciLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNjUyNDMyMTQ0NTYwLCJlbnZpcm9ubWVudCI6IlNhbmRib3gifQ.WlDvZlNCU4prMRi97mFTKz7p8jJTGSUCGjcs_RUS_LkBZDfmxszg_S1pzdMsWlvsStXAOboEgHuQbbHzLb-NNQ"]}`) 11 | var nRsp *apple.OrderLookupResponse 12 | if err := json.Unmarshal(data, &nRsp); err != nil { 13 | t.Fatal(err) 14 | } 15 | t.Log(nRsp.Transactions[0]) 16 | } 17 | -------------------------------------------------------------------------------- /apple_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang-jwt/jwt/v5" 6 | "github.com/smartwalle/apple/internal/storekit" 7 | "net/url" 8 | ) 9 | 10 | type Environment string 11 | 12 | const ( 13 | EnvironmentSandbox Environment = "Sandbox" 14 | EnvironmentProduction Environment = "Production" 15 | ) 16 | 17 | type Param interface { 18 | Values() url.Values 19 | } 20 | 21 | type Error struct { 22 | Code int `json:"errorCode"` 23 | Message string `json:"errorMessage"` 24 | } 25 | 26 | func (e Error) Error() string { 27 | return fmt.Sprintf("%d - %s", e.Code, e.Message) 28 | } 29 | 30 | type SignedTransaction string 31 | 32 | func (s SignedTransaction) Decode() (*Transaction, error) { 33 | if s == "" { 34 | return nil, nil 35 | } 36 | var item = &Transaction{} 37 | if err := storekit.DecodeClaims(string(s), item); err != nil { 38 | return nil, err 39 | } 40 | return item, nil 41 | } 42 | 43 | type InAppOwnershipType string 44 | 45 | const ( 46 | InAppOwnershipTypeFamilyShared InAppOwnershipType = "FAMILY_SHARED" 47 | InAppOwnershipTypePUrchased InAppOwnershipType = "PURCHASED" 48 | ) 49 | 50 | type OfferType int 51 | 52 | const ( 53 | OfferTypeIntroductory OfferType = 1 54 | OfferTypePromotional OfferType = 2 55 | OfferTypeSubscription OfferType = 3 56 | ) 57 | 58 | type TransactionType string 59 | 60 | const ( 61 | TransactionTypeAutoRenewable TransactionType = "Auto-Renewable Subscription" 62 | TransactionTypeNonConsumable TransactionType = "Non-Consumable" 63 | TransactionTypeConsumable TransactionType = "Consumable" 64 | TransactionTypeNonRenewing TransactionType = "Non-Renewing Subscription" 65 | ) 66 | 67 | // Transaction 68 | // https://developer.apple.com/documentation/appstoreserverapi/jwstransactiondecodedpayload 69 | // https://developer.apple.com/documentation/appstoreservernotifications/jwstransactiondecodedpayload 70 | type Transaction struct { 71 | jwt.RegisteredClaims 72 | AppAccountToken string `json:"appAccountToken"` 73 | TransactionId string `json:"transactionId"` 74 | OriginalTransactionId string `json:"originalTransactionId"` 75 | WebOrderLineItemId string `json:"webOrderLineItemId"` 76 | BundleId string `json:"bundleId"` 77 | ProductId string `json:"productId"` 78 | SubscriptionGroupIdentifier string `json:"subscriptionGroupIdentifier"` 79 | PurchaseDate int64 `json:"purchaseDate"` 80 | OriginalPurchaseDate int64 `json:"originalPurchaseDate"` 81 | ExpiresDate int64 `json:"expiresDate"` 82 | Quantity int `json:"quantity"` 83 | Type TransactionType `json:"type"` 84 | InAppOwnershipType InAppOwnershipType `json:"inAppOwnershipType"` 85 | SignedDate int64 `json:"signedDate"` 86 | OfferIdentifier string `json:"offerIdentifier"` 87 | OfferType OfferType `json:"offerType"` 88 | Environment Environment `json:"environment"` 89 | RevocationReason int `json:"revocationReason"` 90 | RevocationDate int64 `json:"revocationDate"` 91 | IsUpgraded bool `json:"isUpgraded"` 92 | Storefront string `json:"storefront"` 93 | StorefrontId string `json:"storefrontId"` 94 | TransactionReason string `json:"transactionReason"` 95 | } 96 | 97 | type SignedRenewal string 98 | 99 | func (s SignedRenewal) Decode() (*Renewal, error) { 100 | if s == "" { 101 | return nil, nil 102 | } 103 | var item = &Renewal{} 104 | if err := storekit.DecodeClaims(string(s), item); err != nil { 105 | return nil, err 106 | } 107 | return item, nil 108 | } 109 | 110 | type AutoRenewStatus int 111 | 112 | const ( 113 | AutoRenewStatusOff AutoRenewStatus = 0 114 | AutoRenewStatusOn AutoRenewStatus = 1 115 | ) 116 | 117 | // Renewal 118 | // https://developer.apple.com/documentation/appstoreserverapi/jwsrenewalinfodecodedpayload 119 | // https://developer.apple.com/documentation/appstoreservernotifications/jwsrenewalinfodecodedpayload 120 | type Renewal struct { 121 | jwt.RegisteredClaims 122 | AutoRenewProductId string `json:"autoRenewProductId"` 123 | AutoRenewStatus AutoRenewStatus `json:"autoRenewStatus"` 124 | Environment Environment `json:"environment"` 125 | ExpirationIntent int `json:"expirationIntent"` 126 | GracePeriodExpiresDate int64 `json:"gracePeriodExpiresDate"` 127 | IsInBillingRetryPeriod bool `json:"isInBillingRetryPeriod"` 128 | OfferIdentifier string `json:"offerIdentifier"` 129 | OfferType OfferType `json:"offerType"` 130 | OriginalTransactionId string `json:"originalTransactionId"` 131 | PriceIncreaseStatus int `json:"priceIncreaseStatus"` 132 | ProductId string `json:"productId"` 133 | RecentSubscriptionStartDate int64 `json:"recentSubscriptionStartDate"` 134 | RenewalDate int64 `json:"renewalDate"` 135 | SignedDate int64 `json:"signedDate"` 136 | } 137 | -------------------------------------------------------------------------------- /transaction_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/smartwalle/apple" 6 | "testing" 7 | ) 8 | 9 | func TestClient_TransactionHistoryResponse(t *testing.T) { 10 | var data = []byte(`{"appAppleId":0,"bundleId":"test.bundle","environment":"Sandbox","hasMore":false,"revision":"","signedTransactions":["eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDA1Mzc3NzkzMSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDAwNTI1NzU2MDciLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMDA0MDI1Njk4IiwiYnVuZGxlSWQiOiJjb20ucHJvamVjdDVlLm15bG9ncy5tZWlqaS5kZWJ1ZyIsInByb2R1Y3RJZCI6ImNvbS5wcm9qZWN0NWUubWVpamkubXlsb2dzLk1vbnRobHkuUHJlbWl1bS50ZXN0Iiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjA5MjAwNDUiLCJwdXJjaGFzZURhdGUiOjE2NTI0MzIxODEwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNjUyMzQzNDgyMDAwLCJleHBpcmVzRGF0ZSI6MTY1MjQzMjM2MTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJhcHBBY2NvdW50VG9rZW4iOiJjNDRkM2EyNC0zN2U5LTQxM2UtYWY1Yy1hNzNhNTNkOGY2OTciLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNjUyNDMyMTQ0NTYwLCJlbnZpcm9ubWVudCI6IlNhbmRib3gifQ.WlDvZlNCU4prMRi97mFTKz7p8jJTGSUCGjcs_RUS_LkBZDfmxszg_S1pzdMsWlvsStXAOboEgHuQbbHzLb-NNQ"]}`) 11 | var nRsp *apple.TransactionHistoryResponse 12 | if err := json.Unmarshal(data, &nRsp); err != nil { 13 | t.Fatal(err) 14 | } 15 | t.Log(nRsp.Transactions[0]) 16 | } 17 | -------------------------------------------------------------------------------- /notification_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/golang-jwt/jwt/v5" 6 | ) 7 | 8 | // TestNotificationResponse https://developer.apple.com/documentation/appstoreserverapi/sendtestnotificationresponse 9 | type TestNotificationResponse struct { 10 | TestNotificationToken string `json:"testNotificationToken"` 11 | } 12 | 13 | // NotificationType https://developer.apple.com/documentation/appstoreservernotifications/notificationtype 14 | type NotificationType string 15 | 16 | const ( 17 | NotificationTypeConsumptionRequest NotificationType = "CONSUMPTION_REQUEST" 18 | NotificationTypeDidChangeRenewalPref NotificationType = "DID_CHANGE_RENEWAL_PREF" 19 | NotificationTypeDidChangeRenewalStatus NotificationType = "DID_CHANGE_RENEWAL_STATUS" 20 | NotificationTypeDidFailToRenew NotificationType = "DID_FAIL_TO_RENEW" 21 | NotificationTypeDidRenew NotificationType = "DID_RENEW" 22 | NotificationTypeExpired NotificationType = "EXPIRED" 23 | NotificationTypeGracePeriodExpired NotificationType = "GRACE_PERIOD_EXPIRED" 24 | NotificationTypeOfferRedeemed NotificationType = "OFFER_REDEEMED" 25 | NotificationTypePriceIncrease NotificationType = "PRICE_INCREASE" 26 | NotificationTypeRefund NotificationType = "REFUND" 27 | NotificationTypeRefundDeclined NotificationType = "REFUND_DECLINED" 28 | NotificationTypeRefundReversed NotificationType = "REFUND_REVERSED" 29 | NotificationTypeRenewalExtended NotificationType = "RENEWAL_EXTENDED" 30 | NotificationTypeRenewalExtension NotificationType = "RENEWAL_EXTENSION" 31 | NotificationTypeRevoke NotificationType = "REVOKE" 32 | NotificationTypeSubscribed NotificationType = "SUBSCRIBED" 33 | NotificationTypeTest NotificationType = "TEST" 34 | ) 35 | 36 | // NotificationSubType https://developer.apple.com/documentation/appstoreservernotifications/subtype 37 | type NotificationSubType string 38 | 39 | const ( 40 | NotificationSubTypeAccept NotificationSubType = "ACCEPTED" 41 | NotificationSubTypeAutoRenewDisabled NotificationSubType = "AUTO_RENEW_DISABLED" 42 | NotificationSubTypeAutoRenewEnabled NotificationSubType = "AUTO_RENEW_ENABLED" 43 | NotificationSubTypeBillingRecovery NotificationSubType = "BILLING_RECOVERY" 44 | NotificationSubTypeBillingRetry NotificationSubType = "BILLING_RETRY" 45 | NotificationSubTypeDowngrade NotificationSubType = "DOWNGRADE" 46 | NotificationSubTypeFailure NotificationSubType = "FAILURE" 47 | NotificationSubTypeGracePeriod NotificationSubType = "GRACE_PERIOD" 48 | NotificationSubTypeInitialBuy NotificationSubType = "INITIAL_BUY" 49 | NotificationSubTypePending NotificationSubType = "PENDING" 50 | NotificationSubTypePriceIncrease NotificationSubType = "PRICE_INCREASE" 51 | NotificationSubTypeProductNotForSale NotificationSubType = "PRODUCT_NOT_FOR_SALE" 52 | NotificationSubTypeResubscribe NotificationSubType = "RESUBSCRIBE" 53 | NotificationSubTypeSummary NotificationSubType = "SUMMARY" 54 | NotificationSubTypeUpgrade NotificationSubType = "UPGRADE" 55 | NotificationSubTypeVoluntary NotificationSubType = "VOLUNTARY" 56 | ) 57 | 58 | // Notification https://developer.apple.com/documentation/appstoreservernotifications/responsebodyv2decodedpayload 59 | type Notification struct { 60 | jwt.RegisteredClaims 61 | NotificationType NotificationType `json:"notificationType"` 62 | Subtype NotificationSubType `json:"subtype"` 63 | Data *NotificationData `json:"data"` 64 | Summary *NotificationSummary `json:"summary"` 65 | Version string `json:"version"` 66 | SignedDate int64 `json:"signedDate"` 67 | NotificationUUID string `json:"notificationUUID"` 68 | } 69 | 70 | type NotificationData struct { 71 | AppAppleId int64 `json:"appAppleId"` 72 | BundleId string `json:"bundleId"` 73 | BundleVersion string `json:"bundleVersion"` 74 | Environment Environment `json:"environment"` 75 | Status int `json:"status"` // https://developer.apple.com/documentation/appstoreservernotifications/status 76 | Renewal *Renewal `json:"renewal"` 77 | Transaction *Transaction `json:"transaction"` 78 | } 79 | 80 | type NotificationDataAlias NotificationData 81 | 82 | func (n *NotificationData) UnmarshalJSON(data []byte) (err error) { 83 | var aux = struct { 84 | *NotificationDataAlias 85 | SignedRenewal SignedRenewal `json:"signedRenewalInfo"` 86 | SignedTransaction SignedTransaction `json:"signedTransactionInfo"` 87 | }{ 88 | NotificationDataAlias: (*NotificationDataAlias)(n), 89 | } 90 | 91 | if err = json.Unmarshal(data, &aux); err != nil { 92 | return err 93 | } 94 | 95 | if n.Renewal, err = aux.SignedRenewal.Decode(); err != nil { 96 | return err 97 | } 98 | if n.Transaction, err = aux.SignedTransaction.Decode(); err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | type NotificationSummary struct { 105 | RequestIdentifier string `json:"requestIdentifier"` 106 | Environment Environment `json:"environment"` 107 | AppAppleId int64 `json:"appAppleId"` 108 | BundleId string `json:"bundleId"` 109 | ProductId string `json:"productId"` 110 | StorefrontCountryCodes []string `json:"storefrontCountryCodes"` 111 | FailedCount int64 `json:"failedCount"` 112 | SucceededCount int64 `json:"succeededCount"` 113 | } 114 | -------------------------------------------------------------------------------- /subscription_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/smartwalle/apple" 6 | "testing" 7 | ) 8 | 9 | func TestClient_SubscriptionsStatusResponse(t *testing.T) { 10 | var data = []byte(`{"data":[{"subscriptionGroupIdentifier":"","lastTransactions":[{"originalTransactionId":"","status":0,"signedRenewalInfo":"","signedTransactionInfo":"eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDA1Mzc3NzkzMSIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDAwNTI1NzU2MDciLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMDA0MDI1Njk4IiwiYnVuZGxlSWQiOiJjb20ucHJvamVjdDVlLm15bG9ncy5tZWlqaS5kZWJ1ZyIsInByb2R1Y3RJZCI6ImNvbS5wcm9qZWN0NWUubWVpamkubXlsb2dzLk1vbnRobHkuUHJlbWl1bS50ZXN0Iiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjA5MjAwNDUiLCJwdXJjaGFzZURhdGUiOjE2NTI0MzIxODEwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNjUyMzQzNDgyMDAwLCJleHBpcmVzRGF0ZSI6MTY1MjQzMjM2MTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJhcHBBY2NvdW50VG9rZW4iOiJjNDRkM2EyNC0zN2U5LTQxM2UtYWY1Yy1hNzNhNTNkOGY2OTciLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNjUyNDMyMTQ0NTYwLCJlbnZpcm9ubWVudCI6IlNhbmRib3gifQ.WlDvZlNCU4prMRi97mFTKz7p8jJTGSUCGjcs_RUS_LkBZDfmxszg_S1pzdMsWlvsStXAOboEgHuQbbHzLb-NNQ"}]}],"environment":"Production","appAppleId":1261810679,"bundleId":"test.bundle"}`) 11 | var nRsp *apple.SubscriptionsStatusResponse 12 | if err := json.Unmarshal(data, &nRsp); err != nil { 13 | t.Fatal(err) 14 | } 15 | 16 | t.Log(nRsp.Data[0].LastTransactions[0].Transaction) 17 | } 18 | -------------------------------------------------------------------------------- /receipt_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "github.com/smartwalle/apple" 5 | "testing" 6 | ) 7 | 8 | func TestVerifyReceipt(t *testing.T) { 9 | var summary, info, err = apple.VerifyReceipt("460001400577115", "MIIUTAYJKoZIhvcNAQcCoIIUPTCCFDkCAQExCzAJBgUrDgMCGgUAMIIDigYJKoZIhvcNAQcBoIIDewSCA3cxggNzMAoCARQCAQEEAgwAMAsCARkCAQEEAwIBAzAMAgEOAgEBBAQCAgD9MA0CAQMCAQEEBQwDMzI1MA0CAQoCAQEEBRYDMTcrMA0CAQsCAQEEBQIDGcsNMA0CAQ0CAQEEBQIDAkuBMA0CARMCAQEEBQwDMzI1MA4CAQECAQEEBgIESzWz9zAOAgEJAgEBBAYCBFAyNTgwDgIBEAIBAQQGAgQyqyjxMBICAQ8CAQEECgIIBvf+gIVyuBwwFAIBAAIBAQQMDApQcm9kdWN0aW9uMBgCAQQCAQIEEDXZIuXFt+8k8Z943qzVWwIwHAIBBQIBAQQUOoUJrZTGXssALA9S5s98RcdNZh4wHgIBCAIBAQQWFhQyMDIzLTAyLTAzVDEzOjA4OjQ1WjAeAgEMAgEBBBYWFDIwMjMtMDItMDNUMTM6MDg6NDVaMB4CARICAQEEFhYUMjAyMy0wMi0wM1QxMjoyMDoxMFowIQIBAgIBAQQZDBdjb20ueW91bGUyMDExLmh5bWFqaWFuZzAxAgEHAgEBBCmqUy0hnS30XTJGa67oy4s2ayyWDIm/eC7rnbh5j5Pn63ZFCRE5ZqSsDjBVAgEGAgEBBE1uPHM1NbMJ34vHNpidCRV0Y8ppytQ9B5PZn57e0Xy7mmR4J89Thwl4WUh3R7kPEiurxX3kJpPegomqYJ3vqluMN7Y8X3fadWbXZYumQDCCAV4CARECAQEEggFUMYIBUDALAgIGrAIBAQQCFgAwCwICBq0CAQEEAgwAMAsCAgawAgEBBAIWADALAgIGsgIBAQQCDAAwCwICBrMCAQEEAgwAMAsCAga0AgEBBAIMADALAgIGtQIBAQQCDAAwCwICBrYCAQEEAgwAMAwCAgalAgEBBAMCAQEwDAICBqsCAQEEAwIBATAMAgIGrwIBAQQDAgEAMAwCAgaxAgEBBAMCAQAwDAICBroCAQEEAwIBADAPAgIGrgIBAQQGAgRV70B8MBUCAgamAgEBBAwMCmRpYW1vbmRfMzAwGgICBqcCAQEEEQwPNDYwMDAxNDAwNTc3MTE1MBoCAgapAgEBBBEMDzQ2MDAwMTQwMDU3NzExNTAfAgIGqAIBAQQWFhQyMDIzLTAyLTAzVDEzOjA4OjQ1WjAfAgIGqgIBAQQWFhQyMDIzLTAyLTAzVDEzOjA4OjQ1WqCCDuIwggXGMIIErqADAgECAhAtqwMbvdZlc9IHKXk8RJfEMA0GCSqGSIb3DQEBBQUAMHUxCzAJBgNVBAYTAlVTMRMwEQYDVQQKDApBcHBsZSBJbmMuMQswCQYDVQQLDAJHNzFEMEIGA1UEAww7QXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjIxMjAyMjE0NjA0WhcNMjMxMTE3MjA0MDUyWjCBiTE3MDUGA1UEAwwuTWFjIEFwcCBTdG9yZSBhbmQgaVR1bmVzIFN0b3JlIFJlY2VpcHQgU2lnbmluZzEsMCoGA1UECwwjQXBwbGUgV29ybGR3aWRlIERldmVsb3BlciBSZWxhdGlvbnMxEzARBgNVBAoMCkFwcGxlIEluYy4xCzAJBgNVBAYTAlVTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwN3GrrTovG3rwX21zphZ9lBYtkLcleMaxfXPZKp/0sxhTNYU43eBxFkxtxnHTUurnSemHD5UclAiHj0wHUoORuXYJikVS+MgnK7V8yVj0JjUcfhulvOOoArFBDXpOPer+DuU2gflWzmF/515QPQaCq6VWZjTHFyKbAV9mh80RcEEzdXJkqVGFwaspIXzd1wfhfejQebbExBvbfAh6qwmpmY9XoIVx1ybKZZNfopOjni7V8k1lHu2AM4YCot1lZvpwxQ+wRA0BG23PDcz380UPmIMwN8vcrvtSr/jyGkNfpZtHU8QN27T/D0aBn1sARTIxF8xalLxMwXIYOPGA80mgQIDAQABo4ICOzCCAjcwDAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRdQhBsG7vHUpdORL0TJ7k6EneDKzBwBggrBgEFBQcBAQRkMGIwLQYIKwYBBQUHMAKGIWh0dHA6Ly9jZXJ0cy5hcHBsZS5jb20vd3dkcmc3LmRlcjAxBggrBgEFBQcwAYYlaHR0cDovL29jc3AuYXBwbGUuY29tL29jc3AwMy13d2RyZzcwMTCCAR8GA1UdIASCARYwggESMIIBDgYKKoZIhvdjZAUGATCB/zA3BggrBgEFBQcCARYraHR0cHM6Ly93d3cuYXBwbGUuY29tL2NlcnRpZmljYXRlYXV0aG9yaXR5LzCBwwYIKwYBBQUHAgIwgbYMgbNSZWxpYW5jZSBvbiB0aGlzIGNlcnRpZmljYXRlIGJ5IGFueSBwYXJ0eSBhc3N1bWVzIGFjY2VwdGFuY2Ugb2YgdGhlIHRoZW4gYXBwbGljYWJsZSBzdGFuZGFyZCB0ZXJtcyBhbmQgY29uZGl0aW9ucyBvZiB1c2UsIGNlcnRpZmljYXRlIHBvbGljeSBhbmQgY2VydGlmaWNhdGlvbiBwcmFjdGljZSBzdGF0ZW1lbnRzLjAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vY3JsLmFwcGxlLmNvbS93d2RyZzcuY3JsMB0GA1UdDgQWBBSyRX3DRIprTEmvblHeF8lRRu/7NDAOBgNVHQ8BAf8EBAMCB4AwEAYKKoZIhvdjZAYLAQQCBQAwDQYJKoZIhvcNAQEFBQADggEBAHeKAt2kspClrJ+HnX5dt7xpBKMa/2Rx09HKJqGLePMVKT5wzOtVcCSbUyIJuKsxLJZ4+IrOFovPKD4SteF6dL9BTFkNb4BWKUaBj+wVlA9Q95m3ln+Fc6eZ7D4mpFTsx77/fiR/xsTmUBXxWRvk94QHKxWUs5bp2J6FXUR0rkXRqO/5pe4dVhlabeorG6IRNA03QBTg6/Gjx3aVZgzbzV8bYn/lKmD2OV2OLS6hxQG5R13RylulVel+o3sQ8wOkgr/JtFWhiFgiBfr9eWthaBD/uNHuXuSszHKEbLMCFSuqOa+wBeZXWw+kKKYppEuHd52jEN9i2HloYOf6TsrIZMswggRVMIIDPaADAgECAhQ0GFj/Af4GP47xnx/pPAG0wUb/yTANBgkqhkiG9w0BAQUFADBiMQswCQYDVQQGEwJVUzETMBEGA1UEChMKQXBwbGUgSW5jLjEmMCQGA1UECxMdQXBwbGUgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxFjAUBgNVBAMTDUFwcGxlIFJvb3QgQ0EwHhcNMjIxMTE3MjA0MDUzWhcNMjMxMTE3MjA0MDUyWjB1MQswCQYDVQQGEwJVUzETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UECwwCRzcxRDBCBgNVBAMMO0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArK7R07aKsRsola3eUVFMPzPhTlyvs/wC0mVPKtR0aIx1F2XPKORICZhxUjIsFk54jpJWZKndi83i1Mc7ohJFNwIZYmQvf2HG01kiv6v5FKPttp6Zui/xsdwwQk+2trLGdKpiVrvtRDYP0eUgdJNXOl2e3AH8eG9pFjXDbgHCnnLUcTaxdgl6vg0ql/GwXgsbEq0rqwffYy31iOkyEqJVWEN2PD0XgB8p27Gpn6uWBZ0V3N3bTg/nE3xaKy4CQfbuemq2c2D3lxkUi5UzOJPaACU2rlVafJ/59GIEB3TpHaeVVyOsKyTaZE8ocumWsAg8iBsUY0PXia6YwfItjuNRJQIDAQABo4HvMIHsMBIGA1UdEwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAUK9BpR5R2Cf70a40uQKb3R01/CF4wRAYIKwYBBQUHAQEEODA2MDQGCCsGAQUFBzABhihodHRwOi8vb2NzcC5hcHBsZS5jb20vb2NzcDAzLWFwcGxlcm9vdGNhMC4GA1UdHwQnMCUwI6AhoB+GHWh0dHA6Ly9jcmwuYXBwbGUuY29tL3Jvb3QuY3JsMB0GA1UdDgQWBBRdQhBsG7vHUpdORL0TJ7k6EneDKzAOBgNVHQ8BAf8EBAMCAQYwEAYKKoZIhvdjZAYCAQQCBQAwDQYJKoZIhvcNAQEFBQADggEBAFKjCCkTZbe1H+Y0A+32GHe8PcontXDs7GwzS/aZJZQHniEzA2r1fQouK98IqYLeSn/h5wtLBbgnmEndwQyG14FkroKcxEXx6o8cIjDjoiVhRIn+hXpW8HKSfAxEVCS3taSfJvAy+VedanlsQO0PNAYGQv/YDjFlbeYuAdkGv8XKDa5H1AUXiDzpnOQZZG2KlK0R3AH25Xivrehw1w1dgT5GKiyuJKHH0uB9vx31NmvF3qkKmoCxEV6yZH6zwVfMwmxZmbf0sN0x2kjWaoHusotQNRbm51xxYm6w8lHiqG34Kstoc8amxBpDSQE+qakAioZsg4jSXHBXetr4dswZ1bAwggS7MIIDo6ADAgECAgECMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpBcHBsZSBJbmMuMSYwJAYDVQQLEx1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEWMBQGA1UEAxMNQXBwbGUgUm9vdCBDQTAeFw0wNjA0MjUyMTQwMzZaFw0zNTAyMDkyMTQwMzZaMGIxCzAJBgNVBAYTAlVTMRMwEQYDVQQKEwpBcHBsZSBJbmMuMSYwJAYDVQQLEx1BcHBsZSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEWMBQGA1UEAxMNQXBwbGUgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOSRqQkfkdseR1DrBe1eeYQt6zaiV0xV7IsZid75S2z1B6siMALoGD74UAnTf0GomPnRymacJGsR0KO75Bsqwx+VnnoMpEeLW9QWNzPLxA9NzhRp0ckZcvVdDtV/X5vyJQO6VY9NXQ3xZDUjFUsVWR2zlPf2nJ7PULrBWFBnjwi0IPfLrCwgb3C2PwEwjLdDzw+dPfMrSSgayP7OtbkO2V4c1ss9tTqt9A8OAJILsSEWLnTVPA3bYharo3GSR1NVwa8vQbP4++NwzeajTEV+H0xrUJZBicR0YgsQg0GHM4qBsTBY7FoEMoxos48d3mVz/2deZbxJ2HafMxRloXeUyS0CAwEAAaOCAXowggF2MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQr0GlHlHYJ/vRrjS5ApvdHTX8IXjAfBgNVHSMEGDAWgBQr0GlHlHYJ/vRrjS5ApvdHTX8IXjCCAREGA1UdIASCAQgwggEEMIIBAAYJKoZIhvdjZAUBMIHyMCoGCCsGAQUFBwIBFh5odHRwczovL3d3dy5hcHBsZS5jb20vYXBwbGVjYS8wgcMGCCsGAQUFBwICMIG2GoGzUmVsaWFuY2Ugb24gdGhpcyBjZXJ0aWZpY2F0ZSBieSBhbnkgcGFydHkgYXNzdW1lcyBhY2NlcHRhbmNlIG9mIHRoZSB0aGVuIGFwcGxpY2FibGUgc3RhbmRhcmQgdGVybXMgYW5kIGNvbmRpdGlvbnMgb2YgdXNlLCBjZXJ0aWZpY2F0ZSBwb2xpY3kgYW5kIGNlcnRpZmljYXRpb24gcHJhY3RpY2Ugc3RhdGVtZW50cy4wDQYJKoZIhvcNAQEFBQADggEBAFw2mUwteLftjJvc83eb8nbSdzBPwR+Fg4UbmT1HN/Kpm0COLNSxkBLYvvRzm+7SZA/LeU802KI++Xj/a8gH7H05g4tTINM4xLG/mk8Ka/8r/FmnBQl8F0BWER5007eLIztHo9VvJOLr0bdw3w9F4SfK8W147ee1Fxeo3H4iNcol1dkP1mvUoiQjEfehrI9zgWDGG1sJL5Ky+ERI8GA4nhX1PSZnIIozavcNgs/e66Mv+VNqW2TAYzN39zoHLFbr2g8hDtq6cxlPtdk2f8GHVdmnmbkyQvvY1XGefqFStxu9k0IkEirHDx22TZxeY8hLgBdQqorV2uT80AkHN7B1dSExggGxMIIBrQIBATCBiTB1MQswCQYDVQQGEwJVUzETMBEGA1UECgwKQXBwbGUgSW5jLjELMAkGA1UECwwCRzcxRDBCBgNVBAMMO0FwcGxlIFdvcmxkd2lkZSBEZXZlbG9wZXIgUmVsYXRpb25zIENlcnRpZmljYXRpb24gQXV0aG9yaXR5AhAtqwMbvdZlc9IHKXk8RJfEMAkGBSsOAwIaBQAwDQYJKoZIhvcNAQEBBQAEggEAaqDUzwypJ81QT6t9NkTSLLz8DCZ++1kPBHxAXZXirjf/wO/eS4KmEa9AHCsRjRZdwdA9E4Af3GGkLaRhxcNUh7OmkI0vb29iPmLnfsKCuYtoOTaqMtrRMDHC71tfB5TVasCohs0N+6EdW1BoL2j4YcDQgExj9W/xmCSYGmE9vdw3WXvTg1Rllj9yfC68Z73IhNe6tBn/M9N8lzp7PryWeQ1NjLfrafAmJkMM6d7mwoypHCZ7R5d2SqcHCamGEAkXadU6VrOU1GBirBc5odDC1esQG+GyUG4j0i60WtM53UTAehZkucp4bC7Ah4u4XLXbzRNdZR3KxBDw1UYpWfFxYw==") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | t.Log(summary) 14 | if info != nil { 15 | t.Log(summary.Environment, info.TransactionId, info.ProductId) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /receipt_type.go: -------------------------------------------------------------------------------- 1 | package apple 2 | 3 | import "net/http" 4 | 5 | // 21000 App Store 无法读取你提供的JSON数据 6 | // 21002 收据数据不符合格式 7 | // 21003 收据无法被验证 8 | // 21004 你提供的共享密钥和账户的共享密钥不一致 9 | // 21005 收据服务器当前不可用 10 | // 21006 收据是有效的,但订阅服务已经过期。当收到这个信息时,解码后的收据信息也包含在返回内容中 11 | // 21007 收据信息是测试用(sandbox),但却被发送到产品环境中验证 12 | // 21008 收据信息是产品环境中使用,但却被发送到测试环境中验证 13 | 14 | type ReceiptOptions struct { 15 | Client *http.Client `json:"-"` 16 | Receipt string `json:"receipt-data"` 17 | Password string `json:"password,omitempty"` 18 | ExcludeOldTransactions bool `json:"exclude-old-transactions"` 19 | } 20 | 21 | type ReceiptOption func(opts *ReceiptOptions) 22 | 23 | func WithHTTPClient(client *http.Client) ReceiptOption { 24 | return func(opts *ReceiptOptions) { 25 | if client != nil { 26 | opts.Client = client 27 | } 28 | } 29 | } 30 | 31 | func WithPassword(password string) ReceiptOption { 32 | return func(opts *ReceiptOptions) { 33 | opts.Password = password 34 | } 35 | } 36 | 37 | func WithExcludeOldTransactions(value bool) ReceiptOption { 38 | return func(opts *ReceiptOptions) { 39 | opts.ExcludeOldTransactions = value 40 | } 41 | } 42 | 43 | type ReceiptSummary struct { 44 | Environment Environment `json:"environment"` 45 | IsRetryable bool `json:"is_retryable"` 46 | LatestReceipt string `json:"latest_receipt,omitempty"` 47 | LatestReceiptInfo []*LatestReceiptInfo `json:"latest_receipt_info,omitempty"` 48 | PendingRenewalInfo []*PendingRenewalInfo `json:"pending_renewal_info,omitempty"` 49 | Receipt *Receipt `json:"receipt"` 50 | Status int `json:"status"` 51 | } 52 | 53 | type Receipt struct { 54 | AdamId int64 `json:"adam_id"` 55 | AppItemId int64 `json:"app_item_id"` 56 | ApplicationVersion string `json:"application_version"` 57 | BundleId string `json:"bundle_id"` 58 | DownloadId int64 `json:"download_id"` 59 | ExpirationDate string `json:"expiration_date"` 60 | ExpirationDateMs string `json:"expiration_date_ms"` 61 | ExpirationDatePST string `json:"expiration_date_pst"` 62 | OriginalPurchaseDate string `json:"original_purchase_date"` 63 | OriginalPurchaseDateMS string `json:"original_purchase_date_ms"` 64 | OriginalPurchaseDatePST string `json:"original_purchase_date_pst"` 65 | OriginalApplicationVersion string `json:"original_application_version"` 66 | PreorderDate string `json:"preorder_date"` 67 | PreorderDateMS string `json:"preorder_date_ms"` 68 | PreorderDatePST string `json:"preorder_date_pst"` 69 | ReceiptCreationDate string `json:"receipt_creation_date"` 70 | ReceiptCreationDateMS string `json:"receipt_creation_date_ms"` 71 | ReceiptCreationDatePST string `json:"receipt_creation_date_pst"` 72 | ReceiptType string `json:"receipt_type"` 73 | RequestDate string `json:"request_date"` 74 | RequestDateMS string `json:"request_date_ms"` 75 | RequestDatePST string `json:"request_date_pst"` 76 | VersionExternalIdentifier int64 `json:"version_external_identifier"` 77 | InApp []*InApp `json:"in_app"` 78 | } 79 | 80 | type InApp struct { 81 | CancellationDate string `json:"cancellation_date"` 82 | CancellationDateMs string `json:"cancellation_date_ms"` 83 | CancellationDatePST string `json:"cancellation_date_pst"` 84 | CancellationReason string `json:"cancellation_reason"` 85 | ExpiresDate string `json:"expires_date"` 86 | ExpiresDateMs string `json:"expires_date_ms"` 87 | ExpiresDatePST string `json:"expires_date_pst"` 88 | IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"` 89 | IsTrialPeriod string `json:"is_trial_period"` 90 | OriginalPurchaseDate string `json:"original_purchase_date"` 91 | OriginalPurchaseDateMS string `json:"original_purchase_date_ms"` 92 | OriginalPurchaseDatePST string `json:"original_purchase_date_pst"` 93 | OriginalTransactionId string `json:"original_transaction_id"` 94 | ProductId string `json:"product_id"` 95 | PromotionalOfferId string `json:"promotional_offer_id"` 96 | PurchaseDate string `json:"purchase_date"` 97 | PurchaseDateMS string `json:"purchase_date_ms"` 98 | PurchaseDatePST string `json:"purchase_date_pst"` 99 | Quantity string `json:"quantity"` 100 | TransactionId string `json:"transaction_id"` 101 | WebOrderLineItemId string `json:"web_order_line_item_id"` 102 | } 103 | 104 | type LatestReceiptInfo struct { 105 | AppAccountToken string `json:"app_account_token"` 106 | CancellationDate string `json:"cancellation_date"` 107 | CancellationDateMs string `json:"cancellation_date_ms"` 108 | CancellationDatePST string `json:"cancellation_date_pst"` 109 | CancellationReason string `json:"cancellation_reason"` 110 | ExpiresDate string `json:"expires_date"` 111 | ExpiresDateMs string `json:"expires_date_ms"` 112 | ExpiresDatePST string `json:"expires_date_pst"` 113 | InAppOwnershipType string `json:"in_app_ownership_type"` 114 | IsInIntroOfferPeriod string `json:"is_in_intro_offer_period"` 115 | IsTrialPeriod string `json:"is_trial_period"` 116 | IsUpgraded string `json:"is_upgraded"` 117 | OfferCodeRefName string `json:"offer_code_ref_name"` 118 | OriginalPurchaseDate string `json:"original_purchase_date"` 119 | OriginalPurchaseDateMS string `json:"original_purchase_date_ms"` 120 | OriginalPurchaseDatePST string `json:"original_purchase_date_pst"` 121 | OriginalTransactionId string `json:"original_transaction_id"` 122 | ProductId string `json:"product_id"` 123 | PromotionalOfferId string `json:"promotional_offer_id"` 124 | PurchaseDate string `json:"purchase_date"` 125 | PurchaseDateMS string `json:"purchase_date_ms"` 126 | PurchaseDatePST string `json:"purchase_date_pst"` 127 | Quantity string `json:"quantity"` 128 | SubscriptionGroupIdentifier string `json:"subscription_group_identifier"` 129 | TransactionId string `json:"transaction_id"` 130 | WebOrderLineItemId string `json:"web_order_line_item_id"` 131 | } 132 | 133 | type PendingRenewalInfo struct { 134 | SubscriptionExpirationIntent string `json:"expiration_intent"` 135 | SubscriptionAutoRenewProductID string `json:"auto_renew_product_id"` 136 | SubscriptionRetryFlag string `json:"is_in_billing_retry_period"` 137 | SubscriptionAutoRenewStatus string `json:"auto_renew_status"` 138 | SubscriptionPriceConsentStatus string `json:"price_consent_status"` 139 | ProductID string `json:"product_id"` 140 | OriginalTransactionID string `json:"original_transaction_id"` 141 | OfferCodeRefName string `json:"offer_code_ref_name,omitempty"` 142 | PromotionalOfferID string `json:"promotional_offer_id,omitempty"` 143 | PriceIncreaseStatus string `json:"price_increase_status,omitempty"` 144 | GracePeriodDate string `json:"grace_period_expires_date,omitempty"` 145 | GracePeriodDateMS string `json:"grace_period_expires_date_ms,omitempty"` 146 | GracePeriodDatePST string `json:"grace_period_expires_date_pst,omitempty"` 147 | } 148 | -------------------------------------------------------------------------------- /notification_test.go: -------------------------------------------------------------------------------- 1 | package apple_test 2 | 3 | import ( 4 | "github.com/smartwalle/apple" 5 | "testing" 6 | ) 7 | 8 | func TestDecodeNotification(t *testing.T) { 9 | var data = `{"signedPayload": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQb1BsZHZwU29FSDBsQnJqRFB2OWpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJeE1EZ3lOVEF5TlRBek5Gb1hEVEl6TURreU5EQXlOVEF6TTFvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCT29UY2FQY3BlaXBOTDllUTA2dEN1N3BVY3dkQ1hkTjh2R3FhVWpkNThaOHRMeGlVQzBkQmVBK2V1TVlnZ2gxLzVpQWsrRk14VUZtQTJhMXI0YUNaOFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkNPQ21NQnEvLzFMNWltdlZtcVgxb0NZZXFyTU1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQWw0SkI5R0pIaXhQMm51aWJ5VTFrM3dyaTVwc0dJeFBNRTA1c0ZLcTdoUXV6dmJleUJ1ODJGb3p6eG1ienBvZ29BakJMU0ZsMGRaV0lZbDJlalBWK0RpNWZCbktQdThteW1CUXRvRS9IMmJFUzBxQXM4Yk51ZVUzQ0JqamgxbHduRHNJPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJub3RpZmljYXRpb25UeXBlIjoiRElEX1JFTkVXIiwibm90aWZpY2F0aW9uVVVJRCI6Ijg1MTBhNTEzLWJlMTUtNGY1Yy1hODAyLWRiNGE5N2ZkMDJjNiIsImRhdGEiOnsiYnVuZGxlSWQiOiJjb20ucHJvamVjdDVlLm15bG9ncy5tZWlqaS5kZWJ1ZyIsImJ1bmRsZVZlcnNpb24iOiI0NTE4IiwiZW52aXJvbm1lbnQiOiJTYW5kYm94Iiwic2lnbmVkVHJhbnNhY3Rpb25JbmZvIjoiZXlKaGJHY2lPaUpGVXpJMU5pSXNJbmcxWXlJNld5Sk5TVWxGVFVSRFEwRTNZV2RCZDBsQ1FXZEpVV0ZRYjFCc1pIWndVMjlGU0RCc1FuSnFSRkIyT1dwQlMwSm5aM0ZvYTJwUFVGRlJSRUY2UWpGTlZWRjNVV2RaUkZaUlVVUkVSSFJDWTBoQ2MxcFRRbGhpTTBweldraGtjRnBIVldkU1IxWXlXbGQ0ZG1OSFZubEpSa3BzWWtkR01HRlhPWFZqZVVKRVdsaEtNR0ZYV25CWk1rWXdZVmM1ZFVsRlJqRmtSMmgyWTIxc01HVlVSVXhOUVd0SFFURlZSVU4zZDBOU2VsbDRSWHBCVWtKblRsWkNRVzlOUTJ0R2QyTkhlR3hKUld4MVdYazBlRU42UVVwQ1owNVdRa0ZaVkVGc1ZsUk5RalJZUkZSSmVFMUVaM2xPVkVGNVRsUkJlazVHYjFoRVZFbDZUVVJyZVU1RVFYbE9WRUY2VFRGdmQyZGFTWGhSUkVFclFtZE9Wa0pCVFUxT01VSjVZakpSWjFKVlRrUkpSVEZvV1hsQ1FtTklRV2RWTTFKMlkyMVZaMWxYTld0SlIyeFZaRmMxYkdONVFsUmtSemw1V2xOQ1UxcFhUbXhoV0VJd1NVWk9jRm95TlhCaWJXTjRURVJCY1VKblRsWkNRWE5OU1RCR2QyTkhlR3hKUm1SMlkyMTRhMlF5Ykd0YVUwSkZXbGhhYkdKSE9YZGFXRWxuVlcxV2MxbFlVbkJpTWpWNlRWSk5kMFZSV1VSV1VWRkxSRUZ3UW1OSVFuTmFVMEpLWW0xTmRVMVJjM2REVVZsRVZsRlJSMFYzU2xaVmVrSmFUVUpOUjBKNWNVZFRUVFE1UVdkRlIwTkRjVWRUVFRRNVFYZEZTRUV3U1VGQ1QyOVVZMkZRWTNCbGFYQk9URGxsVVRBMmRFTjFOM0JWWTNka1ExaGtUamgyUjNGaFZXcGtOVGhhT0hSTWVHbFZRekJrUW1WQksyVjFUVmxuWjJneEx6VnBRV3NyUmsxNFZVWnRRVEpoTVhJMFlVTmFPRk5xWjJkSlNVMUpTVU5DUkVGTlFtZE9Wa2hTVFVKQlpqaEZRV3BCUVUxQ09FZEJNVlZrU1hkUldVMUNZVUZHUkRoMmJFTk9VakF4UkVwdGFXYzVOMkpDT0RWaksyeHJSMHRhVFVoQlIwTkRjMGRCVVZWR1FuZEZRa0pIVVhkWmFrRjBRbWRuY2tKblJVWkNVV04zUVc5WmFHRklVakJqUkc5MlRESk9iR051VW5wTWJVWjNZMGQ0YkV4dFRuWmlVemt6WkRKU2VWcDZXWFZhUjFaNVRVUkZSME5EYzBkQlVWVkdRbnBCUW1ocFZtOWtTRkozVDJrNGRtSXlUbnBqUXpWb1kwaENjMXBUTldwaU1qQjJZakpPZW1ORVFYcE1XR1F6V2toS2JrNXFRWGxOU1VsQ1NHZFpSRlpTTUdkQ1NVbENSbFJEUTBGU1JYZG5aMFZPUW1kdmNXaHJhVWM1TWs1clFsRlpRazFKU0N0TlNVaEVRbWRuY2tKblJVWkNVV05EUVdwRFFuUm5lVUp6TVVwc1lrZHNhR0p0VG14SlJ6bDFTVWhTYjJGWVRXZFpNbFo1WkVkc2JXRlhUbWhrUjFWbldXNXJaMWxYTlRWSlNFSm9ZMjVTTlVsSFJucGpNMVowV2xoTloxbFhUbXBhV0VJd1dWYzFhbHBUUW5aYWFVSXdZVWRWWjJSSGFHeGlhVUpvWTBoQ2MyRlhUbWhaYlhoc1NVaE9NRmxYTld0WldFcHJTVWhTYkdOdE1YcEpSMFoxV2tOQ2FtSXlOV3RoV0ZKd1lqSTFla2xIT1cxSlNGWjZXbE4zWjFreVZubGtSMnh0WVZkT2FHUkhWV2RqUnpsellWZE9OVWxIUm5WYVEwSnFXbGhLTUdGWFduQlpNa1l3WVZjNWRVbElRbmxaVjA0d1lWZE9iRWxJVGpCWldGSnNZbGRXZFdSSVRYVk5SRmxIUTBOelIwRlJWVVpDZDBsQ1JtbHdiMlJJVW5kUGFUaDJaRE5rTTB4dFJuZGpSM2hzVEcxT2RtSlRPV3BhV0Vvd1lWZGFjRmt5UmpCYVYwWXhaRWRvZG1OdGJEQmxVemgzU0ZGWlJGWlNNRTlDUWxsRlJrTlBRMjFOUW5Fdkx6Rk1OV2x0ZGxadGNWZ3hiME5aWlhGeVRVMUJORWRCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVVVKbmIzRm9hMmxIT1RKT2EwSm5jMEpDUVVsR1FVUkJTMEpuWjNGb2EycFBVRkZSUkVGM1RtOUJSRUpzUVdwRlFXdzBTa0k1UjBwSWFYaFFNbTUxYVdKNVZURnJNM2R5YVRWd2MwZEplRkJOUlRBMWMwWkxjVGRvVVhWNmRtSmxlVUoxT0RKR2IzcDZlRzFpZW5CdloyOUJha0pNVTBac01HUmFWMGxaYkRKbGFsQldLMFJwTldaQ2JrdFFkVGh0ZVcxQ1VYUnZSUzlJTW1KRlV6QnhRWE00WWs1MVpWVXpRMEpxYW1neGJIZHVSSE5KUFNJc0lrMUpTVVJHYWtORFFYQjVaMEYzU1VKQlowbFZTWE5IYUZKM2NEQmpNbTUyVlRSWlUzbGpZV1pRVkdwNllrNWpkME5uV1VsTGIxcEplbW93UlVGM1RYZGFla1ZpVFVKclIwRXhWVVZCZDNkVFVWaENkMkpIVldkVmJUbDJaRU5DUkZGVFFYUkpSV042VFZOWmQwcEJXVVJXVVZGTVJFSXhRbU5JUW5OYVUwSkVXbGhLTUdGWFduQlpNa1l3WVZjNWRVbEZSakZrUjJoMlkyMXNNR1ZVUlZSTlFrVkhRVEZWUlVObmQwdFJXRUozWWtkVloxTlhOV3BNYWtWTVRVRnJSMEV4VlVWQ2FFMURWbFpOZDBob1kwNU5ha1YzVFhwRk0wMXFRWHBPZWtWM1YyaGpUazE2V1hkTmVrVTFUVVJCZDAxRVFYZFhha0l4VFZWUmQxRm5XVVJXVVZGRVJFUjBRbU5JUW5OYVUwSllZak5LYzFwSVpIQmFSMVZuVWtkV01scFhlSFpqUjFaNVNVWktiR0pIUmpCaFZ6bDFZM2xDUkZwWVNqQmhWMXB3V1RKR01HRlhPWFZKUlVZeFpFZG9kbU50YkRCbFZFVk1UVUZyUjBFeFZVVkRkM2REVW5wWmVFVjZRVkpDWjA1V1FrRnZUVU5yUm5kalIzaHNTVVZzZFZsNU5IaERla0ZLUW1kT1ZrSkJXVlJCYkZaVVRVaFpkMFZCV1VoTGIxcEplbW93UTBGUldVWkxORVZGUVVOSlJGbG5RVVZpYzFGTFF6azBVSEpzVjIxYVdHNVlaM1I0ZW1SV1NrdzRWREJUUjFsdVowUlNSM0J1WjI0elRqWlFWRGhLVFVWaU4wWkVhVFJpUW0xUWFFTnVXak12YzNFMlVFWXZZMGRqUzFoWGMwdzFkazkwWlZKb2VVbzBOWGd6UVZOUU4yTlBRaXRoWVc4NU1HWmpjSGhUZGk5RldrWmlibWxCWWs1bldrZG9TV2h3U1c4MFNEWk5TVWd6VFVKSlIwRXhWV1JGZDBWQ0wzZFJTVTFCV1VKQlpqaERRVkZCZDBoM1dVUldVakJxUWtKbmQwWnZRVlYxTjBSbGIxWm5lbWxLY1d0cGNHNWxkbkl6Y25JNWNreEtTM04zVW1kWlNVdDNXVUpDVVZWSVFWRkZSVTlxUVRSTlJGbEhRME56UjBGUlZVWkNla0ZDYUdsd2IyUklVbmRQYVRoMllqSk9lbU5ETldoalNFSnpXbE0xYW1JeU1IWmlNazU2WTBSQmVreFhSbmRqUjNoc1kyMDVkbVJIVG1oYWVrMTNUbmRaUkZaU01HWkNSRUYzVEdwQmMyOURjV2RMU1ZsdFlVaFNNR05FYjNaTU1rNTVZa00xYUdOSVFuTmFVelZxWWpJd2RsbFlRbmRpUjFaNVlqSTVNRmt5Um01TmVUVnFZMjEzZDBoUldVUldVakJQUWtKWlJVWkVPSFpzUTA1U01ERkVTbTFwWnprM1lrSTROV01yYkd0SFMxcE5RVFJIUVRGVlpFUjNSVUl2ZDFGRlFYZEpRa0pxUVZGQ1oyOXhhR3RwUnpreVRtdENaMGxDUWtGSlJrRkVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZRVVJDYkVGcVFrRllhRk54TlVsNVMyOW5UVU5RZEhjME9UQkNZVUkyTnpkRFlVVkhTbGgxWmxGQ0wwVnhXa2RrTmtOVGFtbERkRTl1ZFUxVVlsaFdXRzE0ZUdONFptdERUVkZFVkZOUWVHRnlXbGgyVG5KcmVGVXpWR3RWVFVrek0zbDZka1pXVmxKVU5IZDRWMHBET1RrMFQzTmtZMW8wSzFKSFRuTlpSSGxTTldkdFpISXdia1JIWnowaUxDSk5TVWxEVVhwRFEwRmpiV2RCZDBsQ1FXZEpTVXhqV0RocFRreEdVelZWZDBObldVbExiMXBKZW1vd1JVRjNUWGRhZWtWaVRVSnJSMEV4VlVWQmQzZFRVVmhDZDJKSFZXZFZiVGwyWkVOQ1JGRlRRWFJKUldONlRWTlpkMHBCV1VSV1VWRk1SRUl4UW1OSVFuTmFVMEpFV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxGUmpGa1IyaDJZMjFzTUdWVVJWUk5Ra1ZIUVRGVlJVTm5kMHRSV0VKM1lrZFZaMU5YTldwTWFrVk1UVUZyUjBFeFZVVkNhRTFEVmxaTmQwaG9ZMDVOVkZGM1RrUk5kMDFVWjNoUFZFRXlWMmhqVGsxNmEzZE9SRTEzVFZSbmVFOVVRVEpYYWtKdVRWSnpkMGRSV1VSV1VWRkVSRUpLUW1OSVFuTmFVMEpUWWpJNU1FbEZUa0pKUXpCblVucE5lRXBxUVd0Q1owNVdRa0Z6VFVoVlJuZGpSM2hzU1VWT2JHTnVVbkJhYld4cVdWaFNjR0l5TkdkUldGWXdZVWM1ZVdGWVVqVk5VazEzUlZGWlJGWlJVVXRFUVhCQ1kwaENjMXBUUWtwaWJVMTFUVkZ6ZDBOUldVUldVVkZIUlhkS1ZsVjZRakpOUWtGSFFubHhSMU5OTkRsQlowVkhRbE4xUWtKQlFXbEJNa2xCUWtwcWNFeDZNVUZqY1ZSMGEzbEtlV2RTVFdNelVrTldPR05YYWxSdVNHTkdRbUphUkhWWGJVSlRjRE5hU0hSbVZHcHFWSFY0ZUVWMFdDOHhTRGRaZVZsc00wbzJXVkppVkhwQ1VFVldiMEV2Vm1oWlJFdFlNVVI1ZUU1Q01HTlVaR1J4V0d3MVpIWk5WbnAwU3pVeE4wbEVkbGwxVmxSYVdIQnRhMDlzUlV0TllVNURUVVZCZDBoUldVUldVakJQUWtKWlJVWk1kWGN6Y1VaWlRUUnBZWEJKY1ZvemNqWTVOall2WVhsNVUzSk5RVGhIUVRGVlpFVjNSVUl2ZDFGR1RVRk5Ra0ZtT0hkRVoxbEVWbEl3VUVGUlNDOUNRVkZFUVdkRlIwMUJiMGREUTNGSFUwMDBPVUpCVFVSQk1tZEJUVWRWUTAxUlEwUTJZMGhGUm13MFlWaFVVVmt5WlROMk9VZDNUMEZGV2t4MVRpdDVVbWhJUmtRdk0yMWxiM2xvY0cxMlQzZG5VRlZ1VUZkVWVHNVROR0YwSzNGSmVGVkRUVWN4Yldsb1JFc3hRVE5WVkRneVRsRjZOakJwYlU5c1RUSTNhbUprYjFoME1sRm1lVVpOYlN0WmFHbGtSR3RNUmpGMlRGVmhaMDAyUW1kRU5UWkxlVXRCUFQwaVhYMC5leUowY21GdWMyRmpkR2x2Ymtsa0lqb2lNakF3TURBd01EQTFNemMzTnprek1TSXNJbTl5YVdkcGJtRnNWSEpoYm5OaFkzUnBiMjVKWkNJNklqSXdNREF3TURBd05USTFOelUyTURjaUxDSjNaV0pQY21SbGNreHBibVZKZEdWdFNXUWlPaUl5TURBd01EQXdNREEwTURJMU5qazRJaXdpWW5WdVpHeGxTV1FpT2lKamIyMHVjSEp2YW1WamREVmxMbTE1Ykc5bmN5NXRaV2xxYVM1a1pXSjFaeUlzSW5CeWIyUjFZM1JKWkNJNkltTnZiUzV3Y205cVpXTjBOV1V1YldWcGFta3ViWGxzYjJkekxrMXZiblJvYkhrdVVISmxiV2wxYlM1MFpYTjBJaXdpYzNWaWMyTnlhWEIwYVc5dVIzSnZkWEJKWkdWdWRHbG1hV1Z5SWpvaU1qQTVNakF3TkRVaUxDSndkWEpqYUdGelpVUmhkR1VpT2pFMk5USTBNekl4T0RFd01EQXNJbTl5YVdkcGJtRnNVSFZ5WTJoaGMyVkVZWFJsSWpveE5qVXlNelF6TkRneU1EQXdMQ0psZUhCcGNtVnpSR0YwWlNJNk1UWTFNalF6TWpNMk1UQXdNQ3dpY1hWaGJuUnBkSGtpT2pFc0luUjVjR1VpT2lKQmRYUnZMVkpsYm1WM1lXSnNaU0JUZFdKelkzSnBjSFJwYjI0aUxDSmhjSEJCWTJOdmRXNTBWRzlyWlc0aU9pSmpORFJrTTJFeU5DMHpOMlU1TFRReE0yVXRZV1kxWXkxaE56TmhOVE5rT0dZMk9UY2lMQ0pwYmtGd2NFOTNibVZ5YzJocGNGUjVjR1VpT2lKUVZWSkRTRUZUUlVRaUxDSnphV2R1WldSRVlYUmxJam94TmpVeU5ETXlNVFEwTlRZd0xDSmxiblpwY205dWJXVnVkQ0k2SWxOaGJtUmliM2dpZlEuV2xEdlpsTkNVNHByTVJpOTdtRlRLejdwOGpKVEdTVUNHamNzX1JVU19Ma0JaRGZteHN6Z19TMXB6ZE1zV2x2c1N0WEFPYm9FZ0h1UWJiSHpMYi1OTlEiLCJzaWduZWRSZW5ld2FsSW5mbyI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5nMVl5STZXeUpOU1VsRlRVUkRRMEUzWVdkQmQwbENRV2RKVVdGUWIxQnNaSFp3VTI5RlNEQnNRbkpxUkZCMk9XcEJTMEpuWjNGb2EycFBVRkZSUkVGNlFqRk5WVkYzVVdkWlJGWlJVVVJFUkhSQ1kwaENjMXBUUWxoaU0wcHpXa2hrY0ZwSFZXZFNSMVl5V2xkNGRtTkhWbmxKUmtwc1lrZEdNR0ZYT1hWamVVSkVXbGhLTUdGWFduQlpNa1l3WVZjNWRVbEZSakZrUjJoMlkyMXNNR1ZVUlV4TlFXdEhRVEZWUlVOM2QwTlNlbGw0UlhwQlVrSm5UbFpDUVc5TlEydEdkMk5IZUd4SlJXeDFXWGswZUVONlFVcENaMDVXUWtGWlZFRnNWbFJOUWpSWVJGUkplRTFFWjNsT1ZFRjVUbFJCZWs1R2IxaEVWRWw2VFVScmVVNUVRWGxPVkVGNlRURnZkMmRhU1hoUlJFRXJRbWRPVmtKQlRVMU9NVUo1WWpKUloxSlZUa1JKUlRGb1dYbENRbU5JUVdkVk0xSjJZMjFWWjFsWE5XdEpSMnhWWkZjMWJHTjVRbFJrUnpsNVdsTkNVMXBYVG14aFdFSXdTVVpPY0ZveU5YQmliV040VEVSQmNVSm5UbFpDUVhOTlNUQkdkMk5IZUd4SlJtUjJZMjE0YTJReWJHdGFVMEpGV2xoYWJHSkhPWGRhV0VsblZXMVdjMWxZVW5CaU1qVjZUVkpOZDBWUldVUldVVkZMUkVGd1FtTklRbk5hVTBKS1ltMU5kVTFSYzNkRFVWbEVWbEZSUjBWM1NsWlZla0phVFVKTlIwSjVjVWRUVFRRNVFXZEZSME5EY1VkVFRUUTVRWGRGU0VFd1NVRkNUMjlVWTJGUVkzQmxhWEJPVERsbFVUQTJkRU4xTjNCVlkzZGtRMWhrVGpoMlIzRmhWV3BrTlRoYU9IUk1lR2xWUXpCa1FtVkJLMlYxVFZsbloyZ3hMelZwUVdzclJrMTRWVVp0UVRKaE1YSTBZVU5hT0ZOcVoyZEpTVTFKU1VOQ1JFRk5RbWRPVmtoU1RVSkJaamhGUVdwQlFVMUNPRWRCTVZWa1NYZFJXVTFDWVVGR1JEaDJiRU5PVWpBeFJFcHRhV2M1TjJKQ09EVmpLMnhyUjB0YVRVaEJSME5EYzBkQlVWVkdRbmRGUWtKSFVYZFpha0YwUW1kbmNrSm5SVVpDVVdOM1FXOVphR0ZJVWpCalJHOTJUREpPYkdOdVVucE1iVVozWTBkNGJFeHRUblppVXprelpESlNlVnA2V1hWYVIxWjVUVVJGUjBORGMwZEJVVlZHUW5wQlFtaHBWbTlrU0ZKM1QyazRkbUl5VG5walF6Vm9ZMGhDYzFwVE5XcGlNakIyWWpKT2VtTkVRWHBNV0dReldraEtiazVxUVhsTlNVbENTR2RaUkZaU01HZENTVWxDUmxSRFEwRlNSWGRuWjBWT1FtZHZjV2hyYVVjNU1rNXJRbEZaUWsxSlNDdE5TVWhFUW1kbmNrSm5SVVpDVVdORFFXcERRblJuZVVKek1VcHNZa2RzYUdKdFRteEpSemwxU1VoU2IyRllUV2RaTWxaNVpFZHNiV0ZYVG1oa1IxVm5XVzVyWjFsWE5UVkpTRUpvWTI1U05VbEhSbnBqTTFaMFdsaE5aMWxYVG1wYVdFSXdXVmMxYWxwVFFuWmFhVUl3WVVkVloyUkhhR3hpYVVKb1kwaENjMkZYVG1oWmJYaHNTVWhPTUZsWE5XdFpXRXByU1VoU2JHTnRNWHBKUjBaMVdrTkNhbUl5Tld0aFdGSndZakkxZWtsSE9XMUpTRlo2V2xOM1oxa3lWbmxrUjJ4dFlWZE9hR1JIVldkalJ6bHpZVmRPTlVsSFJuVmFRMEpxV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxJUW5sWlYwNHdZVmRPYkVsSVRqQlpXRkpzWWxkV2RXUklUWFZOUkZsSFEwTnpSMEZSVlVaQ2QwbENSbWx3YjJSSVVuZFBhVGgyWkROa00weHRSbmRqUjNoc1RHMU9kbUpUT1dwYVdFb3dZVmRhY0ZreVJqQmFWMFl4WkVkb2RtTnRiREJsVXpoM1NGRlpSRlpTTUU5Q1FsbEZSa05QUTIxTlFuRXZMekZNTldsdGRsWnRjVmd4YjBOWlpYRnlUVTFCTkVkQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlVVSm5iM0ZvYTJsSE9USk9hMEpuYzBKQ1FVbEdRVVJCUzBKblozRm9hMnBQVUZGUlJFRjNUbTlCUkVKc1FXcEZRV3cwU2tJNVIwcElhWGhRTW01MWFXSjVWVEZyTTNkeWFUVndjMGRKZUZCTlJUQTFjMFpMY1Rkb1VYVjZkbUpsZVVKMU9ESkdiM3A2ZUcxaWVuQnZaMjlCYWtKTVUwWnNNR1JhVjBsWmJESmxhbEJXSzBScE5XWkNia3RRZFRodGVXMUNVWFJ2UlM5SU1tSkZVekJ4UVhNNFlrNTFaVlV6UTBKcWFtZ3hiSGR1UkhOSlBTSXNJazFKU1VSR2FrTkRRWEI1WjBGM1NVSkJaMGxWU1hOSGFGSjNjREJqTW01MlZUUlpVM2xqWVdaUVZHcDZZazVqZDBObldVbExiMXBKZW1vd1JVRjNUWGRhZWtWaVRVSnJSMEV4VlVWQmQzZFRVVmhDZDJKSFZXZFZiVGwyWkVOQ1JGRlRRWFJKUldONlRWTlpkMHBCV1VSV1VWRk1SRUl4UW1OSVFuTmFVMEpFV2xoS01HRlhXbkJaTWtZd1lWYzVkVWxGUmpGa1IyaDJZMjFzTUdWVVJWUk5Ra1ZIUVRGVlJVTm5kMHRSV0VKM1lrZFZaMU5YTldwTWFrVk1UVUZyUjBFeFZVVkNhRTFEVmxaTmQwaG9ZMDVOYWtWM1RYcEZNMDFxUVhwT2VrVjNWMmhqVGsxNldYZE5la1UxVFVSQmQwMUVRWGRYYWtJeFRWVlJkMUZuV1VSV1VWRkVSRVIwUW1OSVFuTmFVMEpZWWpOS2MxcElaSEJhUjFWblVrZFdNbHBYZUhaalIxWjVTVVpLYkdKSFJqQmhWemwxWTNsQ1JGcFlTakJoVjFwd1dUSkdNR0ZYT1hWSlJVWXhaRWRvZG1OdGJEQmxWRVZNVFVGclIwRXhWVVZEZDNkRFVucFplRVY2UVZKQ1owNVdRa0Z2VFVOclJuZGpSM2hzU1VWc2RWbDVOSGhEZWtGS1FtZE9Wa0pCV1ZSQmJGWlVUVWhaZDBWQldVaExiMXBKZW1vd1EwRlJXVVpMTkVWRlFVTkpSRmxuUVVWaWMxRkxRemswVUhKc1YyMWFXRzVZWjNSNGVtUldTa3c0VkRCVFIxbHVaMFJTUjNCdVoyNHpUalpRVkRoS1RVVmlOMFpFYVRSaVFtMVFhRU51V2pNdmMzRTJVRVl2WTBkalMxaFhjMHcxZGs5MFpWSm9lVW8wTlhnelFWTlFOMk5QUWl0aFlXODVNR1pqY0hoVGRpOUZXa1ppYm1sQllrNW5Xa2RvU1dod1NXODBTRFpOU1VnelRVSkpSMEV4VldSRmQwVkNMM2RSU1UxQldVSkJaamhEUVZGQmQwaDNXVVJXVWpCcVFrSm5kMFp2UVZWMU4wUmxiMVpuZW1sS2NXdHBjRzVsZG5JemNuSTVja3hLUzNOM1VtZFpTVXQzV1VKQ1VWVklRVkZGUlU5cVFUUk5SRmxIUTBOelIwRlJWVVpDZWtGQ2FHbHdiMlJJVW5kUGFUaDJZakpPZW1ORE5XaGpTRUp6V2xNMWFtSXlNSFppTWs1NlkwUkJla3hYUm5kalIzaHNZMjA1ZG1SSFRtaGFlazEzVG5kWlJGWlNNR1pDUkVGM1RHcEJjMjlEY1dkTFNWbHRZVWhTTUdORWIzWk1NazU1WWtNMWFHTklRbk5hVXpWcVlqSXdkbGxZUW5kaVIxWjVZakk1TUZreVJtNU5lVFZxWTIxM2QwaFJXVVJXVWpCUFFrSlpSVVpFT0hac1EwNVNNREZFU20xcFp6azNZa0k0TldNcmJHdEhTMXBOUVRSSFFURlZaRVIzUlVJdmQxRkZRWGRKUWtKcVFWRkNaMjl4YUd0cFJ6a3lUbXRDWjBsQ1FrRkpSa0ZFUVV0Q1oyZHhhR3RxVDFCUlVVUkJkMDV2UVVSQ2JFRnFRa0ZZYUZOeE5VbDVTMjluVFVOUWRIYzBPVEJDWVVJMk56ZERZVVZIU2xoMVpsRkNMMFZ4V2tka05rTlRhbWxEZEU5dWRVMVVZbGhXV0cxNGVHTjRabXREVFZGRVZGTlFlR0Z5V2xoMlRuSnJlRlV6Vkd0VlRVa3pNM2w2ZGtaV1ZsSlVOSGQ0VjBwRE9UazBUM05rWTFvMEsxSkhUbk5aUkhsU05XZHRaSEl3YmtSSFp6MGlMQ0pOU1VsRFVYcERRMEZqYldkQmQwbENRV2RKU1V4aldEaHBUa3hHVXpWVmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhkYWVrVmlUVUpyUjBFeFZVVkJkM2RUVVZoQ2QySkhWV2RWYlRsMlpFTkNSRkZUUVhSSlJXTjZUVk5aZDBwQldVUldVVkZNUkVJeFFtTklRbk5hVTBKRVdsaEtNR0ZYV25CWk1rWXdZVmM1ZFVsRlJqRmtSMmgyWTIxc01HVlVSVlJOUWtWSFFURlZSVU5uZDB0UldFSjNZa2RWWjFOWE5XcE1ha1ZNVFVGclIwRXhWVVZDYUUxRFZsWk5kMGhvWTA1TlZGRjNUa1JOZDAxVVozaFBWRUV5VjJoalRrMTZhM2RPUkUxM1RWUm5lRTlVUVRKWGFrSnVUVkp6ZDBkUldVUldVVkZFUkVKS1FtTklRbk5hVTBKVFlqSTVNRWxGVGtKSlF6Qm5VbnBOZUVwcVFXdENaMDVXUWtGelRVaFZSbmRqUjNoc1NVVk9iR051VW5CYWJXeHFXVmhTY0dJeU5HZFJXRll3WVVjNWVXRllValZOVWsxM1JWRlpSRlpSVVV0RVFYQkNZMGhDYzFwVFFrcGliVTExVFZGemQwTlJXVVJXVVZGSFJYZEtWbFY2UWpKTlFrRkhRbmx4UjFOTk5EbEJaMFZIUWxOMVFrSkJRV2xCTWtsQlFrcHFjRXg2TVVGamNWUjBhM2xLZVdkU1RXTXpVa05XT0dOWGFsUnVTR05HUW1KYVJIVlhiVUpUY0ROYVNIUm1WR3BxVkhWNGVFVjBXQzh4U0RkWmVWbHNNMG8yV1ZKaVZIcENVRVZXYjBFdlZtaFpSRXRZTVVSNWVFNUNNR05VWkdSeFdHdzFaSFpOVm5wMFN6VXhOMGxFZGxsMVZsUmFXSEJ0YTA5c1JVdE5ZVTVEVFVWQmQwaFJXVVJXVWpCUFFrSlpSVVpNZFhjemNVWlpUVFJwWVhCSmNWb3pjalk1TmpZdllYbDVVM0pOUVRoSFFURlZaRVYzUlVJdmQxRkdUVUZOUWtGbU9IZEVaMWxFVmxJd1VFRlJTQzlDUVZGRVFXZEZSMDFCYjBkRFEzRkhVMDAwT1VKQlRVUkJNbWRCVFVkVlEwMVJRMFEyWTBoRlJtdzBZVmhVVVZreVpUTjJPVWQzVDBGRldreDFUaXQ1VW1oSVJrUXZNMjFsYjNsb2NHMTJUM2RuVUZWdVVGZFVlRzVUTkdGMEszRkplRlZEVFVjeGJXbG9SRXN4UVROVlZEZ3lUbEY2TmpCcGJVOXNUVEkzYW1Ka2IxaDBNbEZtZVVaTmJTdFphR2xrUkd0TVJqRjJURlZoWjAwMlFtZEVOVFpMZVV0QlBUMGlYWDAuZXlKdmNtbG5hVzVoYkZSeVlXNXpZV04wYVc5dVNXUWlPaUl5TURBd01EQXdNRFV5TlRjMU5qQTNJaXdpWVhWMGIxSmxibVYzVUhKdlpIVmpkRWxrSWpvaVkyOXRMbkJ5YjJwbFkzUTFaUzV0WldscWFTNXRlV3h2WjNNdVRXOXVkR2hzZVM1UWNtVnRhWFZ0TG5SbGMzUWlMQ0p3Y205a2RXTjBTV1FpT2lKamIyMHVjSEp2YW1WamREVmxMbTFsYVdwcExtMTViRzluY3k1TmIyNTBhR3g1TGxCeVpXMXBkVzB1ZEdWemRDSXNJbUYxZEc5U1pXNWxkMU4wWVhSMWN5STZNU3dpYzJsbmJtVmtSR0YwWlNJNk1UWTFNalF6TWpFME5EVXpOeXdpWlc1MmFYSnZibTFsYm5RaU9pSlRZVzVrWW05NEluMC53NF9SdTlZdkRfOHkzNW83M2UzUzVFd3hEMXVST0JFTFdHSl9DellJQmRlbG5GWm9UdFdsamk4cFpBTm1xQ1VxODVkeHA3MmhreG1DWVI3cm12bHJhdyJ9LCJ2ZXJzaW9uIjoiMi4wIn0.khc9ZwQngf1P99s_sJCHm68BBLoNe1jbJQktGXlpk_Djgoe9rqpfi3BrQcbYZ-xGm1YcR6FW8KQD-YBhHt771Q"}` 10 | var notification, err = apple.DecodeNotification([]byte(data)) 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | t.Log(notification.NotificationType) 15 | t.Log(notification.NotificationUUID) 16 | t.Log(notification.Data.Renewal) 17 | t.Log(notification.Data.Transaction) 18 | } 19 | --------------------------------------------------------------------------------