Skip to content
This repository has been archived by the owner on Sep 7, 2024. It is now read-only.
/ storekit-go Public archive
forked from Gurpartap/storekit-go

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

neone/storekit-go

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 (
	"errors"
	"fmt"
	"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.
	respBody, resp, err := client.Verify(&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).
	for _, latestReceiptInfo := range resp.LatestReceiptInfo {
		productID := latestReceiptInfo.ProductIdentifier
		expiresAtMs := latestReceiptInfo.SubscriptionExpirationDateMs
		// 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

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%