appstore Golang Client Library

repository·master·Indexed 41 days ago

https://github.com/richzw/appstore

A Golang client library for interacting with the Apple App Store Server API and handling App Store Server Notifications. It provides tools for retrieving transaction and refund history, looking up order IDs, and parsing signed JWS payloads and V2 notifications. The library includes built-in support for ECDSA public key extraction, rate limiting, environment switching between production and sandbox, and a functional middleware pattern for HTTP client configuration.

Tokens
14.2K
Snippets
47
Records
51
Agent score
52%

What's inside appstore

  1. Configure the StoreClient

    master

    To use the library, you must initialize a StoreConfig and pass it to appstore.NewStoreClient.

    Required configuration fields in StoreConfig:

    • KeyContent: The byte slice of your private key (generated from App Store Connect).
    • KeyID: Your API Key ID.
    • BundleID: Your app's bundle identifier.
    • Issuer: Your API Key Issuer ID.
    • Sandbox: A boolean to toggle between the production and sandbox environments.
    import(
    	"github.com/richzw/appstore"
    )
    
    c := &appstore.StoreConfig{
        KeyContent: []byte(ACCOUNTPRIVATEKEY),
        KeyID:      "FAKEKEYID",
        BundleID:   "fake.bundle.id",
        Issuer:     "xxxxx-xx-xx-xx-xxxxxxxxxx",
        Sandbox:    false,
    }
    a := appstore.NewStoreClient(c)
  2. Configure HTTP client middleware using DoFunc

    master

    The library uses a functional middleware pattern to decorate an HTTPClient. A DoFunc is a function type that matches the signature func(*http.Request) (*http.Response, error). You can chain multiple decorators (like adding headers, setting bodies, or adding retries) to build a complex request execution pipeline. The base HTTPClient must implement the Do(r *http.Request) (*http.Response, error) method.

    // Example of how DoFunc works as a decorator
    var client appstore.HTTPClient = // your implementation
    
    // Create a decorated function
    decoratedDo := appstore.SetHeader(client, "X-Custom-Header", "value")
    
    // Use it to execute a request
    resp, err := decoratedDo(req)
  3. Initialize the StoreClient

    master

    To interact with the Apple App Store Server API, you must create a StoreClient using a StoreConfig. The configuration requires credentials obtained from App Store Connect.

    Key configuration fields:

    • KeyContent: The raw bytes of your .p8 private key.
    • KeyID: Your private key ID (e.g., 2X9R4HXF34).
    • BundleID: Your app's bundle ID.
    • Issuer: Your issuer ID from App Store Connect.
    • Sandbox: Set to true to use the Sandbox environment (HostSandBox), otherwise defaults to Production (HostProduction).
    • TrustedCertPool: An optional *x509.CertPool. Defaults to a pool containing only the Apple Root CA - G3.
    config := &appstore.StoreConfig{
    	KeyContent: []byte("..."),
    	KeyID:      "2X9R4HXF34",
    	BundleID:   "com.example.app",
    	Issuer:     "57246542-96fe-1a63-e053-0824d011072a",
    	Sandbox:    true,
    }
    client := appstore.NewStoreClient(config)
  4. Handle Rate Limits and Environments

    master

    Environment Switching

    The library handles the base URL based on the Sandbox flag in StoreConfig. If you are unsure which environment a transaction belongs to:

    1. Attempt the call using the production URL.
    2. If you receive error code 4040010 (TransactionIdNotFoundError), retry the call using the sandbox URL.

    Rate Limiting

    If you exceed the per-hour limit, the API returns an HTTP 429 with a RateLimitExceededError.

    • Retry Logic: Check the Retry-After header, which contains a UNIX time in milliseconds indicating when you can next send a request.
    • Best Practices: Throttle periodic requests and implement a queue to retry failed jobs after the specified delay.
  5. Get Transaction Info

    master

    Retrieve information about a specific transaction using its ID. After receiving the response, use ParseSignedTransactions to decode the signed transaction data for validation.

    Validation Tip: To ensure a transaction is valid, check if the TransactionID in the parsed response matches the transactionId you requested.

    // ... setup client 'a' ...
    transactionId := "FAKETRANSACTIONID"
    response, err := a.GetTransactionInfo(context.TODO(), transactionId)
    
    transactions, err := a.ParseSignedTransactions([]string{response.SignedTransactionInfo})
    if transactions[0].TransactionID == transactionId {
        // the transaction is valid
    }
  6. Look Up Order ID

    master

    Retrieve orders associated with a specific invoice order ID using LookupOrderID. Use ParseSignedTransactions to decode the resulting signed transactions.

    // ... setup client 'a' ...
    invoiceOrderId := "FAKEORDERID"
    rsp, err := a.LookupOrderID(context.TODO(), invoiceOrderId)
    
    orders, err := a.ParseSignedTransactions(rsp.SignedTransactions)
  7. Parse App Store Notifications (V2)

    master

    The library provides several ways to parse App Store Server Notifications:

    1. ParseNotificationV2(tokenStr): Parses a signed renewal info or signed transaction info string. Returns a JWT token which can be used to access claims via jwt.MapClaims.
    2. ParseNotificationV2WithClaim(tokenStr): Parses a JWSTransactionDecodedPayload encoded string, allowing direct access to the decoded payload fields.
    3. Parsing from a Request Body: If you receive a JSON request body from the App Store, unmarshal it into an appstore.NotificationV2 struct, then use:
      • ParseNotificationV2Payload(notification.SignedPayload) to get the main payload.
      • ParseNotificationV2TransactionInfo(payload.Data.SignedTransactionInfo) to get transaction details.
      • ParseNotificationV2RenewalInfo(payload.Data.SignedRenewalInfo) to get renewal details.
    // Example: Parsing from a JSON request body
    var notification appstore.NotificationV2
    json.Unmarshal(reqBody, &notification)
    
    // 1. Parse the main payload
    payload, err := a.ParseNotificationV2Payload(notification.SignedPayload)
    
    // 2. Parse transaction info from payload
    transactionInfo, err := a.ParseNotificationV2TransactionInfo(payload.Data.SignedTransactionInfo)
    
    // 3. Parse renewal info from payload
    renewalInfo, err := a.ParseNotificationV2RenewalInfo(payload.Data.SignedRenewalInfo)
  8. Get Transaction History

    master

    Retrieve the transaction history for a given original transaction ID. You can filter results by setting productType in a url.Values query object (e.g., AUTO_RENEWABLE or NON_CONSUMABLE).

    // ... setup client 'a' ...
    originalTransactionId := "FAKEORDERID"
    query := &url.Values{}
    query.Set("productType", "AUTO_RENEWABLE")
    query.Set("productType", "NON_CONSUMABLE")
    
    gotRsp, err := a.GetTransactionHistory(context.TODO(), originalTransactionId, query)
    
    for _, rsp := range gotRsp {
       trans, err := a.ParseSignedTransactions(rsp.SignedTransactions)
    }
  9. Get Refund History

    master

    Retrieve the refund history for a specific original transaction ID using GetRefundHistory. Use ParseSignedTransactions to decode the signed transaction data.

    // ... setup client 'a' ...
    originalTransactionId := "FAKEORDERID"
    gotRsp, err := a.GetRefundHistory(context.TODO(), originalTransactionId)
    
    for _, rsp := range gotRsp {
       trans, err := a.ParseSignedTransactions(rsp.SignedTransactions)
    }
  10. Parse JWS Encoded Strings

    master

    The ParseJWSEncodeString(jwsEncode string) method can automatically detect and decode JWS strings into either a *JWSTransaction or a *JWSRenewalInfoDecodedPayload based on the payload content.

    decoded, err := client.ParseJWSEncodeString(jwsString)
    if err != nil {
    	// handle error
    }
    // decoded will be either *JWSTransaction or *JWSRenewalInfoDecodedPayload
  11. Retrieve Transaction Information

    master

    Use GetTransactionInfo to fetch details about a specific transaction using its transactionId.

    ctx := context.Background()
    transactionId := "12345678"
    info, err := client.GetTransactionInfo(ctx, transactionId)
    if err != nil {
    	// handle error
    }
    // use info (*TransactionInfoResponse)