Fork of https://github.com/Gurpartap/storekit-go
Use this for verifying App Store receipts.
- Battle proven technology
- Blockchain free
See GoDoc for detailed API response reference.
package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/qonversion/storekit-go"
)
func main() {
// Get it from https://AppsToReconnect.apple.com 🤯
appStoreSharedSecret = os.GetEnv("APP_STORE_SHARED_SECRET")
// Your own userId
userId := "12345"
// Input coming from either user device or subscription notifications webhook
receiptData := "..."
err := verifyAndSave(appStoreSharedSecret, userId, receiptData)
if err != nil {
fmt.Println("could not verify receipt:", err)
}
}
func verifyAndSave(appStoreSharedSecret, userId string, receiptData string) error {
// Use .OnProductionEnv() when deploying
//
// storekit-go automatically retries sandbox server upon incompatible
// environment error. This is necessary because App Store Reviewer's purchase
// requests go through the sandbox server instead of production.
//
// Use .WithoutEnvAutoFix() to disable automatic env switching and retrying
// (not recommended on production)
client := storekit.NewVerificationClient().OnSandboxEnv()
// respBody is raw bytes of response, useful for storing, auditing, and for
// future verification checks. resp is the same parsed and mapped to a struct.
ctx, _ := context.WithTimeout(context.Background(), 15*time.Second)
respBody, resp, err := client.Verify(ctx, &storekit.ReceiptRequest{
ReceiptData: receiptData,
Password: appStoreSharedSecret,
ExcludeOldTransactions: true,
})
if err != nil {
return err // code: internal error
}
if resp.Status != 0 {
return errors.New(
fmt.Sprintf("receipt rejected by App Store with status = %d", resp.Status),
) // code: permission denied
}
// If receipt does not contain any active subscription info it is probably a
// fraudulent attempt at activating subscription from a jailbroken device.
if len(resp.LatestReceiptInfo) == 0 {
// keep it 🤫 that we know what's going on
return errors.New("unknown error") // code: internal (instead of invalid argument)
}
// resp.LatestReceiptInfo works for me. but, alternatively (as Apple devs also
// recommend) you can loop over resp.Receipt.InAppPurchaseReceipt, and filter
// for the receipt with the highest expiresAtMs to find the appropriate latest
// subscription (not shown in this example). if you have multiple subscription
// groups, look for transactions with expiresAt > time.Now().
for _, latestReceiptInfo := range resp.LatestReceiptInfo {
productID := latestReceiptInfo.ProductId
expiresAtMs := latestReceiptInfo.ExpiresDateMs
// cancelledAtStr := latestReceiptInfo.CancellationDate
// defensively check for necessary data, because StoreKit API responses can be a
// bit adventurous
if productID == "" {
return errors.New("missing product_id in the latest receipt info") // code: internal error
}
if expiresAtMs == 0 {
return errors.New("missing expiry date in latest receipt info") // code: internal error
}
expiresAt := time.Unix(0, expiresAtMs*1000000)
fmt.Printf(
"userId = %s has subscribed for product_id = %s which expires_at = %s",
userId,
productID,
expiresAt,
)
// ✅ Save or return productID, expiresAt, cancelledAt, respBody
}
}