IAP 라이브러리 추가 from kd-branch
This commit is contained in:
110
iap/galaxystore.go
Normal file
110
iap/galaxystore.go
Normal file
@ -0,0 +1,110 @@
|
||||
package iap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GalaxyStoreIAPVerifier struct {
|
||||
ItemId string `json:"itemId"`
|
||||
PaymentId string `json:"paymentId"`
|
||||
OrderId string `json:"orderId"`
|
||||
PackageName string `json:"packageName"`
|
||||
ItemName string `json:"itemName"`
|
||||
ItemDesc string `json:"itemDesc"`
|
||||
PurchaseDate string `json:"purchaseDate"`
|
||||
PaymentAmount string `json:"paymentAmount"`
|
||||
Status string `json:"status"`
|
||||
PaymentMethod string `json:"paymentMethod"`
|
||||
Mode string `json:"mode"`
|
||||
ConsumeYN string `json:"consumeYN"`
|
||||
ConsumeDate string `json:"consumeDate"`
|
||||
ConsumeDeviceModel string `json:"consumeDeviceModel"`
|
||||
PassThroughParam string `json:"passThroughParam"`
|
||||
CurrencyCode string `json:"currencyCode"`
|
||||
CurrencyUnit string `json:"currencyUnit"`
|
||||
VerificationLog string
|
||||
}
|
||||
|
||||
func (verifer *GalaxyStoreIAPVerifier) Verify(purchaseId string, paymentId string, developerPayload string, itemId string, packageName string) (bool, error) {
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", "https://iap.samsungapps.com/iap/v6/receipt?purchaseID="+purchaseId, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
json.Unmarshal(body, verifer)
|
||||
|
||||
verifer.VerificationLog = string(body)
|
||||
|
||||
if verifer.Status != "success" {
|
||||
return false, errors.New("Status is wrong:" + verifer.Status)
|
||||
}
|
||||
|
||||
if verifer.ItemId != itemId {
|
||||
return false, errors.New("itemId is different")
|
||||
}
|
||||
|
||||
if verifer.PackageName != packageName {
|
||||
return false, errors.New("packageName is different")
|
||||
}
|
||||
|
||||
if !strings.EqualFold(verifer.PassThroughParam, developerPayload) {
|
||||
return false, errors.New("developerPayload is different : " + verifer.PassThroughParam + "/" + developerPayload)
|
||||
}
|
||||
|
||||
if verifer.PaymentId != paymentId {
|
||||
return false, errors.New("paymentId is different")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Owned Item - mItemId: kd_currency_gold_75
|
||||
// Owned Item - mItemName: Gold x75
|
||||
// Owned Item - mItemPrice: 1300.000000
|
||||
// Owned Item - mItemPriceString: ₩1,300
|
||||
// Owned Item - mCurrencyUnit: ₩
|
||||
// Owned Item - mCurrencyCode: KRW
|
||||
// Owned Item - mItemDesc: 75 Gold
|
||||
// Owned Item - mPaymentId: TPMTID20240208KR32371566
|
||||
// Owned Item - mPurchaseId: 34c50c9677e8e837e7b7a6eaac37a385fb7e4b36437820825bc7d824647c6aa7
|
||||
// Owned Item - mPurchaseDate: -2147483648
|
||||
// Owned Item - mPassThroughParam: testpayload
|
||||
|
||||
// {
|
||||
// "itemId": "kd_currency_gold_75",
|
||||
// "paymentId": "TPMTID20240208KR32367386",
|
||||
// "orderId": "P20240208KR32367386",
|
||||
// "packageName": "com.yjmgames.kingdom.gal",
|
||||
// "itemName": "Gold x75",
|
||||
// "itemDesc": "75 Gold",
|
||||
// "purchaseDate": "2024-02-08 02:33:24",
|
||||
// "paymentAmount": "1300.0",
|
||||
// "status": "success",
|
||||
// "paymentMethod": "Credit Card",
|
||||
// "mode": "TEST",
|
||||
// "consumeYN": "Y",
|
||||
// "consumeDate": "2024-02-08 02:52:10",
|
||||
// "consumeDeviceModel": "SM-S918N",
|
||||
// "passThroughParam": "testpayload",
|
||||
// "currencyCode": "KRW",
|
||||
// "currencyUnit": "₩"
|
||||
// }
|
||||
173
iap/iap.go
Normal file
173
iap/iap.go
Normal file
@ -0,0 +1,173 @@
|
||||
package iap
|
||||
|
||||
import (
|
||||
"context"
|
||||
b64 "encoding/base64"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"go-ayo/logger"
|
||||
|
||||
"github.com/awa/go-iap/appstore/api"
|
||||
"github.com/awa/go-iap/playstore"
|
||||
)
|
||||
|
||||
var PlayStorePublicKey string
|
||||
var PlayStorePackageName string
|
||||
var PlayStoreClient *playstore.Client
|
||||
|
||||
type PlayStoreReceiptInfo struct {
|
||||
OrderId string `json:"orderId"`
|
||||
PackageName string `json:"packageName"`
|
||||
ProductId string `json:"productId"`
|
||||
PurchaseTime int `json:"purchaseTime"`
|
||||
PurchaseState int `json:"purchaseState"`
|
||||
PurchaseToken string `json:"purchaseToken"`
|
||||
Quantity int `json:"quantity"`
|
||||
Acknowledged bool `json:"acknowledged"`
|
||||
}
|
||||
|
||||
func PlayStoreInit(packageName string, publickey string) {
|
||||
jsonKey, err := ioutil.ReadFile("playstore-iap-kingdom.json")
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
PlayStoreClient, err = playstore.New(jsonKey)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
PlayStorePublicKey = publickey
|
||||
PlayStorePackageName = packageName
|
||||
}
|
||||
|
||||
func PlayStoreVerifyReceipt(receptData string, signature string) (isVerified bool, receiptInfo PlayStoreReceiptInfo) {
|
||||
var info PlayStoreReceiptInfo
|
||||
uDec, _ := b64.URLEncoding.DecodeString(receptData)
|
||||
|
||||
bOk, err := playstore.VerifySignature(PlayStorePublicKey, []byte(uDec), signature)
|
||||
if bOk == false || err != nil {
|
||||
logger.Println("playstore - VerifySignature fail :" + err.Error())
|
||||
return bOk, info
|
||||
}
|
||||
json.Unmarshal(uDec, &info)
|
||||
return bOk, info
|
||||
}
|
||||
|
||||
func PlayStoreVerifyProduct(info PlayStoreReceiptInfo) (bool, int, int) {
|
||||
|
||||
productPurchases, err := PlayStoreClient.VerifyProduct(context.Background(), PlayStorePackageName, info.ProductId, info.PurchaseToken)
|
||||
if err != nil {
|
||||
logger.Println("playstore - PlayStoreVerifyProduct fail :" + err.Error())
|
||||
return false, -1, -1
|
||||
}
|
||||
// fmt.Println(productPurchases)
|
||||
// fmt.Println("================")
|
||||
// fmt.Println(productPurchases.PurchaseState)
|
||||
// fmt.Println(productPurchases.ConsumptionState)
|
||||
// fmt.Println("================")
|
||||
return true, int(productPurchases.PurchaseState), int(productPurchases.ConsumptionState)
|
||||
}
|
||||
|
||||
func PlayStoreConsumeProduct(info PlayStoreReceiptInfo) bool {
|
||||
|
||||
err := PlayStoreClient.ConsumeProduct(context.Background(), PlayStorePackageName, info.ProductId, info.PurchaseToken)
|
||||
if err != nil {
|
||||
logger.Error("playstore - PlayStoreConsumeProduct fail :" + err.Error())
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
var AppStoreClient *api.StoreClient
|
||||
|
||||
func AppStoreInit(keyPath string, configFilePath string) {
|
||||
keyContentBytes, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
return
|
||||
}
|
||||
configBytes, err := os.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
return
|
||||
}
|
||||
|
||||
var configMap map[string]any
|
||||
err = json.Unmarshal(configBytes, &configMap)
|
||||
|
||||
config := &api.StoreConfig{
|
||||
KeyContent: keyContentBytes,
|
||||
KeyID: configMap["KeyID"].(string),
|
||||
BundleID: configMap["BundleID"].(string),
|
||||
Issuer: configMap["Issuer"].(string),
|
||||
Sandbox: configMap["Sandbox"].(bool),
|
||||
}
|
||||
|
||||
AppStoreClient = api.NewStoreClient(config)
|
||||
|
||||
if config.Sandbox {
|
||||
logger.Println("IPA - Appstore : Use Sandbox")
|
||||
} else {
|
||||
logger.Println("IPA - Appstore : Normal")
|
||||
}
|
||||
}
|
||||
|
||||
func AppStoreVerifyReceipt(transactionId string) (bool, *api.JWSTransaction) {
|
||||
res, err := AppStoreClient.GetTransactionInfo(context.Background(), transactionId)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
transaction, err := AppStoreClient.ParseSignedTransaction(res.SignedTransactionInfo)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if transaction.TransactionID != transactionId {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if len(transaction.OriginalTransactionId) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, transaction
|
||||
}
|
||||
|
||||
func AppStoreConsumeProduct(transaction *api.JWSTransaction) bool {
|
||||
//https://developer.apple.com/documentation/appstoreserverapi/consumptionrequest
|
||||
consumeReqBody := api.ConsumptionRequestBody{
|
||||
AccountTenure: 0,
|
||||
AppAccountToken: transaction.AppAccountToken,
|
||||
ConsumptionStatus: 3,
|
||||
CustomerConsented: true,
|
||||
DeliveryStatus: 0,
|
||||
LifetimeDollarsPurchased: 0,
|
||||
LifetimeDollarsRefunded: 0,
|
||||
Platform: 1,
|
||||
PlayTime: 0,
|
||||
SampleContentProvided: false,
|
||||
UserStatus: 1,
|
||||
}
|
||||
statusCode, err := AppStoreClient.SendConsumptionInfo(context.Background(), transaction.OriginalTransactionId, consumeReqBody)
|
||||
|
||||
if statusCode != http.StatusAccepted {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
249
iap/onestore.go
Normal file
249
iap/onestore.go
Normal file
@ -0,0 +1,249 @@
|
||||
package iap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go-ayo/logger"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Ref https://onestore-dev.gitbook.io/dev/tools/tools/v21/06.-api-api-v7#accesstoken
|
||||
const shouldGenerateAccessTokenSec = 600
|
||||
|
||||
type OneStoreIAPVerificationInfo struct {
|
||||
ConsumptionState int `json:"consumptionState"`
|
||||
DeveloperPayload string `json:"developerPayload"`
|
||||
PurchaseState int `json:"purchaseState"`
|
||||
PurchaseTime int64 `json:"purchaseTime"`
|
||||
PurchaseId string `json:"purchaseId"`
|
||||
AcknowledgeState int `json:"acknowledgeState"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
type OneStoreIAPClientPurcharinfo struct {
|
||||
OrderId string `json:"orderId"`
|
||||
PackageName string `json:"packageName"`
|
||||
ProductId string `json:"productId"`
|
||||
PurchaseTime int64 `json:"purchaseTime"`
|
||||
PurchaseId string `json:"purchaseId"`
|
||||
PurchaseToken string `json:"purchaseToken"`
|
||||
DeveloperPayload string `json:"developerPayload"`
|
||||
Quantity int `json:"quantity"`
|
||||
}
|
||||
|
||||
type OneStoreConfig struct {
|
||||
OneClientID string
|
||||
OneClientSecret string
|
||||
}
|
||||
|
||||
type OneStoreTokenData struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type,omitempty"`
|
||||
ClientId string `json:"client_id,omitempty"`
|
||||
ExpiresIn int `json:"expires_in,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
func (s *OnestoreAccessToken) HostURL() string {
|
||||
if s.isSandBox {
|
||||
//-- Sandbox
|
||||
return "https://sbpp.onestore.co.kr"
|
||||
|
||||
} else {
|
||||
//-- RealURL
|
||||
return "https://iap-apis.onestore.net"
|
||||
}
|
||||
}
|
||||
|
||||
func (s *OnestoreAccessToken) GenerateNewToken() error {
|
||||
var newToken OneStoreTokenData
|
||||
|
||||
//==
|
||||
HostURL := s.HostURL()
|
||||
data := []byte("grant_type=client_credentials&client_id=" + s.config.OneClientID + "&client_secret=" + s.config.OneClientSecret)
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("POST", HostURL+"/v7/oauth/token", bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Add("x-market-code", s.marketCode)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
json.Unmarshal(body, &newToken)
|
||||
|
||||
newExpiredSec := (newToken.ExpiresIn - shouldGenerateAccessTokenSec)
|
||||
if newExpiredSec < 0 {
|
||||
newExpiredSec = 0
|
||||
}
|
||||
newToken.Expiry = time.Now().Round(0).Add(time.Duration(newExpiredSec) * time.Second)
|
||||
s.data = &newToken
|
||||
logger.Println("IAP - OneStore : Generate Access Token :", s.data)
|
||||
return nil
|
||||
}
|
||||
|
||||
type OnestoreAccessToken struct {
|
||||
mu sync.Mutex // guards t
|
||||
data *OneStoreTokenData
|
||||
|
||||
marketCode string
|
||||
isSandBox bool
|
||||
config *OneStoreConfig
|
||||
}
|
||||
|
||||
func (t *OnestoreAccessToken) Valid() bool {
|
||||
return t != nil && t.data != nil && t.data.AccessToken != "" && !t.expired()
|
||||
}
|
||||
|
||||
func (t *OnestoreAccessToken) expired() bool {
|
||||
return t.data.Expiry.Before(time.Now())
|
||||
}
|
||||
|
||||
func (s *OnestoreAccessToken) Token() (string, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.Valid() {
|
||||
return s.data.AccessToken, nil
|
||||
}
|
||||
//== Expire 되면 새로 얻어 오자..
|
||||
err := s.GenerateNewToken()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return s.data.AccessToken, nil
|
||||
}
|
||||
|
||||
type OnestoreIAPVerifier struct {
|
||||
config OneStoreConfig
|
||||
SandBox_OnestoreAccessToken_MKT_ONE OnestoreAccessToken
|
||||
SandBox_OnestoreAccessToken_MKT_GLB OnestoreAccessToken
|
||||
Live_OnestoreAccessToken_MKT_ONE OnestoreAccessToken
|
||||
Live_OnestoreAccessToken_MKT_GLB OnestoreAccessToken
|
||||
}
|
||||
|
||||
func (s *OnestoreIAPVerifier) Init(clientId string, clientSecret string) {
|
||||
s.config.OneClientID = clientId
|
||||
s.config.OneClientSecret = clientSecret
|
||||
s.SandBox_OnestoreAccessToken_MKT_ONE.marketCode = "MKT_ONE"
|
||||
s.SandBox_OnestoreAccessToken_MKT_GLB.marketCode = "MKT_GLB"
|
||||
s.Live_OnestoreAccessToken_MKT_ONE.marketCode = "MKT_ONE"
|
||||
s.Live_OnestoreAccessToken_MKT_GLB.marketCode = "MKT_GLB"
|
||||
s.SandBox_OnestoreAccessToken_MKT_ONE.isSandBox = true
|
||||
s.SandBox_OnestoreAccessToken_MKT_GLB.isSandBox = true
|
||||
s.Live_OnestoreAccessToken_MKT_ONE.isSandBox = false
|
||||
s.Live_OnestoreAccessToken_MKT_GLB.isSandBox = false
|
||||
s.SandBox_OnestoreAccessToken_MKT_ONE.config = &s.config
|
||||
s.SandBox_OnestoreAccessToken_MKT_GLB.config = &s.config
|
||||
s.Live_OnestoreAccessToken_MKT_ONE.config = &s.config
|
||||
s.Live_OnestoreAccessToken_MKT_GLB.config = &s.config
|
||||
}
|
||||
|
||||
func (s *OnestoreIAPVerifier) Verify(marketCode string, isSandBox bool, packageName string, clientInfo OneStoreIAPClientPurcharinfo) (bool, error) {
|
||||
var accesstoken string
|
||||
|
||||
if marketCode != "MKT_ONE" && marketCode != "MKT_GLB" {
|
||||
return false, errors.New("marketCode is wrong")
|
||||
}
|
||||
|
||||
if clientInfo.ProductId == "" {
|
||||
return false, errors.New("productId is empty")
|
||||
}
|
||||
|
||||
if clientInfo.PurchaseToken == "" {
|
||||
return false, errors.New("purchaseToken is empty")
|
||||
}
|
||||
|
||||
if packageName != clientInfo.PackageName {
|
||||
return false, errors.New("packageName is wrong")
|
||||
}
|
||||
|
||||
var err error
|
||||
var HostURL string
|
||||
if marketCode == "MKT_ONE" {
|
||||
if isSandBox {
|
||||
accesstoken, err = s.SandBox_OnestoreAccessToken_MKT_ONE.Token()
|
||||
HostURL = s.SandBox_OnestoreAccessToken_MKT_ONE.HostURL()
|
||||
} else {
|
||||
accesstoken, err = s.Live_OnestoreAccessToken_MKT_ONE.Token()
|
||||
HostURL = s.Live_OnestoreAccessToken_MKT_ONE.HostURL()
|
||||
}
|
||||
} else {
|
||||
if isSandBox {
|
||||
accesstoken, err = s.SandBox_OnestoreAccessToken_MKT_GLB.Token()
|
||||
HostURL = s.SandBox_OnestoreAccessToken_MKT_GLB.HostURL()
|
||||
} else {
|
||||
accesstoken, err = s.Live_OnestoreAccessToken_MKT_GLB.Token()
|
||||
HostURL = s.Live_OnestoreAccessToken_MKT_GLB.HostURL()
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", HostURL+"/v7/apps/"+s.config.OneClientID+"/purchases/inapp/products/"+clientInfo.ProductId+"/"+clientInfo.PurchaseToken, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req.Header.Add("Authorization", "Bearer "+accesstoken)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("x-market-code", marketCode)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var verificatioinfo OneStoreIAPVerificationInfo
|
||||
json.Unmarshal(body, &verificatioinfo)
|
||||
|
||||
// fmt.Println("================")
|
||||
// fmt.Println(string(body))
|
||||
// fmt.Println("================")
|
||||
|
||||
if verificatioinfo.PurchaseState != 0 {
|
||||
return false, errors.New("canceled payment")
|
||||
}
|
||||
|
||||
if !strings.EqualFold(clientInfo.DeveloperPayload, verificatioinfo.DeveloperPayload) {
|
||||
return false, errors.New("developerPayload is wrong :" + clientInfo.DeveloperPayload + "/" + verificatioinfo.DeveloperPayload)
|
||||
}
|
||||
|
||||
if clientInfo.PurchaseId != verificatioinfo.PurchaseId {
|
||||
return false, errors.New("purchaseId is wrong")
|
||||
}
|
||||
|
||||
if clientInfo.PurchaseTime != verificatioinfo.PurchaseTime {
|
||||
return false, errors.New("purchaseTime is wrong")
|
||||
}
|
||||
|
||||
if clientInfo.Quantity != verificatioinfo.Quantity {
|
||||
return false, errors.New("quantity is wrong")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user