227 lines
6.3 KiB
Go
227 lines
6.3 KiB
Go
|
|
package appstore
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"crypto/ecdsa"
|
|||
|
|
"crypto/x509"
|
|||
|
|
"encoding/base64"
|
|||
|
|
"encoding/json"
|
|||
|
|
"encoding/pem"
|
|||
|
|
"fmt"
|
|||
|
|
"sandc/pkg/bhttp"
|
|||
|
|
"strings"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
jwt "github.com/dgrijalva/jwt-go"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type options struct {
|
|||
|
|
token string
|
|||
|
|
}
|
|||
|
|
type Option func(*options)
|
|||
|
|
|
|||
|
|
func WithToken(b string) Option {
|
|||
|
|
return func(c *options) {
|
|||
|
|
c.token = b
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type Client struct {
|
|||
|
|
privateKeyByte []byte
|
|||
|
|
issuer string
|
|||
|
|
keyID string
|
|||
|
|
token string
|
|||
|
|
env string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NewClient creates a new App Store Connect API client.
|
|||
|
|
func NewClient(issuer, keyID, env string, privateKeyByte []byte, opts ...Option) (*Client, error) {
|
|||
|
|
opt := options{
|
|||
|
|
token: "",
|
|||
|
|
}
|
|||
|
|
for _, o := range opts {
|
|||
|
|
o(&opt)
|
|||
|
|
}
|
|||
|
|
return &Client{
|
|||
|
|
privateKeyByte: privateKeyByte,
|
|||
|
|
issuer: issuer,
|
|||
|
|
keyID: keyID,
|
|||
|
|
env: env,
|
|||
|
|
token: opt.token,
|
|||
|
|
}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// IsProdEnv returns true if the client is configured to use the production environment.
|
|||
|
|
func (c *Client) IsProdEnv() bool {
|
|||
|
|
return c.env == "prod"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// loadPrivateKey loads a private key from a file.
|
|||
|
|
func (c *Client) loadPrivateKey(bytes []byte) (*ecdsa.PrivateKey, error) {
|
|||
|
|
block, _ := pem.Decode(bytes)
|
|||
|
|
if block == nil {
|
|||
|
|
return nil, fmt.Errorf("failed to decode PEM block containing private key")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
ecdsaKey, ok := key.(*ecdsa.PrivateKey)
|
|||
|
|
if !ok {
|
|||
|
|
return nil, fmt.Errorf("not an ECDSA private key")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return ecdsaKey, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateToken generates a JWT token for the App Store Connect API.
|
|||
|
|
func (c *Client) GenerateToken(pkg string, expire time.Duration) (string, error) {
|
|||
|
|
privateKey, err := c.loadPrivateKey(c.privateKeyByte)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
|||
|
|
"iss": c.issuer,
|
|||
|
|
"iat": time.Now().Unix(),
|
|||
|
|
"exp": time.Now().Add(expire).Unix(),
|
|||
|
|
"aud": "appstoreconnect-v1",
|
|||
|
|
"bid": pkg,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
token.Header["kid"] = c.keyID
|
|||
|
|
|
|||
|
|
signedToken, err := token.SignedString(privateKey)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.token = signedToken
|
|||
|
|
|
|||
|
|
return signedToken, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// map[bundleId:com.hotpotgames.mergegangster.global environment:Sandbox inAppOwnershipType:PURCHASED originalPurchaseDate:1.689239628e+12 originalTransactionId:2000000368088682 productId:mergegangster_noads purchaseDate:1.689239628e+12 quantity:1 signedDate:1.689594496689e+12 storefront:HKG storefrontId:143463 transactionId:2000000368088682 transactionReason:PURCHASE type:Non-Consumable]
|
|||
|
|
// TransactionInfo represents the transaction info for a given transaction id
|
|||
|
|
type TransactionInfo struct {
|
|||
|
|
BundleId string `json:"bundleId"`
|
|||
|
|
Environment string `json:"environment"`
|
|||
|
|
InAppOwnershipType string `json:"inAppOwnershipType"`
|
|||
|
|
OriginalPurchaseDate int64 `json:"originalPurchaseDate"`
|
|||
|
|
OriginalTransactionId string `json:"originalTransactionId"`
|
|||
|
|
ProductId string `json:"productId"`
|
|||
|
|
PurchaseDate int64 `json:"purchaseDate"`
|
|||
|
|
Quantity int32 `json:"quantity"`
|
|||
|
|
SignedDate int64 `json:"signedDate"`
|
|||
|
|
Storefront string `json:"storefront"`
|
|||
|
|
StorefrontId string `json:"storefrontId"`
|
|||
|
|
TransactionId string `json:"transactionId"`
|
|||
|
|
TransactionReason string `json:"transactionReason"`
|
|||
|
|
Type string `json:"type"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// TransactionInfoRes represents the transaction info response
|
|||
|
|
type TransactionInfoRes struct {
|
|||
|
|
SignedTransactionInfo string `json:"signedTransactionInfo,omitempty"`
|
|||
|
|
ErrorCode int `json:"errorCode,omitempty"`
|
|||
|
|
ErrorMessage string `json:"errorMessage,omitempty"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GetTransactionInfo returns the transaction info for a given transaction id
|
|||
|
|
func (c *Client) GetTransactionInfo(transactionID, pkg string) (*TransactionInfo, error) {
|
|||
|
|
var url string
|
|||
|
|
if c.IsProdEnv() {
|
|||
|
|
url = fmt.Sprintf("https://api.storekit.itunes.apple.com/inApps/v1/transactions/%s", transactionID)
|
|||
|
|
} else {
|
|||
|
|
url = fmt.Sprintf("https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/%s", transactionID)
|
|||
|
|
}
|
|||
|
|
bhttp, err := bhttp.NewBhttpClient()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("bhttpClient init failed: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if c.token == "" {
|
|||
|
|
token, err := c.GenerateToken(pkg, 24*time.Hour)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("generate token failed: %w", err)
|
|||
|
|
}
|
|||
|
|
c.token = token
|
|||
|
|
fmt.Println("new")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fmt.Println("token: ", c.token)
|
|||
|
|
|
|||
|
|
bhttp.SetHeader("Authorization", fmt.Sprintf("Bearer %s", c.token))
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
resJson, err := bhttp.DoGet(url)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("getTransactionInfo http get error: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var info TransactionInfoRes
|
|||
|
|
err = json.Unmarshal(resJson, &info)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("getTransactionInfo unmarshal err: %w, body: %s", err, string(resJson))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if info.ErrorMessage != "" {
|
|||
|
|
return nil, fmt.Errorf("getTransactionInfo error: %s", info.ErrorMessage)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// signedTransactionInfo A customer’s in-app purchase transaction, signed by Apple, in JSON Web Signature (JWS) format.
|
|||
|
|
signedPayload := info.SignedTransactionInfo
|
|||
|
|
|
|||
|
|
// decode signedTransactionInfo
|
|||
|
|
segments := strings.Split(signedPayload, ".")
|
|||
|
|
payloadBytes, err := base64.RawURLEncoding.DecodeString(segments[1])
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("error decoding payload: %w", err)
|
|||
|
|
}
|
|||
|
|
var payload TransactionInfo
|
|||
|
|
err = json.Unmarshal(payloadBytes, &payload)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("error unmarshaling payload: %w", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &payload, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// GenerateToken generates a JWT token for the App Store Connect API.
|
|||
|
|
func GenerateToken(issuer, keyID, pkg string, privateKeyByte []byte, expire time.Duration) (string, error) {
|
|||
|
|
block, _ := pem.Decode(privateKeyByte)
|
|||
|
|
if block == nil {
|
|||
|
|
return "", fmt.Errorf("failed to decode PEM block containing private key")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
privateKey, ok := key.(*ecdsa.PrivateKey)
|
|||
|
|
if !ok {
|
|||
|
|
return "", fmt.Errorf("not an ECDSA private key")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
token := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
|||
|
|
"iss": issuer,
|
|||
|
|
"iat": time.Now().Unix(),
|
|||
|
|
"exp": time.Now().Add(expire).Unix(),
|
|||
|
|
"aud": "appstoreconnect-v1",
|
|||
|
|
"bid": pkg,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
token.Header["kid"] = keyID
|
|||
|
|
|
|||
|
|
signedToken, err := token.SignedString(privateKey)
|
|||
|
|
if err != nil {
|
|||
|
|
return "", err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return signedToken, nil
|
|||
|
|
}
|