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 }