Created
December 16, 2016 19:20
Standalone (Play Store only) go-iap for GAE
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package yourgaeproject | |
// go-iap doesn't work for GAE since we need to use urlfetch | |
// | |
// Copy this source file to your GAE to your project. | |
// | |
// The API is mostly the same as go-iap. I prefixed some stuff since this file lives within my GAE project's package. | |
// client, err := PlaystoreNew(ctx, jsonKey) | |
// | |
// purchase, err := client.VerifyProduct("com.you.yourapp", productId, purchaseToken) | |
// Check purchaseState (not sure if 0 means Purchased or Canceled) | |
import ( | |
"crypto" | |
"crypto/rsa" | |
"crypto/sha1" | |
"crypto/x509" | |
"encoding/base64" | |
"fmt" | |
"net/http" | |
"time" | |
"golang.org/x/net/context" | |
"golang.org/x/oauth2" | |
"golang.org/x/oauth2/google" | |
androidpublisher "google.golang.org/api/androidpublisher/v2" | |
"google.golang.org/appengine/urlfetch" | |
) | |
const ( | |
defaultPlaystoreTimeout = time.Second * 5 | |
) | |
var playstoreTimeout = defaultPlaystoreTimeout | |
// SetTimeout sets dial timeout duration | |
func PlaystoreSetTimeout(t time.Duration) { | |
playstoreTimeout = t | |
} | |
// The IABClient type is an interface to verify purchase token | |
type PlaystoreIABClient interface { | |
VerifySubscription(string, string, string) (*androidpublisher.SubscriptionPurchase, error) | |
VerifyProduct(string, string, string) (*androidpublisher.ProductPurchase, error) | |
CancelSubscription(string, string, string) error | |
RefundSubscription(string, string, string) error | |
RevokeSubscription(string, string, string) error | |
} | |
// The Client type implements VerifySubscription method | |
type PlaystoreClient struct { | |
httpClient *http.Client | |
} | |
// New returns http client which includes the credentials to access androidpublisher API. | |
// You should create a service account for your project at | |
// https://console.developers.google.com and download a JSON key file to set this argument. | |
func PlaystoreNew(ctx context.Context, jsonKey []byte) (PlaystoreClient, error) { | |
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope) | |
if err != nil { | |
return PlaystoreClient{}, err | |
} | |
httpClient := &http.Client{ | |
Transport: &oauth2.Transport{ | |
Source: conf.TokenSource(ctx), | |
Base: &urlfetch.Transport{ | |
Context: ctx, | |
}, | |
}, | |
} | |
return PlaystoreClient{ | |
httpClient: httpClient, | |
}, err | |
} | |
// VerifySubscription verifies subscription status | |
func (c *PlaystoreClient) VerifySubscription( | |
packageName string, | |
subscriptionID string, | |
token string, | |
) (*androidpublisher.SubscriptionPurchase, error) { | |
service, err := androidpublisher.New(c.httpClient) | |
if err != nil { | |
return nil, err | |
} | |
ps := androidpublisher.NewPurchasesSubscriptionsService(service) | |
result, err := ps.Get(packageName, subscriptionID, token).Do() | |
return result, err | |
} | |
// VerifyProduct verifies product status | |
func (c *PlaystoreClient) VerifyProduct( | |
packageName string, | |
productID string, | |
token string, | |
) (*androidpublisher.ProductPurchase, error) { | |
service, err := androidpublisher.New(c.httpClient) | |
if err != nil { | |
return nil, err | |
} | |
ps := androidpublisher.NewPurchasesProductsService(service) | |
result, err := ps.Get(packageName, productID, token).Do() | |
return result, err | |
} | |
// CancelSubscription cancels a user's subscription purchase. | |
func (c *PlaystoreClient) CancelSubscription(packageName string, subscriptionID string, token string) error { | |
service, err := androidpublisher.New(c.httpClient) | |
if err != nil { | |
return err | |
} | |
ps := androidpublisher.NewPurchasesSubscriptionsService(service) | |
err = ps.Cancel(packageName, subscriptionID, token).Do() | |
return err | |
} | |
// RefundSubscription refunds a user's subscription purchase, but the subscription remains valid | |
// until its expiration time and it will continue to recur. | |
func (c *PlaystoreClient) RefundSubscription(packageName string, subscriptionID string, token string) error { | |
service, err := androidpublisher.New(c.httpClient) | |
if err != nil { | |
return err | |
} | |
ps := androidpublisher.NewPurchasesSubscriptionsService(service) | |
err = ps.Refund(packageName, subscriptionID, token).Do() | |
return err | |
} | |
// RevokeSubscription refunds and immediately revokes a user's subscription purchase. | |
// Access to the subscription will be terminated immediately and it will stop recurring. | |
func (c *PlaystoreClient) RevokeSubscription(packageName string, subscriptionID string, token string) error { | |
service, err := androidpublisher.New(c.httpClient) | |
if err != nil { | |
return err | |
} | |
ps := androidpublisher.NewPurchasesSubscriptionsService(service) | |
err = ps.Revoke(packageName, subscriptionID, token).Do() | |
return err | |
} | |
// VerifySignature verifies in app billing signature. | |
// You need to prepare a public key for your Android app's in app billing | |
// at https://play.google.com/apps/publish/ | |
func PlaystoreVerifySignature(base64EncodedPublicKey string, receipt []byte, signature string) (isValid bool, err error) { | |
// prepare public key | |
decodedPublicKey, err := base64.StdEncoding.DecodeString(base64EncodedPublicKey) | |
if err != nil { | |
return false, fmt.Errorf("failed to decode public key") | |
} | |
publicKeyInterface, err := x509.ParsePKIXPublicKey(decodedPublicKey) | |
if err != nil { | |
return false, fmt.Errorf("failed to parse public key") | |
} | |
publicKey, _ := publicKeyInterface.(*rsa.PublicKey) | |
// generate hash value from receipt | |
hasher := sha1.New() | |
hasher.Write(receipt) | |
hashedReceipt := hasher.Sum(nil) | |
// decode signature | |
decodedSignature, err := base64.StdEncoding.DecodeString(signature) | |
if err != nil { | |
return false, fmt.Errorf("failed to decode signature") | |
} | |
// verify | |
if err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA1, hashedReceipt, decodedSignature); err != nil { | |
return false, nil | |
} | |
return true, nil | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment