Skip to content

🧾 StoreKit API client for verifying in-app purchase receipts with Apple's App Store

License

Notifications You must be signed in to change notification settings

Gurpartap/storekit-go

Repository files navigation

storekit-go

GoDoc

Use this for verifying App Store receipts.

  • Battle proven technology
  • Blockchain free

See GoDoc for detailed API response reference.

Usage example (auto-renewing subscriptions)

package main

import (
	"context"
	"errors"
	"fmt"
	"os"
	"time"

	"github.com/Gurpartap/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 := []byte("...")

	err := verifyAndSave(appStoreSharedSecret, userId, receiptData)
	if err != nil {
		fmt.Println("could not verify receipt:", err)
	}
}

func verifyAndSave(appStoreSharedSecret, userId string, receiptData []byte) 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
	}
}

About

🧾 StoreKit API client for verifying in-app purchase receipts with Apple's App Store

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages