Unverified Commit b761963c authored by Simon Esposito's avatar Simon Esposito Committed by GitHub
Browse files

Return validated IAP with seen_before flag. (#676)

* Return validated IAP with seen_before flag

Change IAP validation APIs to return all purchases with a 'seen before'
flag for already validated purchases instead of returning an error
in such cases and only newly seen purchases in case of success.
parent 5d3f3f99
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -13,7 +13,7 @@ require (
	github.com/gorilla/mux v1.8.0
	github.com/gorilla/websocket v1.4.2
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0
	github.com/heroiclabs/nakama-common v1.16.0
	github.com/heroiclabs/nakama-common v0.0.0-20210907132733-e48b382b6dab
	github.com/jackc/pgconn v1.8.1
	github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451
	github.com/jackc/pgtype v1.7.0
+2 −4
+0 −10
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ import (

	"github.com/gofrs/uuid"
	"github.com/heroiclabs/nakama-common/api"
	"github.com/heroiclabs/nakama-common/runtime"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
@@ -61,9 +60,6 @@ func (s *ApiServer) ValidatePurchaseApple(ctx context.Context, in *api.ValidateP

	validation, err := ValidatePurchasesApple(ctx, s.logger, s.db, userID, s.config.GetIAP().Apple.SharedPassword, in.Receipt)
	if err != nil {
		if err == runtime.ErrPurchaseReceiptAlreadySeen {
			return nil, status.Error(codes.AlreadyExists, err.Error())
		}
		return nil, err
	}

@@ -116,9 +112,6 @@ func (s *ApiServer) ValidatePurchaseGoogle(ctx context.Context, in *api.Validate

	validation, err := ValidatePurchaseGoogle(ctx, s.logger, s.db, userID, s.config.GetIAP().Google, in.Purchase)
	if err != nil {
		if err == runtime.ErrPurchaseReceiptAlreadySeen {
			return nil, status.Error(codes.AlreadyExists, err.Error())
		}
		return nil, err
	}

@@ -177,9 +170,6 @@ func (s *ApiServer) ValidatePurchaseHuawei(ctx context.Context, in *api.Validate

	validation, err := ValidatePurchaseHuawei(ctx, s.logger, s.db, userID, s.config.GetIAP().Huawei, in.Purchase, in.Signature)
	if err != nil {
		if err == runtime.ErrPurchaseReceiptAlreadySeen {
			return nil, status.Error(codes.AlreadyExists, err.Error())
		}
		return nil, err
	}

+55 −35
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ import (

	"github.com/gofrs/uuid"
	"github.com/heroiclabs/nakama-common/api"
	"github.com/heroiclabs/nakama-common/runtime"
	"github.com/heroiclabs/nakama/v3/iap"
	"github.com/jackc/pgtype"
	"go.uber.org/zap"
@@ -87,17 +86,13 @@ func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB,
		})
	}

	storedPurchases, err := storePurchases(ctx, db, storagePurchases)
	purchases, err := storePurchases(ctx, db, storagePurchases)
	if err != nil {
		return nil, err
	}

	if len(storedPurchases) < 1 {
		return nil, runtime.ErrPurchaseReceiptAlreadySeen
	}

	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(storedPurchases))
	for _, p := range storedPurchases {
	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(purchases))
	for _, p := range purchases {
		validatedPurchases = append(validatedPurchases, &api.ValidatedPurchase{
			ProductId:        p.productId,
			TransactionId:    p.transactionId,
@@ -130,7 +125,7 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
		return nil, err
	}

	storedPurchases, err := storePurchases(ctx, db, []*storagePurchase{
	purchases, err := storePurchases(ctx, db, []*storagePurchase{
		{
			userID:        userID,
			store:         api.ValidatedPurchase_GOOGLE_PLAY_STORE,
@@ -148,12 +143,8 @@ func ValidatePurchaseGoogle(ctx context.Context, logger *zap.Logger, db *sql.DB,
		return nil, err
	}

	if len(storedPurchases) < 1 {
		return nil, runtime.ErrPurchaseReceiptAlreadySeen
	}

	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(storedPurchases))
	for _, p := range storedPurchases {
	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(purchases))
	for _, p := range purchases {
		validatedPurchases = append(validatedPurchases, &api.ValidatedPurchase{
			ProductId:        p.productId,
			TransactionId:    p.transactionId,
@@ -195,7 +186,7 @@ func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB,
		env = api.ValidatedPurchase_SANDBOX
	}

	storedPurchases, err := storePurchases(ctx, db, []*storagePurchase{
	purchases, err := storePurchases(ctx, db, []*storagePurchase{
		{
			userID:        userID,
			store:         api.ValidatedPurchase_HUAWEI_APP_GALLERY,
@@ -213,12 +204,8 @@ func ValidatePurchaseHuawei(ctx context.Context, logger *zap.Logger, db *sql.DB,
		return nil, err
	}

	if len(storedPurchases) < 1 {
		return nil, runtime.ErrPurchaseReceiptAlreadySeen
	}

	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(storedPurchases))
	for _, p := range storedPurchases {
	validatedPurchases := make([]*api.ValidatedPurchase, 0, len(purchases))
	for _, p := range purchases {
		validatedPurchases = append(validatedPurchases, &api.ValidatedPurchase{
			ProductId:        p.productId,
			TransactionId:    p.transactionId,
@@ -412,6 +399,7 @@ type storagePurchase struct {
	createTime    time.Time // Set by storePurchases
	updateTime    time.Time // Set by storePurchases
	environment   api.ValidatedPurchase_Environment
	seenBefore    bool // Set by storePurchases
}

func storePurchases(ctx context.Context, db *sql.DB, purchases []*storagePurchase) ([]*storagePurchase, error) {
@@ -421,12 +409,14 @@ func storePurchases(ctx context.Context, db *sql.DB, purchases []*storagePurchas

	statements := make([]string, 0, len(purchases))
	params := make([]interface{}, 0, len(purchases)*7)
	transactionIDsToPurchase := make(map[string]*storagePurchase)
	offset := 0
	for _, purchase := range purchases {
		statement := fmt.Sprintf("($%d, $%d, $%d, $%d, $%d, $%d, $%d)", offset+1, offset+2, offset+3, offset+4, offset+5, offset+6, offset+7)
		offset += 7
		statements = append(statements, statement)
		params = append(params, purchase.userID, purchase.store, purchase.transactionId, purchase.productId, purchase.purchaseTime, purchase.rawResponse, purchase.environment)
		transactionIDsToPurchase[purchase.transactionId] = purchase
	}

	query := `
@@ -451,38 +441,68 @@ DO
RETURNING
    transaction_id, create_time, update_time
`
	storedTransactionIDs := make(map[string]*storagePurchase)
	insertedTransactionIDs := make(map[string]struct{})
	rows, err := db.QueryContext(ctx, query, params...)
	if err != nil {
		return nil, err
	}
	for rows.Next() {
		// Newly inserted purchases
		var transactionId string
		var createTime pgtype.Timestamptz
		var updateTime pgtype.Timestamptz
		err := rows.Scan(&transactionId, &createTime, &updateTime)
		if err != nil {
		if err = rows.Scan(&transactionId, &createTime, &updateTime); err != nil {
			rows.Close()
			return nil, err
		}
		storedTransactionIDs[transactionId] = &storagePurchase{
			createTime: createTime.Time,
			updateTime: updateTime.Time,
		storedPurchase, _ := transactionIDsToPurchase[transactionId]
		storedPurchase.createTime = createTime.Time
		storedPurchase.updateTime = updateTime.Time
		storedPurchase.seenBefore = false
		insertedTransactionIDs[storedPurchase.transactionId] = struct{}{}
	}
	rows.Close()
	if err := rows.Err(); err != nil {
		return nil, err
	}

	// Go over purchases that have not been inserted (already exist in the DB) and fetch createTime and updateTime
	if len(transactionIDsToPurchase) > len(insertedTransactionIDs) {
		seenIDs := make([]string, 0, len(transactionIDsToPurchase))
		for tID, _ := range transactionIDsToPurchase {
			if _, ok := insertedTransactionIDs[tID]; !ok {
				seenIDs = append(seenIDs, tID)
			}
		}

		rows, err = db.QueryContext(ctx, "SELECT transaction_id, create_time, update_time FROM purchase WHERE transaction_id IN ($1)", strings.Join(seenIDs, ", "))
		if err != nil {
			return nil, err
		}
		for rows.Next() {
			// Already seen purchases
			var transactionId string
			var createTime pgtype.Timestamptz
			var updateTime pgtype.Timestamptz
			if err = rows.Scan(&transactionId, &createTime, &updateTime); err != nil {
				rows.Close()
				return nil, err
			}
			storedPurchase, _ := transactionIDsToPurchase[transactionId]
			storedPurchase.createTime = createTime.Time
			storedPurchase.updateTime = updateTime.Time
			storedPurchase.seenBefore = true
		}
		rows.Close()
		if err := rows.Err(); err != nil {
			return nil, err
		}
	}

	storedPurchases := make([]*storagePurchase, 0, len(storedTransactionIDs))
	for _, purchase := range purchases {
		if ts, ok := storedTransactionIDs[purchase.transactionId]; ok {
			purchase.createTime = ts.createTime
			purchase.updateTime = ts.updateTime
	storedPurchases := make([]*storagePurchase, 0, len(transactionIDsToPurchase))
	for _, purchase := range transactionIDsToPurchase {
		storedPurchases = append(storedPurchases, purchase)
	}
	}

	return storedPurchases, nil
}
+2 −1
Original line number Diff line number Diff line
@@ -6257,7 +6257,7 @@ func getJsValidatedPurchasesData(validation *api.ValidatePurchaseResponse) map[s
}

func getJsValidatedPurchaseData(purchase *api.ValidatedPurchase) map[string]interface{} {
	validatedPurchaseMap := make(map[string]interface{}, 8)
	validatedPurchaseMap := make(map[string]interface{}, 9)
	validatedPurchaseMap["productId"] = purchase.ProductId
	validatedPurchaseMap["transactionId"] = purchase.TransactionId
	validatedPurchaseMap["store"] = purchase.Store.String()
@@ -6266,6 +6266,7 @@ func getJsValidatedPurchaseData(purchase *api.ValidatedPurchase) map[string]inte
	validatedPurchaseMap["createTime"] = purchase.CreateTime.Seconds
	validatedPurchaseMap["updateTime"] = purchase.UpdateTime.Seconds
	validatedPurchaseMap["environment"] = purchase.Environment.String()
	validatedPurchaseMap["seenBefore"] = purchase.SeenBefore

	return validatedPurchaseMap
}
Loading