Initial commit

This commit is contained in:
alexpete
2021-03-05 11:26:34 -08:00
commit a10351f38d
27091 changed files with 5521199 additions and 0 deletions
@@ -0,0 +1,354 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "InAppPurchasesAndroid.h"
#include "InAppPurchasesModule.h"
#include <InAppPurchases/InAppPurchasesResponseBus.h>
#include <AzCore/NativeUI/NativeUIRequests.h>
#include <AzCore/Android/JNI/JNI.h>
#include <AzCore/Android/Utils.h>
#include <AzCore/EBus/BusImpl.h>
#include <AzCore/JSON/document.h>
#include <AzCore/JSON/error/en.h>
#include <AzCore/IO/FileIO.h>
namespace InAppPurchases
{
static bool IsFieldIdValid(jfieldID fid)
{
if (fid == NULL)
{
return false;
}
return true;
}
static PurchasedProductDetailsAndroid* ParseReceiptDetails(JNIEnv* env, jobjectArray jpurchasedProductDetails, int index)
{
jobject jpurchasedProduct = env->GetObjectArrayElement(jpurchasedProductDetails, index);
const int NUM_FIELDS_PURCHASED_PRODUCTS = 7;
jfieldID fid[NUM_FIELDS_PURCHASED_PRODUCTS];
jclass cls = env->GetObjectClass(jpurchasedProduct);
fid[0] = env->GetFieldID(cls, "m_productId", "Ljava/lang/String;");
fid[1] = env->GetFieldID(cls, "m_orderId", "Ljava/lang/String;");
fid[2] = env->GetFieldID(cls, "m_packageName", "Ljava/lang/String;");
fid[3] = env->GetFieldID(cls, "m_purchaseToken", "Ljava/lang/String;");
fid[4] = env->GetFieldID(cls, "m_signature", "Ljava/lang/String;");
fid[5] = env->GetFieldID(cls, "m_purchaseTime", "J");
fid[6] = env->GetFieldID(cls, "m_isAutoRenewing", "Z");
for (int i = 0; i < NUM_FIELDS_PURCHASED_PRODUCTS; i++)
{
if (!IsFieldIdValid(fid[i]))
{
AZ_TracePrintf("LumberyardInAppBilling", "Invaild FieldId in PurchasedProductDetails\n");
return nullptr;
}
}
PurchasedProductDetailsAndroid* purchasedProductDetails = new PurchasedProductDetailsAndroid();
purchasedProductDetails->SetProductId(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jpurchasedProduct, fid[0]))));
purchasedProductDetails->SetOrderId(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jpurchasedProduct, fid[1]))));
purchasedProductDetails->SetPackageName(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jpurchasedProduct, fid[2]))));
purchasedProductDetails->SetPurchaseToken(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jpurchasedProduct, fid[3]))));
purchasedProductDetails->SetPurchaseSignature(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jpurchasedProduct, fid[4]))));
purchasedProductDetails->SetPurchaseTime(env->GetLongField(jpurchasedProduct, fid[5]));
purchasedProductDetails->SetIsAutoRenewing(env->GetBooleanField(jpurchasedProduct, fid[6]));
return purchasedProductDetails;
}
void ProductInfoRetrieved(JNIEnv* env, jobject obj, jobjectArray jproductDetails)
{
int numProducts = env->GetArrayLength(jproductDetails);
InAppPurchasesInterface::GetInstance()->GetCache()->ClearCachedProductDetails();
const int NUM_FIELDS_PRODUCTS = 7;
jfieldID fid[NUM_FIELDS_PRODUCTS];
jclass cls;
if (numProducts > 0)
{
cls = env->GetObjectClass(env->GetObjectArrayElement(jproductDetails, 0));
fid[0] = env->GetFieldID(cls, "m_productId", "Ljava/lang/String;");
fid[1] = env->GetFieldID(cls, "m_type", "Ljava/lang/String;");
fid[2] = env->GetFieldID(cls, "m_price", "Ljava/lang/String;");
fid[3] = env->GetFieldID(cls, "m_currencyCode", "Ljava/lang/String;");
fid[4] = env->GetFieldID(cls, "m_title", "Ljava/lang/String;");
fid[5] = env->GetFieldID(cls, "m_description", "Ljava/lang/String;");
fid[6] = env->GetFieldID(cls, "m_priceMicro", "J");
}
for (int i = 0; i < NUM_FIELDS_PRODUCTS; i++)
{
if (!IsFieldIdValid(fid[i]))
{
AZ_TracePrintf("LumberyardInAppBilling", "Invaild FieldId in ProductDetails\n");
return;
}
}
for (int i = 0; i < numProducts; i++)
{
jobject jproduct = env->GetObjectArrayElement(jproductDetails, i);
ProductDetailsAndroid* productDetails = new ProductDetailsAndroid();
productDetails->SetProductId(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[0]))));
productDetails->SetProductType(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[1]))));
productDetails->SetProductPrice(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[2]))));
productDetails->SetProductCurrencyCode(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[3]))));
productDetails->SetProductTitle(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[4]))));
productDetails->SetProductDescription(AZ::Android::JNI::ConvertJstringToString(static_cast<jstring>(env->GetObjectField(jproduct, fid[5]))));
productDetails->SetProductPriceMicro(env->GetLongField(jproduct, fid[6]));
InAppPurchasesInterface::GetInstance()->GetCache()->AddProductDetailsToCache(productDetails);
}
EBUS_EVENT(InAppPurchasesResponseBus, ProductInfoRetrieved, InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedProductDetails());
}
void PurchasedProductsRetrieved(JNIEnv* env, jobject object, jobjectArray jpurchasedProductDetails)
{
InAppPurchasesInterface::GetInstance()->GetCache()->ClearCachedPurchasedProductDetails();
int numPurchasedProducts = env->GetArrayLength(jpurchasedProductDetails);
for (int i = 0; i < numPurchasedProducts; i++)
{
PurchasedProductDetailsAndroid* purchasedProduct = ParseReceiptDetails(env, jpurchasedProductDetails, i);
if (purchasedProduct != nullptr)
{
InAppPurchasesInterface::GetInstance()->GetCache()->AddPurchasedProductDetailsToCache(purchasedProduct);
}
}
EBUS_EVENT(InAppPurchasesResponseBus, PurchasedProductsRetrieved, InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedPurchasedProductDetails());
}
void NewProductPurchased(JNIEnv* env, jobject object, jobjectArray jpurchaseReceipt)
{
PurchasedProductDetailsAndroid* purchasedProduct = ParseReceiptDetails(env, jpurchaseReceipt, 0);
if (purchasedProduct != nullptr)
{
InAppPurchasesInterface::GetInstance()->GetCache()->AddPurchasedProductDetailsToCache(purchasedProduct);
EBUS_EVENT(InAppPurchasesResponseBus, NewProductPurchased, purchasedProduct);
}
}
void PurchaseConsumed(JNIEnv* env, jobject object, jstring jpurchaseToken)
{
const char* purchaseToken = env->GetStringUTFChars(jpurchaseToken, nullptr);
EBUS_EVENT(InAppPurchasesResponseBus, PurchaseConsumed, AZStd::string(purchaseToken, env->GetStringUTFLength(jpurchaseToken)));
env->ReleaseStringUTFChars(jpurchaseToken, purchaseToken);
}
static JNINativeMethod methods[] = {
{ "nativeProductInfoRetrieved", "([Ljava/lang/Object;)V", (void*)ProductInfoRetrieved },
{ "nativePurchasedProductsRetrieved", "([Ljava/lang/Object;)V", (void*)PurchasedProductsRetrieved },
{ "nativeNewProductPurchased", "([Ljava/lang/Object;)V", (void*)NewProductPurchased },
{ "nativePurchaseConsumed", "(Ljava/lang/String;)V", (void*)PurchaseConsumed }
};
InAppPurchasesInterface* InAppPurchasesInterface::CreateInstance()
{
return new InAppPurchasesAndroid();
}
void InAppPurchasesAndroid::Initialize()
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
jobject activityObject = AZ::Android::Utils::GetActivityRef();
jclass billingClass = AZ::Android::JNI::LoadClass("com/amazon/lumberyard/iap/LumberyardInAppBilling");
jmethodID mid = env->GetMethodID(billingClass, "<init>", "(Landroid/app/Activity;)V");
jobject billingInstance = env->NewObject(billingClass, mid, activityObject);
m_billingInstance = env->NewGlobalRef(billingInstance);
env->RegisterNatives(billingClass, methods, sizeof(methods) / sizeof(methods[0]));
mid = env->GetMethodID(billingClass, "IsKindleDevice", "()Z");
jboolean result = env->CallBooleanMethod(m_billingInstance, mid);
if (result)
{
EBUS_EVENT(AZ::NativeUI::NativeUIRequestBus, DisplayOkDialog, "Kindle Device Detected", "IAP currently unsupported on Kindle devices", false);
}
env->DeleteGlobalRef(billingClass);
env->DeleteLocalRef(billingInstance);
}
InAppPurchasesAndroid::~InAppPurchasesAndroid()
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
jclass billingClass = env->GetObjectClass(m_billingInstance);
jmethodID mid = env->GetMethodID(billingClass, "UnbindService", "()V");
env->CallVoidMethod(m_billingInstance, mid);
env->DeleteLocalRef(billingClass);
env->DeleteGlobalRef(m_billingInstance);
}
void InAppPurchasesAndroid::QueryProductInfo(AZStd::vector<AZStd::string>& productIds) const
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
jobjectArray jproductIds = static_cast<jobjectArray>(env->NewObjectArray(productIds.size(), env->FindClass("java/lang/String"), env->NewStringUTF("")));
for (int i = 0; i < productIds.size(); i++)
{
env->SetObjectArrayElement(jproductIds, i, env->NewStringUTF(productIds[i].c_str()));
}
jclass billingClass = env->GetObjectClass(m_billingInstance);
jmethodID mid = env->GetMethodID(billingClass, "QueryProductInfo", "([Ljava/lang/String;)V");
env->CallVoidMethod(m_billingInstance, mid, jproductIds);
env->DeleteLocalRef(jproductIds);
env->DeleteLocalRef(billingClass);
}
void InAppPurchasesAndroid::QueryProductInfo() const
{
AZ::IO::FileIOBase* fileReader = AZ::IO::FileIOBase::GetInstance();
AZStd::string fileBuffer;
AZ::IO::HandleType fileHandle = AZ::IO::InvalidHandle;
AZ::u64 fileSize = 0;
if (!fileReader->Open("@assets@/product_ids.json", AZ::IO::OpenMode::ModeRead, fileHandle))
{
AZ_TracePrintf("LumberyardInAppBilling", "Unable to open file product_ids.json\n");
return;
}
if ((!fileReader->Size(fileHandle, fileSize)) || (fileSize == 0))
{
AZ_TracePrintf("LumberyardInAppBilling", "Unable to read file product_ids.json - file truncated\n");
fileReader->Close(fileHandle);
return;
}
fileBuffer.resize(fileSize);
if (!fileReader->Read(fileHandle, fileBuffer.data(), fileSize, true))
{
fileBuffer.resize(0);
fileReader->Close(fileHandle);
AZ_TracePrintf("LumberyardInAppBilling", "Failed to read file product_ids.json\n");
return;
}
fileReader->Close(fileHandle);
rapidjson::Document document;
document.Parse(fileBuffer.data());
if (document.HasParseError())
{
const char* errorStr = rapidjson::GetParseError_En(document.GetParseError());
AZ_TracePrintf("LumberyardInAppBilling", "Failed to parse product_ids.json: %s\n", errorStr);
return;
}
const rapidjson::Value& productList = document["product_ids"];
const auto& end = productList.End();
AZStd::vector<AZStd::string> productIds;
for (auto it = productList.Begin(); it != end; it++)
{
const auto& elem = *it;
productIds.push_back(elem["id"].GetString());
}
QueryProductInfo(productIds);
}
void InAppPurchasesAndroid::PurchaseProduct(const AZStd::string& productId, const AZStd::string& developerPayload) const
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
const AZStd::vector <AZStd::unique_ptr<ProductDetails const> >& cachedProductDetails = InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedProductDetails();
AZStd::string productType = "";
for (int i = 0; i < cachedProductDetails.size(); i++)
{
const ProductDetailsAndroid* productDetails = azrtti_cast<const ProductDetailsAndroid*>(cachedProductDetails[i].get());
const AZStd::string& cachedProductId = productDetails->GetProductId();
if (cachedProductId.compare(productId) == 0)
{
productType = productDetails->GetProductType();
break;
}
}
if (productType.empty())
{
AZ_TracePrintf("LumberyardInAppBilling", "Failed to find product with id: %s", productId.c_str());
return;
}
jstring jproductId = env->NewStringUTF(productId.c_str());
jstring jdeveloperPayload = env->NewStringUTF(developerPayload.c_str());
jstring jproductType = env->NewStringUTF(productType.c_str());
jclass billingClass = env->GetObjectClass(m_billingInstance);
jmethodID mid = env->GetMethodID(billingClass, "PurchaseProduct", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
env->CallVoidMethod(m_billingInstance, mid, jproductId, jdeveloperPayload, jproductType);
env->DeleteLocalRef(jproductId);
env->DeleteLocalRef(jdeveloperPayload);
env->DeleteLocalRef(jproductType);
env->DeleteLocalRef(billingClass);
}
void InAppPurchasesAndroid::PurchaseProduct(const AZStd::string& productId) const
{
PurchaseProduct(productId, "");
}
void InAppPurchasesAndroid::QueryPurchasedProducts() const
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
jclass billingClass = env->GetObjectClass(m_billingInstance);
jmethodID mid = env->GetMethodID(billingClass, "QueryPurchasedProducts", "()V");
env->CallVoidMethod(m_billingInstance, mid);
env->DeleteLocalRef(billingClass);
}
void InAppPurchasesAndroid::RestorePurchasedProducts() const
{
}
void InAppPurchasesAndroid::ConsumePurchase(const AZStd::string& purchaseToken) const
{
JNIEnv* env = AZ::Android::JNI::GetEnv();
jclass billingClass = env->GetObjectClass(m_billingInstance);
jmethodID mid = env->GetMethodID(billingClass, "ConsumePurchase", "(Ljava/lang/String;)V");
jstring jpurchaseToken = env->NewStringUTF(purchaseToken.c_str());
env->CallVoidMethod(m_billingInstance, mid, jpurchaseToken);
env->DeleteLocalRef(jpurchaseToken);
env->DeleteLocalRef(billingClass);
}
void InAppPurchasesAndroid::FinishTransaction(const AZStd::string& transactionId, bool downloadHostedContent) const
{
}
InAppPurchasesCache* InAppPurchasesAndroid::GetCache()
{
return &m_cache;
}
}
@@ -0,0 +1,64 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <InAppPurchases/InAppPurchasesInterface.h>
#include <jni.h>
namespace InAppPurchases
{
class InAppPurchasesAndroid
: public InAppPurchasesInterface
{
public:
void Initialize() override;
~InAppPurchasesAndroid();
void QueryProductInfo(AZStd::vector<AZStd::string>& productIds) const override;
void QueryProductInfo() const override;
void PurchaseProduct(const AZStd::string& productId, const AZStd::string& developerPayload) const override;
void PurchaseProduct(const AZStd::string& productId) const override;
void QueryPurchasedProducts() const override;
void RestorePurchasedProducts() const override;
void ConsumePurchase(const AZStd::string& purchaseToken) const override;
void FinishTransaction(const AZStd::string& transactionId, bool downloadHostedContent) const override;
InAppPurchasesCache* GetCache() override;
private:
jobject m_billingInstance;
};
class ProductDetailsAndroid
: public ProductDetails
{
public:
AZ_RTTI(ProductDetailsAndroid, "{59A14DA4-B224-4BBD-B43E-8C7BC2EEFEB5}", ProductDetails);
const AZStd::string& GetProductType() const { return m_productType; }
void SetProductType(const AZStd::string& productType) { m_productType = productType; }
protected:
AZStd::string m_productType;
};
}
@@ -0,0 +1,365 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
package com.amazon.lumberyard.iap;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.Purchase;
import com.android.billingclient.api.PurchasesUpdatedListener;
import com.android.billingclient.api.SkuDetails;
import com.android.billingclient.api.SkuDetailsParams;
import com.android.billingclient.api.SkuDetailsResponseListener;
public class LumberyardInAppBilling implements PurchasesUpdatedListener
{
public LumberyardInAppBilling(Activity activity)
{
m_activity = activity;
m_packageName = m_activity.getPackageName();
Resources resources = m_activity.getResources();
int stringId = resources.getIdentifier("public_key", "string", m_activity.getPackageName());
m_appPublicKey = resources.getString(stringId);
m_setupDone = false;
final LumberyardInAppBilling iapInstance = this;
if (!IsKindleDevice())
{
(new Thread(new Runnable()
{
public void run()
{
Looper.prepare();
m_billingClient = BillingClient.newBuilder(m_activity).setListener(iapInstance).build();
m_billingClient.startConnection(new BillingClientStateListener()
{
@Override
public void onBillingSetupFinished(int responseCode)
{
Log.d(s_tag, "Service connected");
if (!m_billingClient.isReady())
{
Log.d(s_tag, m_packageName + " IN_APP items not supported!");
return;
}
int subscriptionsResponseCode = m_billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS);
if (!VerifyResponseCode(subscriptionsResponseCode))
{
return;
}
subscriptionsResponseCode = m_billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS_UPDATE);
if (!VerifyResponseCode(subscriptionsResponseCode))
{
return;
}
m_setupDone = true;
}
@Override
public void onBillingServiceDisconnected()
{
Log.d(s_tag, "Service disconnected");
}
});
Looper.loop();
}
})).start();
}
Log.d(s_tag, "Instance created");
}
public static native void nativeProductInfoRetrieved(Object[] productDetails);
public static native void nativePurchasedProductsRetrieved(Object[] purchasedProductDetails);
public static native void nativeNewProductPurchased(Object[] purchaseReceipt);
public static native void nativePurchaseConsumed(String purchaseToken);
public void UnbindService()
{
if (!m_setupDone)
{
Log.e(s_tag, "Not initialized!");
return;
}
m_setupDone = false;
m_billingClient.endConnection();
m_billingClient = null;
}
public void QueryProductInfo(final String[] skuListArray)
{
if (!m_setupDone)
{
Log.e(s_tag, "Not initialized!");
return;
}
m_numResponses = 0;
final ArrayList<ProductDetails> responseList = new ArrayList<>();
List<String> skuList = Arrays.asList(skuListArray);
SkuDetailsResponseListener responseListener = new SkuDetailsResponseListener()
{
@Override
public void onSkuDetailsResponse(int responseCode, List<SkuDetails> skuDetailsList)
{
m_numResponses++;
if (!VerifyResponseCode(responseCode))
{
return;
}
for (SkuDetails skuDetails : skuDetailsList)
{
ProductDetails productDetails = new ProductDetails();
productDetails.m_productId = skuDetails.getSku();
productDetails.m_title = skuDetails.getTitle();
productDetails.m_description = skuDetails.getDescription();
productDetails.m_price = skuDetails.getPrice();
productDetails.m_currencyCode = skuDetails.getPriceCurrencyCode();
productDetails.m_type = skuDetails.getType();
productDetails.m_priceMicro = skuDetails.getPriceAmountMicros();
responseList.add(productDetails);
}
// Wait for responses for both subscriptions and regular products
if (m_numResponses == 2)
{
nativeProductInfoRetrieved(responseList.toArray());
}
}
};
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(BillingClient.SkuType.INAPP);
m_billingClient.querySkuDetailsAsync(params.build(), responseListener);
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS);
m_billingClient.querySkuDetailsAsync(params.build(), responseListener);
}
public void PurchaseProduct(String productSku, String developerPayload, String productType)
{
if (!m_setupDone)
{
Log.e(s_tag, "Not initialized!");
return;
}
BillingFlowParams flowParams = BillingFlowParams.newBuilder().setSku(productSku).setType(productType).build();
int responseCode = m_billingClient.launchBillingFlow(m_activity, flowParams);
if (!VerifyResponseCode(responseCode))
{
return;
}
Log.d(s_tag, "Purchase flow initiated.");
}
@Override
public void onPurchasesUpdated(int responseCode, List<Purchase> purchases)
{
if (!VerifyResponseCode(responseCode))
{
return;
}
ArrayList<PurchasedProductDetails> purchasedProducts = new ArrayList<>();
ParsePurchasedProducts(purchases, purchasedProducts);
nativeNewProductPurchased(purchasedProducts.toArray());
}
public void QueryPurchasedProducts()
{
if (!m_setupDone)
{
Log.e(s_tag, "Not initialized!");
return;
}
ArrayList<PurchasedProductDetails> purchasedProducts = new ArrayList<>();
if (!QueryPurchasedProductsBySkuType(BillingClient.SkuType.INAPP, purchasedProducts) || !QueryPurchasedProductsBySkuType(BillingClient.SkuType.SUBS, purchasedProducts))
{
return;
}
nativePurchasedProductsRetrieved(purchasedProducts.toArray());
}
public void ConsumePurchase(final String purchaseToken)
{
if (!m_setupDone)
{
Log.e(s_tag, "Not initialized!");
return;
}
ConsumeResponseListener listener = new ConsumeResponseListener()
{
@Override
public void onConsumeResponse(int responseCode, String purchaseToken)
{
if (!VerifyResponseCode(responseCode))
{
return;
}
nativePurchaseConsumed(purchaseToken);
}
};
m_billingClient.consumeAsync(purchaseToken, listener);
}
public boolean IsKindleDevice()
{
if (Build.MANUFACTURER.equals("Amazon") && Build.MODEL.contains("KF"))
{
Log.e(s_tag, "Kindle devices not currently supported");
return true;
}
return false;
}
private boolean QueryPurchasedProductsBySkuType(String skuType, ArrayList<PurchasedProductDetails> purchasedProducts)
{
Purchase.PurchasesResult purchasesResult = m_billingClient.queryPurchases(skuType);
if (!VerifyResponseCode(purchasesResult.getResponseCode()))
{
return false;
}
ParsePurchasedProducts(purchasesResult.getPurchasesList(), purchasedProducts);
return true;
}
private void ParsePurchasedProducts(List<Purchase> purchases, ArrayList<PurchasedProductDetails> purchasedProducts)
{
for (Purchase purchase : purchases)
{
PurchasedProductDetails purchasedProductDetails = new PurchasedProductDetails();
purchasedProductDetails.m_productId = purchase.getSku();
purchasedProductDetails.m_orderId = purchase.getOrderId();
purchasedProductDetails.m_packageName = purchase.getPackageName();
purchasedProductDetails.m_purchaseToken = purchase.getPurchaseToken();
purchasedProductDetails.m_signature = purchase.getSignature();
purchasedProductDetails.m_purchaseTime = purchase.getPurchaseTime();
purchasedProductDetails.m_isAutoRenewing = purchase.isAutoRenewing();
purchasedProducts.add(purchasedProductDetails);
}
}
private boolean VerifyResponseCode(int responseCode)
{
if (responseCode != BillingClient.BillingResponse.OK)
{
Log.d(s_tag, m_packageName + " returned error code " + BILLING_RESPONSE_RESULT_STRINGS[responseCode]);
return false;
}
return true;
}
public class ProductDetails
{
public String m_title;
public String m_description;
public String m_productId;
public String m_price;
public String m_currencyCode;
public String m_type;
public long m_priceMicro;
};
public class PurchasedProductDetails
{
public String m_productId;
public String m_orderId;
public String m_packageName;
public String m_purchaseToken;
public String m_signature;
public long m_purchaseTime;
public boolean m_isAutoRenewing;
};
private static final String[] BILLING_RESPONSE_RESULT_STRINGS = {
"Billing response result is ok",
"User cancelled the request",
"The requested service is unavailable",
"Billing is currently unavailable",
"The requested item is unavailable",
"Developer error",
"Error",
"The item being purchased is already owned by the user",
"The item is not owned by the user"
};
private static final String s_tag = "LumberyardInAppBilling";
private Activity m_activity;
private BillingClient m_billingClient;
private String m_appPublicKey;
private String m_packageName;
private boolean m_setupDone;
private int m_numResponses;
}
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,15 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
InAppPurchasesAndroid.h
InAppPurchasesAndroid.cpp
)
@@ -0,0 +1,56 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#include <InAppPurchases/InAppPurchasesInterface.h>
#include "InAppPurchasesDelegate.h"
namespace InAppPurchases
{
class InAppPurchasesApple
: public InAppPurchasesInterface
{
public:
void Initialize() override;
~InAppPurchasesApple();
void QueryProductInfo(AZStd::vector<AZStd::string>& productIds) const override;
void QueryProductInfo() const override;
void PurchaseProduct(const AZStd::string& productId, const AZStd::string& developerPayload) const override;
void PurchaseProduct(const AZStd::string& productId) const override;
void QueryPurchasedProducts() const override;
void RestorePurchasedProducts() const override;
void ConsumePurchase(const AZStd::string& purchaseToken) const override;
void FinishTransaction(const AZStd::string& transactionId, bool downloadHostedContent) const override;
InAppPurchasesCache* GetCache() override;
private:
InAppPurchasesDelegate* m_delegate;
};
class ProductDetailsApple
: public ProductDetails
{
public:
AZ_RTTI(ProductDetailsApple, "{AAF5C20F-482A-45BC-B975-F5864B4C00C5}", ProductDetails);
};
}
@@ -0,0 +1,264 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "InAppPurchasesApple.h"
#include "InAppPurchasesModule.h"
#include <InAppPurchases/InAppPurchasesResponseBus.h>
#include <AzCore/JSON/document.h>
#include <openssl/pkcs7.h>
#include <openssl/objects.h>
#include <Payload.h>
namespace
{
enum ReceiptAttributeTypes
{
InAppPurchaseReceipt = 17,
ProductId = 1702,
RestoredTransactionId = 1703,
RestoredPurchaseTime = 1704,
OriginalTransactionId = 1705,
OriginalPurchaseTime = 1706,
SubscriptionExpirationDate = 1708,
};
bool ParseReceipt(void* encodedPayload, size_t size, InAppPurchases::PurchasedProductDetailsApple* purchasedProductDetails = nullptr)
{
Payload_t* payload = nullptr;
asn_DEF_Payload.ber_decoder(nullptr, &asn_DEF_Payload, (void**)&payload, encodedPayload, size, 0);
if (!payload)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Payload is null!");
return false;
}
for (int i = 0; i < payload->list.count; i++)
{
ReceiptAttribute_t* attrib = payload->list.array[i];
switch (attrib->type)
{
case ReceiptAttributeTypes::InAppPurchaseReceipt:
{
ParseReceipt(attrib->value.buf, attrib->value.size, new InAppPurchases::PurchasedProductDetailsApple());
break;
}
case ReceiptAttributeTypes::ProductId:
{
purchasedProductDetails->SetProductId(AZStd::string(reinterpret_cast<const char*>(attrib->value.buf)).substr(2));
break;
}
case ReceiptAttributeTypes::RestoredTransactionId:
{
purchasedProductDetails->SetRestoredOrderId(AZStd::string(reinterpret_cast<const char*>(attrib->value.buf)).substr(2));
break;
}
case ReceiptAttributeTypes::OriginalTransactionId:
{
purchasedProductDetails->SetOrderId(AZStd::string(reinterpret_cast<const char*>(attrib->value.buf)).substr(2));
break;
}
case ReceiptAttributeTypes::RestoredPurchaseTime:
case ReceiptAttributeTypes::OriginalPurchaseTime:
{
AZStd::string dateString = AZStd::string(reinterpret_cast<const char*>(attrib->value.buf)).substr(2);
NSString* dateNSString = [NSString stringWithCString:dateString.c_str() encoding:NSUTF8StringEncoding];
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
if (attrib->type == ReceiptAttributeTypes::RestoredPurchaseTime)
{
purchasedProductDetails->SetRestoredPurchaseTime([[formatter dateFromString:dateNSString] timeIntervalSince1970]);
}
else
{
purchasedProductDetails->SetPurchaseTime([[formatter dateFromString:dateNSString] timeIntervalSince1970]);
}
break;
}
case ReceiptAttributeTypes::SubscriptionExpirationDate:
{
AZStd::string expiration = AZStd::string(reinterpret_cast<const char*>(attrib->value.buf));
// Products that are not subscriptions still have escape sequences. So we can't check for empty string.
if (expiration.size() <= 2)
{
purchasedProductDetails->SetSubscriptionExpirationTime(0);
}
else
{
NSString* expirationString = [NSString stringWithCString:expiration.substr(2).c_str() encoding:NSUTF8StringEncoding];
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
[formatter setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
purchasedProductDetails->SetSubscriptionExpirationTime([[formatter dateFromString:expirationString] timeIntervalSince1970]);
}
break;
}
default:
break;
}
}
if (purchasedProductDetails != nullptr)
{
purchasedProductDetails->SetDeveloperPayload("");
purchasedProductDetails->SetPurchaseState(InAppPurchases::PurchaseState::PURCHASED);
InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->AddPurchasedProductDetailsToCache(purchasedProductDetails);
}
return true;
}
}
namespace InAppPurchases
{
InAppPurchasesInterface* InAppPurchasesInterface::CreateInstance()
{
return new InAppPurchasesApple();
}
void InAppPurchasesApple::Initialize()
{
m_delegate = [[InAppPurchasesDelegate alloc] init];
[m_delegate initialize];
}
InAppPurchasesApple::~InAppPurchasesApple()
{
[m_delegate deinitialize];
[m_delegate release];
}
void InAppPurchasesApple::QueryProductInfo(AZStd::vector<AZStd::string>& productIds) const
{
NSMutableArray* productIdStrings = [[NSMutableArray alloc] init];
for (int i = 0; i < productIds.size(); i++)
{
NSString* productId = [NSString stringWithCString:productIds[i].c_str() encoding:NSUTF8StringEncoding];
[productIdStrings addObject:productId];
}
[m_delegate requestProducts:productIdStrings];
[productIdStrings release];
}
void InAppPurchasesApple::QueryProductInfo() const
{
NSURL* url = [[NSBundle mainBundle] URLForResource:@"product_ids" withExtension:@"plist"];
if (url != nil)
{
NSMutableArray* productIds = [NSMutableArray arrayWithContentsOfURL:url];
if (productIds != nil)
{
[m_delegate requestProducts:productIds];
}
else
{
AZ_TracePrintf("LumberyardInAppPurchases", "Unable to find any product ids in product_ids.plist");
}
}
else
{
AZ_TracePrintf("LumberyardInAppPurchases", "product_ids.plist does not exist");
}
}
void InAppPurchasesApple::PurchaseProduct(const AZStd::string& productId, const AZStd::string& developerPayload) const
{
NSString* productIdString = [NSString stringWithCString:productId.c_str() encoding:NSUTF8StringEncoding];
if (!developerPayload.empty())
{
NSString* developerPayloadString = [NSString stringWithCString:developerPayload.c_str() encoding:NSUTF8StringEncoding];
[m_delegate purchaseProduct:productIdString withUserName:developerPayloadString];
}
else
{
[m_delegate purchaseProduct:productIdString withUserName:nil];
}
}
void InAppPurchasesApple::PurchaseProduct(const AZStd::string& productId) const
{
PurchaseProduct(productId, "");
}
void InAppPurchasesApple::RestorePurchasedProducts() const
{
InAppPurchasesInterface::GetInstance()->GetCache()->ClearCachedPurchasedProductDetails();
[m_delegate restorePurchasedProducts];
}
void InAppPurchasesApple::QueryPurchasedProducts() const
{
[m_delegate refreshAppReceipt];
InAppPurchasesInterface::GetInstance()->GetCache()->ClearCachedPurchasedProductDetails();
NSURL* url = [[NSBundle mainBundle] appStoreReceiptURL];
const char* receiptPath = [[url.absoluteString substringFromIndex:7] cStringUsingEncoding:NSASCIIStringEncoding];
FILE* fp = fopen(receiptPath, "rb");
if (!fp)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Unable to open receipt!");
return;
}
PKCS7* p7 = d2i_PKCS7_fp(fp, nullptr);
fclose(fp);
if (!p7)
{
AZ_TracePrintf("LumberyardInAppPurchases", "PKCS7 container is null!");
return;
}
if (PKCS7_type_is_data(p7->d.sign->contents))
{
if (ParseReceipt(p7->d.sign->contents->d.data->data, p7->d.sign->contents->d.data->length))
{
EBUS_EVENT(InAppPurchasesResponseBus, PurchasedProductsRetrieved, InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedPurchasedProductDetails());
}
}
}
void InAppPurchasesApple::ConsumePurchase(const AZStd::string& purchaseToken) const
{
}
void InAppPurchasesApple::FinishTransaction(const AZStd::string& transactionId, bool downloadHostedContent) const
{
NSString* transactionIdString = [NSString stringWithCString:transactionId.c_str() encoding:NSASCIIStringEncoding];
if (downloadHostedContent)
{
[m_delegate downloadAppleHostedContentAndFinishTransaction:transactionIdString];
}
else
{
[m_delegate finishTransaction:transactionIdString];
}
}
InAppPurchasesCache* InAppPurchasesApple::GetCache()
{
return &m_cache;
}
}
@@ -0,0 +1,31 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#pragma once
#import <StoreKit/StoreKit.h>
@interface InAppPurchasesDelegate : NSObject<SKProductsRequestDelegate, SKPaymentTransactionObserver>
@property (strong, nonatomic) SKProductsRequest* m_productsRequest;
@property (strong, nonatomic) NSMutableArray* m_products;
@property (strong, nonatomic) NSMutableArray* m_unfinishedTransactions;
@property (strong, nonatomic) NSMutableArray* m_unfinishedDownloads;
@property (strong, nonatomic) SKReceiptRefreshRequest* m_receiptRefreshRequest;
-(void) requestProducts:(NSMutableArray*) productIds;
-(void) purchaseProduct:(NSString*) productId withUserName:(NSString*) userName;
-(void) finishTransaction:(NSString*) transactionId;
-(void) downloadAppleHostedContentAndFinishTransaction:(NSString*) transactionId;
-(void) restorePurchasedProducts;
-(void) refreshAppReceipt;
-(void) initialize;
-(void) deinitialize;
@end
@@ -0,0 +1,417 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "InAppPurchasesDelegate.h"
#include "InAppPurchasesApple.h"
#include <InAppPurchases/InAppPurchasesInterface.h>
#include <InAppPurchases/InAppPurchasesResponseBus.h>
#include <CommonCrypto/CommonCrypto.h>
#include <AzCore/std/string/conversions.h>
@implementation InAppPurchasesDelegate
-(NSString*) convertPriceToString:(NSDecimalNumber*) price forLocale:(NSLocale*) locale
{
NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
[numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
[numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
[numberFormatter setLocale:locale];
NSString* formattedPrice = [numberFormatter stringFromNumber:price];
[numberFormatter release];
return formattedPrice;
}
-(InAppPurchases::PurchasedProductDetailsApple*) parseTransactionDetails:(SKPaymentTransaction*) transaction isRestored:(bool) restored
{
InAppPurchases::PurchasedProductDetailsApple* purchasedProductDetails = new InAppPurchases::PurchasedProductDetailsApple();
SKPayment* payment = transaction.payment;
purchasedProductDetails->SetProductId([payment.productIdentifier UTF8String]);
if ([payment.applicationUsername length] > 0)
{
purchasedProductDetails->SetDeveloperPayload([payment.applicationUsername UTF8String]);
}
else
{
purchasedProductDetails->SetDeveloperPayload("");
}
if (restored)
{
purchasedProductDetails->SetRestoredOrderId([transaction.transactionIdentifier cStringUsingEncoding:NSASCIIStringEncoding]);
purchasedProductDetails->SetOrderId([transaction.originalTransaction.transactionIdentifier cStringUsingEncoding:NSASCIIStringEncoding]);
purchasedProductDetails->SetPurchaseTime([transaction.originalTransaction.transactionDate timeIntervalSince1970]);
purchasedProductDetails->SetRestoredPurchaseTime([transaction.transactionDate timeIntervalSince1970]);
}
else
{
purchasedProductDetails->SetOrderId([transaction.transactionIdentifier cStringUsingEncoding:NSASCIIStringEncoding]);
purchasedProductDetails->SetPurchaseTime([transaction.transactionDate timeIntervalSince1970]);
purchasedProductDetails->SetRestoredOrderId("");
purchasedProductDetails->SetRestoredPurchaseTime(0);
}
purchasedProductDetails->SetHasDownloads((transaction.downloads != nil) ? true:false);
return purchasedProductDetails;
}
-(NSString*) hashUserName:(NSString*) userName
{
const int HASH_SIZE = 32;
unsigned char hashedChars[HASH_SIZE];
const char* userNameString = [userName UTF8String];
size_t userNameLength = strlen(userNameString);
if (userNameLength > UINT32_MAX)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Username too long to hash:%s", [userName cStringUsingEncoding:NSASCIIStringEncoding]);
return nil;
}
CC_SHA256(userNameString, (CC_LONG)userNameLength, hashedChars);
NSMutableString* userNameHash = [[NSMutableString alloc] init];
for (int i =0; i < HASH_SIZE; i++)
{
if (i != 0 && i % 4 == 0)
{
[userNameHash appendString:@"-"];
}
[userNameHash appendFormat:@"%02x", hashedChars[i]];
}
return userNameHash;
}
-(void) initialize
{
self.m_products = [[NSMutableArray alloc] init];
self.m_unfinishedTransactions = [[NSMutableArray alloc] init];
self.m_unfinishedDownloads = [[NSMutableArray alloc] init];
[[SKPaymentQueue defaultQueue] addTransactionObserver:self];
}
-(void) deinitialize
{
[self.m_products removeAllObjects];
[self.m_products release];
[self.m_unfinishedTransactions removeAllObjects];
[self.m_unfinishedTransactions release];
[self.m_unfinishedDownloads removeAllObjects];
[self.m_unfinishedDownloads release];
if (self.m_productsRequest != nil)
{
[self.m_productsRequest release];
}
if (self.m_receiptRefreshRequest != nil)
{
[self.m_receiptRefreshRequest release];
}
}
-(void) requestProducts:(NSMutableArray*) productIds
{
self.m_productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:productIds]];
self.m_productsRequest.delegate = self;
[self.m_productsRequest start];
}
-(void) productsRequest:(SKProductsRequest*) request didReceiveResponse:(SKProductsResponse*) response
{
for (NSString* invalidId in response.invalidProductIdentifiers)
{
AZ_TracePrintf("LumberyardInAppPurchases:", "Invalid product ID:", [invalidId cStringUsingEncoding:NSASCIIStringEncoding]);
}
InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->ClearCachedProductDetails();
[self.m_products removeAllObjects];
for (SKProduct* product in response.products)
{
InAppPurchases::ProductDetailsApple* productDetails = new InAppPurchases::ProductDetailsApple;
productDetails->SetProductId([product.productIdentifier UTF8String]);
productDetails->SetProductTitle([product.localizedTitle UTF8String]);
productDetails->SetProductDescription([product.localizedDescription UTF8String]);
productDetails->SetProductPrice([[self convertPriceToString:product.price forLocale:product.priceLocale] UTF8String]);
productDetails->SetProductCurrencyCode([[product.priceLocale objectForKey:NSLocaleCurrencyCode] UTF8String]);
AZStd::string priceMicro = [[NSString stringWithFormat:@"%@", product.price] UTF8String];
productDetails->SetProductPriceMicro((AZStd::stof(priceMicro) * 1000000));
InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->AddProductDetailsToCache(productDetails);
[self.m_products addObject:product];
}
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, ProductInfoRetrieved, InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedProductDetails());
[self.m_productsRequest release];
self.m_productsRequest = nil;
}
-(void) purchaseProduct:(NSString *) productId withUserName:(NSString *) userName
{
SKProduct* productToPurchase = nil;
for (SKProduct* product in self.m_products)
{
if ([product.productIdentifier isEqualToString:productId])
{
productToPurchase = product;
break;
}
}
if (productToPurchase != nil)
{
SKMutablePayment* payment = [SKMutablePayment paymentWithProduct:productToPurchase];
payment.quantity = 1;
if ([userName length] > 0)
{
NSString* userNameHash = [self hashUserName:userName];
payment.applicationUsername = userNameHash;
}
[[SKPaymentQueue defaultQueue] addPayment:payment];
}
else
{
AZ_TracePrintf("LumberyardInAppPurchases", "Invalid product ID:%s", [productId cStringUsingEncoding:NSASCIIStringEncoding]);
}
}
-(void) paymentQueue:(SKPaymentQueue*) queue updatedTransactions:(nonnull NSArray*) transactions
{
bool isRestoredTransaction = false;
for (SKPaymentTransaction* transaction in transactions)
{
switch (transaction.transactionState)
{
case SKPaymentTransactionStatePurchasing:
{
AZ_TracePrintf("LumberyardInAppPurchases", "Transaction in progress");
break;
}
case SKPaymentTransactionStateDeferred:
{
AZ_TracePrintf("LumberyardInAppPurchases", "Transaction deferred");
break;
}
case SKPaymentTransactionStateFailed:
{
if ([self.m_unfinishedTransactions containsObject:transaction] == false)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Transaction failed! Error: %s", [[transaction.error localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
InAppPurchases::PurchasedProductDetailsApple* productDetails = [self parseTransactionDetails:transaction isRestored:false];
productDetails->SetPurchaseState(InAppPurchases::PurchaseState::FAILED);
[self.m_unfinishedTransactions addObject:transaction];
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, PurchaseFailed, productDetails);
delete productDetails;
}
}
break;
case SKPaymentTransactionStatePurchased:
{
if ([self.m_unfinishedTransactions containsObject:transaction] == false)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Transaction succeeded");
InAppPurchases::PurchasedProductDetailsApple* productDetails = [self parseTransactionDetails:transaction isRestored:false];
productDetails->SetPurchaseState(InAppPurchases::PurchaseState::PURCHASED);
InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->AddPurchasedProductDetailsToCache(productDetails);
[self.m_unfinishedTransactions addObject:transaction];
for (SKDownload* download in transaction.downloads)
{
if ([self.m_unfinishedDownloads containsObject:download.contentIdentifier] == false)
{
[self.m_unfinishedDownloads addObject:download.contentIdentifier];
}
}
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, NewProductPurchased, productDetails);
}
}
break;
case SKPaymentTransactionStateRestored:
{
if ([self.m_unfinishedTransactions containsObject:transaction] == false)
{
AZ_TracePrintf("LumberyardInAppPurchases", "Transaction restored");
InAppPurchases::PurchasedProductDetailsApple* productDetails = [self parseTransactionDetails:transaction isRestored:true];
productDetails->SetPurchaseState(InAppPurchases::PurchaseState::RESTORED);
InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->AddPurchasedProductDetailsToCache(productDetails);
[self.m_unfinishedTransactions addObject:transaction];
for (SKDownload* download in transaction.downloads)
{
if ([self.m_unfinishedDownloads containsObject:download.contentIdentifier] == false)
{
[self.m_unfinishedDownloads addObject:download.contentIdentifier];
}
}
isRestoredTransaction = true;
}
}
break;
}
}
if (isRestoredTransaction)
{
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, PurchasedProductsRestored, InAppPurchases::InAppPurchasesInterface::GetInstance()->GetCache()->GetCachedPurchasedProductDetails());
}
}
-(void) finishTransaction:(NSString*) transactionId
{
for (SKPaymentTransaction* transaction in self.m_unfinishedTransactions)
{
if ([transaction.transactionIdentifier isEqualToString:transactionId])
{
[[SKPaymentQueue defaultQueue] finishTransaction:transaction];
[self.m_unfinishedTransactions removeObject:transaction];
return;
}
}
AZ_TracePrintf("LumberyardInAppPurchases", "No unfinished transaction found with ID: %s", [transactionId cStringUsingEncoding:NSASCIIStringEncoding]);
}
-(void) downloadAppleHostedContentAndFinishTransaction:(NSString*) transactionId
{
SKPaymentTransaction* requiredTransaction = nil;
for (SKPaymentTransaction* transaction in self.m_unfinishedTransactions)
{
if ([transaction.transactionIdentifier isEqualToString:transactionId])
{
requiredTransaction = transaction;
break;
}
}
if (requiredTransaction != nil)
{
if (requiredTransaction.downloads != nil)
{
[[SKPaymentQueue defaultQueue] startDownloads:requiredTransaction.downloads];
}
else
{
[self finishTransaction:requiredTransaction.transactionIdentifier];
}
}
else
{
AZ_TracePrintf("LumberyardInAppPurchases", "No unfinished transaction found with ID: %s", [transactionId cStringUsingEncoding:NSASCIIStringEncoding]);
}
}
-(void) paymentQueue:(SKPaymentQueue*) queue updatedDownloads:(NSArray<SKDownload*>*) downloads
{
for (SKDownload* download in downloads)
{
bool wasDownloadHandled = true;
for (NSString* contentIdentifier in self.m_unfinishedDownloads)
{
if ([contentIdentifier isEqualToString:download.contentIdentifier])
{
wasDownloadHandled = false;
break;
}
}
if (wasDownloadHandled)
{
continue;
}
#if (defined(TARGET_OS_OSX) && TARGET_OS_OSX) || (defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_12_0)
switch(download.state)
#else
switch(download.downloadState)
#endif
{
case SKDownloadStateFinished:
{
AZStd::string pathToDownloadedContent = [download.contentURL.absoluteString cStringUsingEncoding:NSASCIIStringEncoding];
AZStd::string transactionId = [download.transaction.transactionIdentifier cStringUsingEncoding:NSASCIIStringEncoding];
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, HostedContentDownloadComplete, transactionId, pathToDownloadedContent);
[self.m_unfinishedDownloads removeObject:download.contentIdentifier];
}
break;
case SKDownloadStateFailed:
{
AZ_TracePrintf("LumberyardInAppPurchases", "Download failed with error: %s", [[download.error localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
AZStd::string transactionId = [download.transaction.transactionIdentifier cStringUsingEncoding:NSASCIIStringEncoding];
AZStd::string contentId = [download.contentIdentifier UTF8String];
EBUS_EVENT(InAppPurchases::InAppPurchasesResponseBus, HostedContentDownloadFailed, transactionId, contentId);
[self.m_unfinishedDownloads removeObject:download.contentIdentifier];
}
break;
case SKDownloadStateCancelled:
{
AZ_TracePrintf("LumberyardInAppPurchases", "Download cancelled: %s", [download.contentIdentifier cStringUsingEncoding:NSASCIIStringEncoding]);
[self.m_unfinishedDownloads removeObject:download.contentIdentifier];
}
break;
case SKDownloadStateActive:
case SKDownloadStateWaiting:
case SKDownloadStatePaused:
break;
}
}
if ([self.m_unfinishedDownloads count] == 0)
{
[self finishTransaction:[downloads objectAtIndex:0].transaction.transactionIdentifier];
}
}
-(void) restorePurchasedProducts
{
[[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
}
-(void) refreshAppReceipt
{
self.m_receiptRefreshRequest = [[SKReceiptRefreshRequest alloc] init];
self.m_receiptRefreshRequest.delegate = self;
[self.m_receiptRefreshRequest start];
}
-(void) request:(SKRequest*) request didFailWithError:(NSError*) error
{
AZ_TracePrintf("LumberyardInAppPurchases", "Request failed with error: %s", [[error localizedDescription] cStringUsingEncoding:NSASCIIStringEncoding]);
}
-(void) requestDidFinish:(SKRequest *)request
{
if ([request isKindOfClass:[SKReceiptRefreshRequest class]])
{
[self.m_receiptRefreshRequest release];
self.m_receiptRefreshRequest = nil;
}
}
@end
@@ -0,0 +1,23 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "InAppPurchases_precompiled.h"
#include <InAppPurchases/InAppPurchasesInterface.h>
namespace InAppPurchases
{
InAppPurchasesInterface* InAppPurchasesInterface::CreateInstance()
{
return nullptr;
}
}
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Common/Unimplemented/InAppPurchases_Unimplemented.cpp
)
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Common/Unimplemented/InAppPurchases_Unimplemented.cpp
)
@@ -0,0 +1,10 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
@@ -0,0 +1,14 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Common/Unimplemented/InAppPurchases_Unimplemented.cpp
)
@@ -0,0 +1,23 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# this gem uses the ASN1 package on IOS.
ly_associate_package(asn1-0.9.27-rev2-ios ASN1)
find_library(STOREKIT_FRAMEWORK StoreKit)
set(LY_BUILD_DEPENDENCIES
PRIVATE
${STOREKIT_FRAMEWORK}
3rdParty::OpenSSL
3rdParty::ASN1
)
@@ -0,0 +1,17 @@
#
# All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
# its licensors.
#
# For complete copyright and license terms please see the LICENSE at the root of this
# distribution (the "License"). All use of this software is governed by the License,
# or, if provided, by the license below or the license accompanying this file. Do not
# remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
set(FILES
../Common/Apple/InAppPurchasesApple.h
../Common/Apple/InAppPurchasesApple.mm
../Common/Apple/InAppPurchasesDelegate.h
../Common/Apple/InAppPurchasesDelegate.mm
)