tkcashgame_v4/pkg/appstore/client.go

227 lines
6.3 KiB
Go
Raw Normal View History

2025-10-22 10:01:11 +00:00
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 customers 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
}