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