diff --git a/EOSIntegrationKit.uplugin b/EOSIntegrationKit.uplugin index 572d33b..70c4f03 100644 --- a/EOSIntegrationKit.uplugin +++ b/EOSIntegrationKit.uplugin @@ -87,7 +87,7 @@ { "Name": "EIKLoginMethods", "Type": "Runtime", - "LoadingPhase": "Default", + "LoadingPhase": "PreLoadingScreen", "PlatformAllowList": [ "Win64", "Android", diff --git a/Source/EIKLoginMethods/EIKLoginMethods.Build.cs b/Source/EIKLoginMethods/EIKLoginMethods.Build.cs index 1aa098c..83f71d3 100644 --- a/Source/EIKLoginMethods/EIKLoginMethods.Build.cs +++ b/Source/EIKLoginMethods/EIKLoginMethods.Build.cs @@ -16,6 +16,15 @@ public EIKLoginMethods(ReadOnlyTargetRules Target) : base(Target) Console.WriteLine("PluginPath: " + PluginPath); //AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(PluginPath, "GoogleOneTap_UPL.xml")); } + + PublicIncludePaths.AddRange( + new string[] + { + Path.Combine(ModuleDirectory, "Public"), + Path.Combine(ModuleDirectory, "Public/GooglePlayBilling") + } + ); + PublicDependencyModuleNames.AddRange( new string[] { @@ -33,6 +42,7 @@ public EIKLoginMethods(ReadOnlyTargetRules Target) : base(Target) "OnlineSubsystemEIK", "GoogleOneTapLibrary", "GooglePlayGamesLibrary", + "GooglePlayBillingLibrary", "Json", "JsonUtilities" } diff --git a/Source/EIKLoginMethods/Private/EIKLoginMethods.cpp b/Source/EIKLoginMethods/Private/EIKLoginMethods.cpp index df23ac0..0ff5fbc 100644 --- a/Source/EIKLoginMethods/Private/EIKLoginMethods.cpp +++ b/Source/EIKLoginMethods/Private/EIKLoginMethods.cpp @@ -4,7 +4,6 @@ void FEIKLoginMethodsModule::StartupModule() { - } void FEIKLoginMethodsModule::ShutdownModule() diff --git a/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.cpp b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.cpp new file mode 100644 index 0000000..7231c7c --- /dev/null +++ b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.cpp @@ -0,0 +1,110 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#include "GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.h" +#include "TimerManager.h" + +#if GPB_SUPPORTED +#include "Android/Utils/AndroidJNIConvertor.h" +#endif + +TWeakObjectPtr UGPB_AcknowledgePurchase::StaticInstance; +FTimerHandle UGPB_AcknowledgePurchase::TimeoutTimerHandle; + +UGPB_AcknowledgePurchase::UGPB_AcknowledgePurchase() +{ +} + +UGPB_AcknowledgePurchase* UGPB_AcknowledgePurchase::AcknowledgePurchase(const FString& PurchaseToken, UObject* WorldContextObject) +{ + UGPB_AcknowledgePurchase* Node = NewObject(); + Node->PurchaseToken = PurchaseToken; + Node->World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + StaticInstance = Node; + return Node; +} + +void UGPB_AcknowledgePurchase::Activate() +{ + Super::Activate(); + + UGPB_AcknowledgePurchase::StaticInstance = this; + + // Set up timeout + if (World.IsValid()) + { + World->GetTimerManager().SetTimer( + TimeoutTimerHandle, + this, + &UGPB_AcknowledgePurchase::OnTimeout, + GPB_AsyncTimeoutSeconds, // Use global configurable timeout + false + ); + } + +#if GPB_SUPPORTED + INIT_JAVA_METHOD(AndroidThunkJava_GPBL_acknowledgePurchase, "(Ljava/lang/String;)V"); + if (JNIEnv* JNIEnvPtr = FAndroidApplication::GetJavaEnv(true)) + { + FJavaWrapper::CallVoidMethod(JNIEnvPtr, FJavaWrapper::GameActivityThis, AndroidThunkJava_GPBL_acknowledgePurchase, + AndroidJNIConvertor::GetJavaString(PurchaseToken)); + } +#else + OnComplete.Broadcast(false, PurchaseToken, "ERROR: Google Play Billing Not Supported!"); + SetReadyToDestroy(); +#endif +} + +void UGPB_AcknowledgePurchase::BeginDestroy() +{ + // Clear timeout timer + if (World.IsValid()) + { + World->GetTimerManager().ClearTimer(TimeoutTimerHandle); + } + + UGPB_AcknowledgePurchase::StaticInstance = nullptr; + Super::BeginDestroy(); +} + +void UGPB_AcknowledgePurchase::OnTimeout() +{ + if (StaticInstance.Get()) + { + StaticInstance->OnComplete.Broadcast(false, StaticInstance->PurchaseToken, "Timeout! Check parameters and try again."); + StaticInstance->SetReadyToDestroy(); + } +} + +#if GPB_SUPPORTED +extern "C" +{ + JNIEXPORT void JNICALL + Java_com_epicgames_unreal_GameActivity_nativeOnAcknowledgeResponse( + JNIEnv* Env, + jclass Clazz, + jboolean bSuccess, + jstring PurchaseToken, + jstring Error) + { + bool bSuccessCpp = (bool)bSuccess; + FString PurchaseTokenStr = AndroidJNIConvertor::FromJavaString(PurchaseToken); + FString ErrorStr = AndroidJNIConvertor::FromJavaString(Error); + + AsyncTask(ENamedThreads::GameThread, [bSuccessCpp, PurchaseTokenStr, ErrorStr]() + { + if (UGPB_AcknowledgePurchase::StaticInstance.Get()) + { + // Always clear timeout timer first + if (UGPB_AcknowledgePurchase::StaticInstance->World.IsValid()) + { + UGPB_AcknowledgePurchase::StaticInstance->World->GetTimerManager().ClearTimer(UGPB_AcknowledgePurchase::TimeoutTimerHandle); + } + // Now broadcast and destroy + UGPB_AcknowledgePurchase::StaticInstance->OnComplete.Broadcast(bSuccessCpp, PurchaseTokenStr, ErrorStr); + UGPB_AcknowledgePurchase::StaticInstance->SetReadyToDestroy(); + } + }); + } +} +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.cpp b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.cpp new file mode 100644 index 0000000..bb0f463 --- /dev/null +++ b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.cpp @@ -0,0 +1,110 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#include "GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.h" +#include "TimerManager.h" + +#if GPB_SUPPORTED +#include "Android/Utils/AndroidJNIConvertor.h" +#endif + +TWeakObjectPtr UGPB_ConsumePurchase::StaticInstance; +FTimerHandle UGPB_ConsumePurchase::TimeoutTimerHandle; + +UGPB_ConsumePurchase::UGPB_ConsumePurchase() +{ +} + +UGPB_ConsumePurchase* UGPB_ConsumePurchase::ConsumePurchase(const FString& PurchaseToken, UObject* WorldContextObject) +{ + UGPB_ConsumePurchase* Node = NewObject(); + Node->PurchaseToken = PurchaseToken; + Node->World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + StaticInstance = Node; + return Node; +} + +void UGPB_ConsumePurchase::Activate() +{ + Super::Activate(); + + UGPB_ConsumePurchase::StaticInstance = this; + + // Set up timeout + if (World.IsValid()) + { + World->GetTimerManager().SetTimer( + TimeoutTimerHandle, + this, + &UGPB_ConsumePurchase::OnTimeout, + GPB_AsyncTimeoutSeconds, // Use global configurable timeout + false + ); + } + +#if GPB_SUPPORTED + INIT_JAVA_METHOD(AndroidThunkJava_GPBL_consumePurchase, "(Ljava/lang/String;)V"); + if (JNIEnv* JNIEnvPtr = FAndroidApplication::GetJavaEnv(true)) + { + FJavaWrapper::CallVoidMethod(JNIEnvPtr, FJavaWrapper::GameActivityThis, AndroidThunkJava_GPBL_consumePurchase, + AndroidJNIConvertor::GetJavaString(PurchaseToken)); + } +#else + OnComplete.Broadcast(false, PurchaseToken, "ERROR: Google Play Billing Not Supported!"); + SetReadyToDestroy(); +#endif +} + +void UGPB_ConsumePurchase::BeginDestroy() +{ + // Clear timeout timer + if (World.IsValid()) + { + World->GetTimerManager().ClearTimer(TimeoutTimerHandle); + } + + UGPB_ConsumePurchase::StaticInstance = nullptr; + Super::BeginDestroy(); +} + +void UGPB_ConsumePurchase::OnTimeout() +{ + if (StaticInstance.Get()) + { + StaticInstance->OnComplete.Broadcast(false, StaticInstance->PurchaseToken, "Timeout! Check parameters and try again."); + StaticInstance->SetReadyToDestroy(); + } +} + +#if GPB_SUPPORTED +extern "C" +{ + JNIEXPORT void JNICALL + Java_com_epicgames_unreal_GameActivity_nativeOnConsumeResponse( + JNIEnv* Env, + jclass Clazz, + jboolean bSuccess, + jstring PurchaseToken, + jstring Error) + { + bool bSuccessCpp = (bool)bSuccess; + FString PurchaseTokenStr = AndroidJNIConvertor::FromJavaString(PurchaseToken); + FString ErrorStr = AndroidJNIConvertor::FromJavaString(Error); + + AsyncTask(ENamedThreads::GameThread, [bSuccessCpp, PurchaseTokenStr, ErrorStr]() + { + if (UGPB_ConsumePurchase::StaticInstance.Get()) + { + // Always clear timeout timer first + if (UGPB_ConsumePurchase::StaticInstance->World.IsValid()) + { + UGPB_ConsumePurchase::StaticInstance->World->GetTimerManager().ClearTimer(UGPB_ConsumePurchase::TimeoutTimerHandle); + } + // Now broadcast and destroy + UGPB_ConsumePurchase::StaticInstance->OnComplete.Broadcast(bSuccessCpp, PurchaseTokenStr, ErrorStr); + UGPB_ConsumePurchase::StaticInstance->SetReadyToDestroy(); + } + }); + } +} +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.cpp b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.cpp new file mode 100644 index 0000000..906571d --- /dev/null +++ b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.cpp @@ -0,0 +1,150 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#include "GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.h" +#include "TimerManager.h" + +#if GPB_SUPPORTED +#include "Android/Utils/AndroidJNIConvertor.h" +#endif + +TWeakObjectPtr UGPB_LaunchPurchaseFlow::StaticInstance = nullptr; +FTimerHandle UGPB_LaunchPurchaseFlow::TimeoutTimerHandle; + +UGPB_LaunchPurchaseFlow::UGPB_LaunchPurchaseFlow() +{ + World = GEngine->GetWorldFromContextObject(this, EGetWorldErrorMode::ReturnNull); +} + +UGPB_LaunchPurchaseFlow* UGPB_LaunchPurchaseFlow::LaunchPurchaseFlow(const FGPB_ProductDetails& ProductDetails, const FString& OfferToken) +{ + UGPB_LaunchPurchaseFlow* Node = NewObject(); + Node->ProductDetails = ProductDetails; + Node->OfferToken = OfferToken; + StaticInstance = Node; + return Node; +} + +void UGPB_LaunchPurchaseFlow::Activate() +{ + Super::Activate(); + + UGPB_LaunchPurchaseFlow::StaticInstance = this; + + // Set up timeout + if (World.IsValid()) + { + World->GetTimerManager().SetTimer( + TimeoutTimerHandle, + this, + &UGPB_LaunchPurchaseFlow::OnTimeout, + GPB_AsyncTimeoutSeconds, // Use global configurable timeout + false + ); + } + +#if GPB_SUPPORTED + INIT_JAVA_METHOD(AndroidThunkJava_GPBL_launchPurchaseFlow, "(Ljava/lang/String;Ljava/lang/String;)V"); + if (JNIEnv* JNIEnvPtr = FAndroidApplication::GetJavaEnv(true)) + { + FJavaWrapper::CallVoidMethod(JNIEnvPtr, FJavaWrapper::GameActivityThis, AndroidThunkJava_GPBL_launchPurchaseFlow, + AndroidJNIConvertor::GetJavaString(ProductDetails.ToJson()), + AndroidJNIConvertor::GetJavaString(OfferToken)); + } +#else + OnComplete.Broadcast(false, FGPB_Purchase(), "ERROR: Google Play Billing Not Supported!"); + SetReadyToDestroy(); +#endif +} + +void UGPB_LaunchPurchaseFlow::BeginDestroy() +{ + // Clear timeout timer + if (World.IsValid()) + { + World->GetTimerManager().ClearTimer(TimeoutTimerHandle); + } + + UGPB_LaunchPurchaseFlow::StaticInstance = nullptr; + Super::BeginDestroy(); +} + +void UGPB_LaunchPurchaseFlow::OnTimeout() +{ + if (StaticInstance.Get()) + { + StaticInstance->OnComplete.Broadcast(false, FGPB_Purchase(), "Timeout! Check parameters and try again."); + StaticInstance->SetReadyToDestroy(); + } +} + +#if GPB_SUPPORTED +extern "C" +{ + JNIEXPORT void JNICALL + Java_com_epicgames_unreal_GameActivity_nativeOnPurchasesUpdated( + JNIEnv* Env, + jclass Clazz, + jboolean bSuccess, + jobjectArray PurchasesJsons, + jstring Error) + { + bool bSuccessCpp = (bool)bSuccess; + FString ErrorStr = AndroidJNIConvertor::FromJavaString(Error); + + TArray PurchasesJsonArray; + if (PurchasesJsons != nullptr) + { + jsize len = Env->GetArrayLength(PurchasesJsons); + for (jsize i = 0; i < len; ++i) + { + jstring jStr = (jstring)Env->GetObjectArrayElement(PurchasesJsons, i); + FString str = AndroidJNIConvertor::FromJavaString(jStr); + PurchasesJsonArray.Add(str); + Env->DeleteLocalRef(jStr); + } + } + + TArray Purchases; + for (const FString& Json : PurchasesJsonArray) + { + Purchases.Add(FGPB_Purchase::ParseFromJson(Json)); + } + + AsyncTask(ENamedThreads::GameThread, [bSuccessCpp, Purchases = MoveTemp(Purchases), ErrorStr]() + { + if (UGPB_LaunchPurchaseFlow::StaticInstance.Get()) + { + // Clear timeout timer since we got a response + if (UWorld* World = UGPB_LaunchPurchaseFlow::StaticInstance->GetWorld()) + { + World->GetTimerManager().ClearTimer(UGPB_LaunchPurchaseFlow::TimeoutTimerHandle); + } + + if (!bSuccessCpp) + { + // If the operation failed, broadcast failure immediately + UGPB_LaunchPurchaseFlow::StaticInstance->OnComplete.Broadcast(false, FGPB_Purchase(), ErrorStr); + UGPB_LaunchPurchaseFlow::StaticInstance->SetReadyToDestroy(); + return; + } + + // For success case, try to find our purchase + for (const FGPB_Purchase& Purchase : Purchases) + { + if (Purchase.Products.Contains(UGPB_LaunchPurchaseFlow::StaticInstance->ProductDetails.ProductId)) + { + UGPB_LaunchPurchaseFlow::StaticInstance->OnComplete.Broadcast(true, Purchase, ErrorStr); + UGPB_LaunchPurchaseFlow::StaticInstance->SetReadyToDestroy(); + return; + } + } + + // If we get here, we didn't find our purchase in the success case + UGPB_LaunchPurchaseFlow::StaticInstance->OnComplete.Broadcast(false, FGPB_Purchase(), ErrorStr); + UGPB_LaunchPurchaseFlow::StaticInstance->SetReadyToDestroy(); + } + }); + } +} +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.cpp b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.cpp new file mode 100644 index 0000000..f0f4ff5 --- /dev/null +++ b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.cpp @@ -0,0 +1,132 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#include "GooglePlayBilling/AsyncNodes/GPB_QueryProduct.h" +#include "TimerManager.h" + +#if GPB_SUPPORTED +#include "Android/Utils/AndroidJNIConvertor.h" +#endif + +TWeakObjectPtr UGPB_QueryProduct::StaticInstance; +FTimerHandle UGPB_QueryProduct::TimeoutTimerHandle; + +UGPB_QueryProduct::UGPB_QueryProduct() +{ +} + +UGPB_QueryProduct* UGPB_QueryProduct::QueryProduct(const FString& ProductID, bool bSubscription, UObject* WorldContextObject) +{ + UGPB_QueryProduct* Node = NewObject(); + Node->ProductID = ProductID; + Node->bSubscription = bSubscription; + Node->World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + StaticInstance = Node; + return Node; +} + +void UGPB_QueryProduct::Activate() +{ + Super::Activate(); + + UGPB_QueryProduct::StaticInstance = this; + + // Set up timeout + if (World.IsValid()) + { + World->GetTimerManager().SetTimer( + TimeoutTimerHandle, + this, + &UGPB_QueryProduct::OnTimeout, + GPB_AsyncTimeoutSeconds, // Use global configurable timeout + false + ); + } + +#if GPB_SUPPORTED + INIT_JAVA_METHOD(AndroidThunkJava_GPBL_queryProduct, "(Ljava/lang/String;Z)V"); + if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) + { + FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_GPBL_queryProduct, + AndroidJNIConvertor::GetJavaString(ProductID), + (jboolean)bSubscription); + } +#else + TArray ProductDetails; + OnComplete.Broadcast(false, ProductDetails, "ERROR: Google Play Billing Not Supported!"); + SetReadyToDestroy(); +#endif +} + +void UGPB_QueryProduct::BeginDestroy() +{ + // Clear timeout timer + if (World.IsValid()) + { + World->GetTimerManager().ClearTimer(TimeoutTimerHandle); + } + + UGPB_QueryProduct::StaticInstance = nullptr; + Super::BeginDestroy(); +} + +void UGPB_QueryProduct::OnTimeout() +{ + if (StaticInstance.Get()) + { + TArray EmptyProductDetails; + StaticInstance->OnComplete.Broadcast(false, EmptyProductDetails, "Timeout! Check parameters and try again."); + StaticInstance->SetReadyToDestroy(); + } +} + +#if GPB_SUPPORTED +extern "C" +{ + JNIEXPORT void JNICALL + Java_com_epicgames_unreal_GameActivity_nativeOnProductQueryResponse( + JNIEnv* Env, + jclass Clazz, + jboolean bSuccess, + jobjectArray ProductDetailsJsons, + jstring Error) + { + bool bSuccessCpp = (bool)bSuccess; + FString ErrorStr = AndroidJNIConvertor::FromJavaString(Error); + + TArray ProductDetailsJsonArray; + if (ProductDetailsJsons != nullptr) + { + jsize len = Env->GetArrayLength(ProductDetailsJsons); + for (jsize i = 0; i < len; ++i) + { + jstring jStr = (jstring)Env->GetObjectArrayElement(ProductDetailsJsons, i); + FString str = AndroidJNIConvertor::FromJavaString(jStr); + ProductDetailsJsonArray.Add(str); + Env->DeleteLocalRef(jStr); + } + } + + TArray ProductDetails; + for (const FString& Json : ProductDetailsJsonArray) + { + ProductDetails.Add(FGPB_ProductDetails::ParseFromJson(Json)); + } + + AsyncTask(ENamedThreads::GameThread, [bSuccessCpp, ProductDetails = MoveTemp(ProductDetails), ErrorStr]() + { + if (UGPB_QueryProduct::StaticInstance.Get()) + { + // Always clear timeout timer first + if (UGPB_QueryProduct::StaticInstance->World.IsValid()) + { + UGPB_QueryProduct::StaticInstance->World->GetTimerManager().ClearTimer(UGPB_QueryProduct::TimeoutTimerHandle); + } + // Now broadcast and destroy + UGPB_QueryProduct::StaticInstance->OnComplete.Broadcast(bSuccessCpp, ProductDetails, ErrorStr); + UGPB_QueryProduct::StaticInstance->SetReadyToDestroy(); + } + }); + } +} +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.cpp b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.cpp new file mode 100644 index 0000000..b195ef6 --- /dev/null +++ b/Source/EIKLoginMethods/Private/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.cpp @@ -0,0 +1,130 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#include "GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.h" +#include "TimerManager.h" + +#if GPB_SUPPORTED +#include "Android/Utils/AndroidJNIConvertor.h" +#endif + +TWeakObjectPtr UGPB_QueryPurchases::StaticInstance; +FTimerHandle UGPB_QueryPurchases::TimeoutTimerHandle; + +UGPB_QueryPurchases::UGPB_QueryPurchases() +{ +} + +UGPB_QueryPurchases* UGPB_QueryPurchases::QueryPurchases(bool bSubscription, UObject* WorldContextObject) +{ + UGPB_QueryPurchases* Node = NewObject(); + Node->bSubscription = bSubscription; + Node->World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull); + StaticInstance = Node; + return Node; +} + +void UGPB_QueryPurchases::Activate() +{ + Super::Activate(); + + UGPB_QueryPurchases::StaticInstance = this; + + // Set up timeout + if (World.IsValid()) + { + World->GetTimerManager().SetTimer( + TimeoutTimerHandle, + this, + &UGPB_QueryPurchases::OnTimeout, + GPB_AsyncTimeoutSeconds, // Use global configurable timeout + false + ); + } + +#if GPB_SUPPORTED + INIT_JAVA_METHOD(AndroidThunkJava_GPBL_queryPurchases, "(Z)V"); + if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) + { + FJavaWrapper::CallVoidMethod(Env, FJavaWrapper::GameActivityThis, AndroidThunkJava_GPBL_queryPurchases, + (jboolean)bSubscription); + } +#else + TArray Purchases; + OnComplete.Broadcast(false, Purchases, "ERROR: Google Play Billing Not Supported!"); + SetReadyToDestroy(); +#endif +} + +void UGPB_QueryPurchases::BeginDestroy() +{ + // Clear timeout timer + if (World.IsValid()) + { + World->GetTimerManager().ClearTimer(TimeoutTimerHandle); + } + + UGPB_QueryPurchases::StaticInstance = nullptr; + Super::BeginDestroy(); +} + +void UGPB_QueryPurchases::OnTimeout() +{ + if (StaticInstance.Get()) + { + TArray EmptyPurchases; + StaticInstance->OnComplete.Broadcast(false, EmptyPurchases, "Timeout! Check parameters and try again."); + StaticInstance->SetReadyToDestroy(); + } +} + +#if GPB_SUPPORTED +extern "C" +{ + JNIEXPORT void JNICALL + Java_com_epicgames_unreal_GameActivity_nativeOnPurchasesQueryResponse( + JNIEnv* Env, + jclass Clazz, + jboolean bSuccess, + jobjectArray PurchasesJsons, + jstring Error) + { + bool bSuccessCpp = (bool)bSuccess; + FString ErrorStr = AndroidJNIConvertor::FromJavaString(Error); + + TArray PurchasesJsonArray; + if (PurchasesJsons != nullptr) + { + jsize len = Env->GetArrayLength(PurchasesJsons); + for (jsize i = 0; i < len; ++i) + { + jstring jStr = (jstring)Env->GetObjectArrayElement(PurchasesJsons, i); + FString str = AndroidJNIConvertor::FromJavaString(jStr); + PurchasesJsonArray.Add(str); + Env->DeleteLocalRef(jStr); + } + } + + TArray Purchases; + for (const FString& Json : PurchasesJsonArray) + { + Purchases.Add(FGPB_Purchase::ParseFromJson(Json)); + } + + AsyncTask(ENamedThreads::GameThread, [bSuccessCpp, Purchases = MoveTemp(Purchases), ErrorStr]() + { + if (UGPB_QueryPurchases::StaticInstance.Get()) + { + // Always clear timeout timer first + if (UGPB_QueryPurchases::StaticInstance->World.IsValid()) + { + UGPB_QueryPurchases::StaticInstance->World->GetTimerManager().ClearTimer(UGPB_QueryPurchases::TimeoutTimerHandle); + } + // Now broadcast and destroy + UGPB_QueryPurchases::StaticInstance->OnComplete.Broadcast(bSuccessCpp, Purchases, ErrorStr); + UGPB_QueryPurchases::StaticInstance->SetReadyToDestroy(); + } + }); + } +} +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.h new file mode 100644 index 0000000..b29b8f9 --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_AcknowledgePurchase.h @@ -0,0 +1,41 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "GooglePlayBilling/GooglePlayBillingStructures.h" +#include "GooglePlayBilling/GooglePlayBillingMethods.h" +#include "GPB_AcknowledgePurchase.generated.h" + +UCLASS() +class EIKLOGINMETHODS_API UGPB_AcknowledgePurchase : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UGPB_AcknowledgePurchase(); + + /** Acknowledges a purchase + * @param PurchaseToken - The purchase token to acknowledge + * @param WorldContextObject - The world context object + * @return The async node instance + */ + UFUNCTION(BlueprintCallable, Category = "Google Play Billing", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject")) + static UGPB_AcknowledgePurchase* AcknowledgePurchase(const FString& PurchaseToken, UObject* WorldContextObject); + + virtual void Activate() override; + virtual void BeginDestroy() override; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnComplete, bool, bSuccess, const FString&, PurchaseToken, const FString&, Error); + UPROPERTY(BlueprintAssignable, Category = "Google Play Billing") + FOnComplete OnComplete; + + TWeakObjectPtr World; + static TWeakObjectPtr StaticInstance; + static FTimerHandle TimeoutTimerHandle; + + void OnTimeout(); + FString PurchaseToken; +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.h new file mode 100644 index 0000000..5554828 --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_ConsumePurchase.h @@ -0,0 +1,41 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "GooglePlayBilling/GooglePlayBillingStructures.h" +#include "GooglePlayBilling/GooglePlayBillingMethods.h" +#include "GPB_ConsumePurchase.generated.h" + +UCLASS() +class EIKLOGINMETHODS_API UGPB_ConsumePurchase : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UGPB_ConsumePurchase(); + + /** Consumes a purchase + * @param PurchaseToken - The purchase token to consume + * @param WorldContextObject - The world context object + * @return The async node instance + */ + UFUNCTION(BlueprintCallable, Category = "Google Play Billing", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject")) + static UGPB_ConsumePurchase* ConsumePurchase(const FString& PurchaseToken, UObject* WorldContextObject); + + virtual void Activate() override; + virtual void BeginDestroy() override; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnComplete, bool, bSuccess, const FString&, PurchaseToken, const FString&, Error); + UPROPERTY(BlueprintAssignable, Category = "Google Play Billing") + FOnComplete OnComplete; + + TWeakObjectPtr World; + static TWeakObjectPtr StaticInstance; + static FTimerHandle TimeoutTimerHandle; + + void OnTimeout(); + FString PurchaseToken; +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.h new file mode 100644 index 0000000..44c97ee --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_LaunchPurchaseFlow.h @@ -0,0 +1,42 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "GooglePlayBilling/GooglePlayBillingStructures.h" +#include "GooglePlayBilling/GooglePlayBillingMethods.h" +#include "GPB_LaunchPurchaseFlow.generated.h" + +UCLASS() +class EIKLOGINMETHODS_API UGPB_LaunchPurchaseFlow : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UGPB_LaunchPurchaseFlow(); + + /** Launches the purchase flow for a product + * @param ProductDetails - The product details to purchase + * @param OfferToken - The offer token for the purchase (empty for one-time purchases) + * @return The async node instance + */ + UFUNCTION(BlueprintCallable, Category = "Google Play Billing", meta = (BlueprintInternalUseOnly = "true")) + static UGPB_LaunchPurchaseFlow* LaunchPurchaseFlow(const FGPB_ProductDetails& ProductDetails, const FString& OfferToken); + + virtual void Activate() override; + virtual void BeginDestroy() override; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnComplete, bool, bSuccess, const FGPB_Purchase&, Purchase, const FString&, Error); + UPROPERTY(BlueprintAssignable, Category = "Google Play Billing") + FOnComplete OnComplete; + + FGPB_ProductDetails ProductDetails; + FString OfferToken; + TWeakObjectPtr World; + static TWeakObjectPtr StaticInstance; + static FTimerHandle TimeoutTimerHandle; + + void OnTimeout(); +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.h new file mode 100644 index 0000000..985b5db --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryProduct.h @@ -0,0 +1,43 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "GooglePlayBilling/GooglePlayBillingStructures.h" +#include "GooglePlayBilling/GooglePlayBillingMethods.h" +#include "GPB_QueryProduct.generated.h" + +UCLASS() +class EIKLOGINMETHODS_API UGPB_QueryProduct : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UGPB_QueryProduct(); + + /** Queries product details from Google Play Billing + * @param ProductID - The product ID to query + * @param bSubscription - Whether this is a subscription product + * @param WorldContextObject - The world context object + * @return The async node instance + */ + UFUNCTION(BlueprintCallable, Category = "Google Play Billing", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject")) + static UGPB_QueryProduct* QueryProduct(const FString& ProductID, bool bSubscription, UObject* WorldContextObject); + + virtual void Activate() override; + virtual void BeginDestroy() override; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnComplete, bool, bSuccess, const TArray&, ProductDetails, const FString&, Error); + UPROPERTY(BlueprintAssignable, Category = "Google Play Billing") + FOnComplete OnComplete; + + TWeakObjectPtr World; + static TWeakObjectPtr StaticInstance; + static FTimerHandle TimeoutTimerHandle; + + void OnTimeout(); + FString ProductID; + bool bSubscription; +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.h new file mode 100644 index 0000000..857f0d2 --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/AsyncNodes/GPB_QueryPurchases.h @@ -0,0 +1,42 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Kismet/BlueprintAsyncActionBase.h" +#include "GooglePlayBilling/GooglePlayBillingStructures.h" +#include "GooglePlayBilling/GooglePlayBillingMethods.h" +#include "GPB_QueryPurchases.generated.h" + +UCLASS() +class EIKLOGINMETHODS_API UGPB_QueryPurchases : public UBlueprintAsyncActionBase +{ + GENERATED_BODY() + +public: + UGPB_QueryPurchases(); + + /** Queries purchases from Google Play Billing + * @param bSubscription - Whether to query subscription purchases + * @param WorldContextObject - The world context object + * @return The async node instance + */ + UFUNCTION(BlueprintCallable, Category = "Google Play Billing", meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject")) + static UGPB_QueryPurchases* QueryPurchases(bool bSubscription, UObject* WorldContextObject); + + virtual void Activate() override; + virtual void BeginDestroy() override; + + DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FOnComplete, bool, bSuccess, const TArray&, Purchases, const FString&, Error); + UPROPERTY(BlueprintAssignable, Category = "Google Play Billing") + FOnComplete OnComplete; + + void OnTimeout(); + + bool bSubscription; + TWeakObjectPtr World; + + static TWeakObjectPtr StaticInstance; + static FTimerHandle TimeoutTimerHandle; +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingMethods.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingMethods.h new file mode 100644 index 0000000..09818f1 --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingMethods.h @@ -0,0 +1,34 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +// Global configurable timeout (in seconds) for Google Play Billing async nodes +static float GPB_AsyncTimeoutSeconds = 30.0f; + +#define GPB_SUPPORTED (PLATFORM_ANDROID && GOOGLE_PLAY_BILLING_ENABLED) + +#if GPB_SUPPORTED +#include "Android/AndroidApplication.h" +#include "Android/AndroidJNI.h" + +#include "Android/Utils/AndroidJNIConvertor.h" + +#define INIT_JAVA_METHOD(name, signature) \ +if (JNIEnv* Env = FAndroidApplication::GetJavaEnv(true)) { \ +name = FJavaWrapper::FindMethod(Env, FJavaWrapper::GameActivityClassID, #name, signature, false); \ +check(name != NULL); \ +} else { \ +check(0); \ +} + +#define DECLARE_JAVA_METHOD(name) \ +static jmethodID name = NULL; + +// ---- Methods ---- +DECLARE_JAVA_METHOD(AndroidThunkJava_GPBL_queryProduct); +DECLARE_JAVA_METHOD(AndroidThunkJava_GPBL_queryPurchases); +DECLARE_JAVA_METHOD(AndroidThunkJava_GPBL_launchPurchaseFlow); +DECLARE_JAVA_METHOD(AndroidThunkJava_GPBL_consumePurchase); +DECLARE_JAVA_METHOD(AndroidThunkJava_GPBL_acknowledgePurchase); +#endif \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingStructures.h b/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingStructures.h new file mode 100644 index 0000000..14a448b --- /dev/null +++ b/Source/EIKLoginMethods/Public/GooglePlayBilling/GooglePlayBillingStructures.h @@ -0,0 +1,573 @@ +// Copyright (C) 2025 Betide Studio. All Rights Reserved. +// Written by AvnishGameDev. + +#pragma once + +#include "CoreMinimal.h" +#include "Dom/JsonObject.h" +#include "Serialization/JsonSerializer.h" +#include "GooglePlayBillingStructures.generated.h" + +inline FString JsonObjToString(const TSharedRef& Obj) +{ + FString Out; + TSharedRef> Writer = TJsonWriterFactory<>::Create(&Out); + FJsonSerializer::Serialize(Obj, Writer); + return Out; +} + +/** + * One–time purchase offer + */ +USTRUCT(BlueprintType) +struct FGPB_OneTimePurchaseOfferDetails +{ + GENERATED_BODY() + + /** The formatted price of the item, including its currency form. For example, '$3.50' */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString FormattedPrice; + + /** The price in micro-units, where 1,000,000 micro-units equals one unit of the currency. For example, if price is '€7.99', price_amount_micros is '7990000' */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int64 PriceAmountMicros = 0; + + /** The ISO 4217 currency code for price. For example, if price is specified in British pounds sterling, price_currency_code is 'GBP' */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString PriceCurrencyCode; + + FString ToJson() const + { + const TSharedRef Obj = MakeShared(); + + Obj->SetStringField(TEXT("formattedPrice"), FormattedPrice); + Obj->SetNumberField(TEXT("priceAmountMicros"), static_cast(PriceAmountMicros)); + Obj->SetStringField(TEXT("priceCurrencyCode"), PriceCurrencyCode); + + return JsonObjToString(Obj); + } + + +}; + +/** Recurrence mode as defined by Play Billing */ +UENUM(BlueprintType) +enum class ERecurrenceMode : uint8 +{ + NONE = 0 UMETA(DisplayName = "None"), + InfiniteRecurring = 1 UMETA(DisplayName = "Infinite Recurring"), + FiniteRecurring = 2 UMETA(DisplayName = "Finite Recurring"), + NonRecurring = 3 UMETA(DisplayName = "Non-recurring") +}; + +/** + * Subscription pricing phase + */ +USTRUCT(BlueprintType) +struct FGPB_PricingPhase +{ + GENERATED_BODY() + + /** The number of billing cycles for which the pricing phase is valid */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int32 BillingCycleCount = 0; + + /** The billing period for the pricing phase, specified in ISO 8601 format */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString BillingPeriod; + + /** The formatted price of the item, including its currency form. For example, '$3.50' */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString FormattedPrice; + + /** The price in micro-units, where 1,000,000 micro-units equals one unit of the currency */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int64 PriceAmountMicros = 0; + + /** The ISO 4217 currency code for price */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString PriceCurrencyCode; + + /** The recurrence mode of the pricing phase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + ERecurrenceMode RecurrenceMode = ERecurrenceMode::NonRecurring; + + FString ToJson() const + { + const TSharedRef Obj = MakeShared(); + + Obj->SetNumberField(TEXT("billingCycleCount"), BillingCycleCount); + Obj->SetStringField(TEXT("billingPeriod"), BillingPeriod); + Obj->SetStringField(TEXT("formattedPrice"), FormattedPrice); + Obj->SetNumberField(TEXT("priceAmountMicros"), static_cast(PriceAmountMicros)); + Obj->SetStringField(TEXT("priceCurrencyCode"), PriceCurrencyCode); + Obj->SetNumberField(TEXT("recurrenceMode"), static_cast(RecurrenceMode)); + + return JsonObjToString(Obj); + } + +}; + +/** + * Installment plan details inside a subscription offer + */ +USTRUCT(BlueprintType) +struct FGPB_InstallmentPlanDetails +{ + GENERATED_BODY() + + /** The number of payments in the installment plan */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int32 InstallmentPlanCommitmentPaymentsCount = 0; + + /** The number of subsequent payments in the installment plan */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int32 SubsequentInstallmentPlanCommitmentPaymentsCount = 0; + + FString ToJson() const + { + // Emit only when we have non-default values + if (InstallmentPlanCommitmentPaymentsCount == 0 && + SubsequentInstallmentPlanCommitmentPaymentsCount == 0) + { + return TEXT("{}"); // matches Java omitting the object + } + + const TSharedRef Obj = MakeShared(); + Obj->SetNumberField(TEXT("installmentPlanCommitmentPaymentsCount"), + InstallmentPlanCommitmentPaymentsCount); + Obj->SetNumberField(TEXT("subsequentInstallmentPlanCommitmentPaymentsCount"), + SubsequentInstallmentPlanCommitmentPaymentsCount); + return JsonObjToString(Obj); + } + +}; + +/** + * Subscription offer + */ +USTRUCT(BlueprintType) +struct FGPB_SubscriptionOfferDetails +{ + GENERATED_BODY() + + /** The base plan ID of the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString BasePlanId; + + /** The offer ID of the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString OfferId; + + /** The list of tags associated with the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + TArray OfferTags; + + /** The offer token of the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString OfferToken; + + /** The installment plan details of the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FGPB_InstallmentPlanDetails InstallmentPlanDetails; + + /** The list of pricing phases for the subscription offer */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + TArray PricingPhases; + + FString ToJson() const + { + const TSharedRef Obj = MakeShared(); + + Obj->SetStringField(TEXT("basePlanId"), BasePlanId); + Obj->SetStringField(TEXT("offerId"), OfferId); + Obj->SetStringField(TEXT("offerToken"), OfferToken); + + /* offerTags array (can be empty) */ + { + TArray> TagVals; + for (const FString& Tag : OfferTags) + { + TagVals.Add(MakeShared(Tag)); + } + Obj->SetArrayField(TEXT("offerTags"), TagVals); + } + + /* installmentPlanDetails (optional) */ + { + const FString InstJson = InstallmentPlanDetails.ToJson(); + if (!InstJson.Equals(TEXT("{}"))) + { + TSharedPtr InstObj; + TSharedRef> R = TJsonReaderFactory<>::Create(InstJson); + FJsonSerializer::Deserialize(R, InstObj); + Obj->SetObjectField(TEXT("installmentPlanDetails"), InstObj); + } + } + + /* pricingPhases array */ + { + TArray> PhaseVals; + for (const FGPB_PricingPhase& Phase : PricingPhases) + { + TSharedPtr PhaseObj; + TSharedRef> R = + TJsonReaderFactory<>::Create(Phase.ToJson()); + FJsonSerializer::Deserialize(R, PhaseObj); + PhaseVals.Add(MakeShared(PhaseObj)); + } + Obj->SetArrayField(TEXT("pricingPhases"), PhaseVals); + } + + return JsonObjToString(Obj); + } + +}; + +/** Product type as defined by Play Billing */ +UENUM(BlueprintType) +enum class EProductType : uint8 +{ + Unknown = 0 UMETA(DisplayName = "Unknown"), + InApp = 1 UMETA(DisplayName = "In-App"), + Subscription = 2 UMETA(DisplayName = "Subscription") +}; + +/** + * Product details root + */ +USTRUCT(BlueprintType) +struct FGPB_ProductDetails +{ + GENERATED_BODY() + + /** The description of the product */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString Description; + + /** The name of the product */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString Name; + + /** The unique product identifier */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString ProductId; + + /** The type of the product (InApp or Subscription) */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + EProductType ProductType = EProductType::Unknown; + + /** The title of the product */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString Title; + + /** The one-time purchase offer details for the product */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FGPB_OneTimePurchaseOfferDetails OneTimePurchaseOfferDetails; + + /** The list of subscription offer details for the product */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + TArray SubscriptionOfferDetails; + + /* ---------- JSON helpers ---------- */ + + /** Parse from a UE JSON object (already deserialized) */ + static FGPB_ProductDetails ParseFromJson(const TSharedPtr& Obj) + { + FGPB_ProductDetails Out; + if (!Obj.IsValid()) return Out; + + Obj->TryGetStringField(TEXT("description"), Out.Description); + Obj->TryGetStringField(TEXT("name"), Out.Name); + Obj->TryGetStringField(TEXT("productId"), Out.ProductId); + + // Parse product type + FString ProductTypeStr; + if (Obj->TryGetStringField(TEXT("productType"), ProductTypeStr)) + { + if (ProductTypeStr.Equals(TEXT("inapp"), ESearchCase::IgnoreCase)) + { + Out.ProductType = EProductType::InApp; + } + else if (ProductTypeStr.Equals(TEXT("subs"), ESearchCase::IgnoreCase)) + { + Out.ProductType = EProductType::Subscription; + } + else + { + Out.ProductType = EProductType::Unknown; + } + } + + Obj->TryGetStringField(TEXT("title"), Out.Title); + + /* One-time purchase ------------- */ + if (const TSharedPtr* OneTimeObj; Obj->TryGetObjectField(TEXT("oneTimePurchaseOfferDetails"), OneTimeObj)) + { + (*OneTimeObj)->TryGetStringField(TEXT("formattedPrice"), Out.OneTimePurchaseOfferDetails.FormattedPrice); + (*OneTimeObj)->TryGetNumberField(TEXT("priceAmountMicros"),Out.OneTimePurchaseOfferDetails.PriceAmountMicros); + (*OneTimeObj)->TryGetStringField(TEXT("priceCurrencyCode"),Out.OneTimePurchaseOfferDetails.PriceCurrencyCode); + } + + /* Subscription offers ----------- */ + if (const TArray>* OffersArray; Obj->TryGetArrayField(TEXT("subscriptionOfferDetails"), OffersArray)) + { + for (const TSharedPtr& OfferVal : *OffersArray) + { + const TSharedPtr OfferObj = OfferVal->AsObject(); + if (!OfferObj.IsValid()) continue; + + FGPB_SubscriptionOfferDetails Offer; + OfferObj->TryGetStringField(TEXT("basePlanId"), Offer.BasePlanId); + OfferObj->TryGetStringField(TEXT("offerId"), Offer.OfferId); + OfferObj->TryGetStringField(TEXT("offerToken"), Offer.OfferToken); + + /* offerTags */ + if (const TArray>* TagsArray; OfferObj->TryGetArrayField(TEXT("offerTags"), TagsArray)) + { + for (const TSharedPtr& TagVal : *TagsArray) + { + FString Tag; TagVal->TryGetString(Tag); + Offer.OfferTags.Add(Tag); + } + } + + /* installmentPlanDetails */ + if (const TSharedPtr* InstObj; OfferObj->TryGetObjectField(TEXT("installmentPlanDetails"), InstObj)) + { + (*InstObj)->TryGetNumberField(TEXT("installmentPlanCommitmentPaymentsCount"), + Offer.InstallmentPlanDetails.InstallmentPlanCommitmentPaymentsCount); + (*InstObj)->TryGetNumberField(TEXT("subsequentInstallmentPlanCommitmentPaymentsCount"), + Offer.InstallmentPlanDetails.SubsequentInstallmentPlanCommitmentPaymentsCount); + } + + /* pricingPhases */ + if (const TArray>* PhasesArray; OfferObj->TryGetArrayField(TEXT("pricingPhases"), PhasesArray)) + { + for (const TSharedPtr& PhaseVal : *PhasesArray) + { + const TSharedPtr PhaseObj = PhaseVal->AsObject(); + if (!PhaseObj.IsValid()) continue; + + FGPB_PricingPhase Phase; + PhaseObj->TryGetNumberField(TEXT("billingCycleCount"), Phase.BillingCycleCount); + PhaseObj->TryGetStringField(TEXT("billingPeriod"), Phase.BillingPeriod); + PhaseObj->TryGetStringField(TEXT("formattedPrice"), Phase.FormattedPrice); + PhaseObj->TryGetNumberField(TEXT("priceAmountMicros"), Phase.PriceAmountMicros); + PhaseObj->TryGetStringField(TEXT("priceCurrencyCode"), Phase.PriceCurrencyCode); + + int32 RecurrenceModeInt = 0; + if (PhaseObj->TryGetNumberField(TEXT("recurrenceMode"), RecurrenceModeInt)) + { + Phase.RecurrenceMode = static_cast(RecurrenceModeInt); + } + + Offer.PricingPhases.Add(Phase); + } + } + + Out.SubscriptionOfferDetails.Add(Offer); + } + } + + return Out; + } + + /** Parse directly from a JSON string coming from Java's `toJson()` */ + static FGPB_ProductDetails ParseFromJson(const FString& JsonString) + { + TSharedPtr Obj; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + if (FJsonSerializer::Deserialize(Reader, Obj) && Obj.IsValid()) + { + return ParseFromJson(Obj); + } + return FGPB_ProductDetails(); + } + + FString ToJson() const + { + const TSharedRef Obj = MakeShared(); + + Obj->SetStringField(TEXT("description"), Description); + Obj->SetStringField(TEXT("name"), Name); + Obj->SetStringField(TEXT("productId"), ProductId); + + // Convert product type to string + FString ProductTypeStr; + switch (ProductType) + { + case EProductType::InApp: + ProductTypeStr = TEXT("inapp"); + break; + case EProductType::Subscription: + ProductTypeStr = TEXT("subs"); + break; + default: + ProductTypeStr = TEXT("unknown"); + break; + } + Obj->SetStringField(TEXT("productType"), ProductTypeStr); + + Obj->SetStringField(TEXT("title"), Title); + + /* oneTimePurchaseOfferDetails (optional) */ + { + const FString OneJson = OneTimePurchaseOfferDetails.ToJson(); + if (!OneJson.Equals(TEXT("{}"))) + { + TSharedPtr OneObj; + TSharedRef> R = + TJsonReaderFactory<>::Create(OneJson); + FJsonSerializer::Deserialize(R, OneObj); + Obj->SetObjectField(TEXT("oneTimePurchaseOfferDetails"), OneObj); + } + } + + /* subscriptionOfferDetails array (may be empty) */ + { + TArray> OfferVals; + for (const FGPB_SubscriptionOfferDetails& Offer : SubscriptionOfferDetails) + { + TSharedPtr OfferObj; + TSharedRef> R = + TJsonReaderFactory<>::Create(Offer.ToJson()); + FJsonSerializer::Deserialize(R, OfferObj); + OfferVals.Add(MakeShared(OfferObj)); + } + Obj->SetArrayField(TEXT("subscriptionOfferDetails"), OfferVals); + } + + return JsonObjToString(Obj); + } + +}; + +/** Java constants 0 / 1 / 2 expressed as a scoped enum */ +UENUM(BlueprintType) +enum class EPurchaseState : uint8 +{ + Unspecified = 0 UMETA(DisplayName = "Unspecified"), + Purchased = 1 UMETA(DisplayName = "Purchased"), + Pending = 2 UMETA(DisplayName = "Pending") +}; + +/** Root struct that represents a Google Play Billing Purchase */ +USTRUCT(BlueprintType) +struct FGPB_Purchase +{ + GENERATED_BODY() + + /** The order ID of the purchase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString OrderId; + + /** The list of product IDs included in the purchase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + TArray Products; + + /** The time of the purchase in milliseconds since the epoch */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int64 PurchaseTime = 0; + + /** The purchase token for the purchase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString PurchaseToken; + + /** The quantity of the purchase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + int32 Quantity = 0; + + /** The signature of the purchase */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + FString Signature; + + /** Whether the purchase has been acknowledged */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + bool bIsAcknowledged = false; + + /** The state of the purchase (Unspecified, Purchased, or Pending) */ + UPROPERTY(BlueprintReadOnly, Category = "Play Billing") + EPurchaseState PurchaseState = EPurchaseState::Unspecified; + + /* ----------- JSON helpers ------------ */ + + /** Parse from an already-deserialized UE JSON object */ + static FGPB_Purchase ParseFromJson(const TSharedPtr& Obj) + { + FGPB_Purchase Out; + if (!Obj.IsValid()) return Out; + + Obj->TryGetStringField(TEXT("orderId"), Out.OrderId); + Obj->TryGetStringField(TEXT("purchaseToken"), Out.PurchaseToken); + Obj->TryGetStringField(TEXT("signature"), Out.Signature); + + Obj->TryGetNumberField(TEXT("purchaseTime"), Out.PurchaseTime); + Obj->TryGetNumberField(TEXT("quantity"), Out.Quantity); + + bool Ack = false; + if (Obj->TryGetBoolField(TEXT("isAcknowledged"), Ack)) Out.bIsAcknowledged = Ack; + + /* products array */ + if (const TArray>* ProdArray; Obj->TryGetArrayField(TEXT("products"), ProdArray)) + { + for (const TSharedPtr& Val : *ProdArray) + { + FString Prod; Val->TryGetString(Prod); + Out.Products.Add(Prod); + } + } + + /* purchaseState integer → enum */ + int32 StateInt = 0; + if (Obj->TryGetNumberField(TEXT("purchaseState"), StateInt)) + { + // Ensure the value is within valid enum range + if (StateInt >= 0 && StateInt <= 2) + { + Out.PurchaseState = static_cast(StateInt); + } + else + { + Out.PurchaseState = EPurchaseState::Unspecified; + } + } + + return Out; + } + + /** Parse directly from a raw JSON string generated by Java */ + static FGPB_Purchase ParseFromJson(const FString& JsonString) + { + TSharedPtr Obj; + TSharedRef> Reader = TJsonReaderFactory<>::Create(JsonString); + if (FJsonSerializer::Deserialize(Reader, Obj) && Obj.IsValid()) + { + return ParseFromJson(Obj); + } + return FGPB_Purchase(); + } + + FString ToJson() const + { + const TSharedRef Obj = MakeShared(); + + Obj->SetStringField(TEXT("orderId"), OrderId); + Obj->SetStringField(TEXT("purchaseToken"), PurchaseToken); + Obj->SetStringField(TEXT("signature"), Signature); + Obj->SetNumberField(TEXT("purchaseTime"), static_cast(PurchaseTime)); + Obj->SetNumberField(TEXT("quantity"), Quantity); + Obj->SetBoolField (TEXT("isAcknowledged"), bIsAcknowledged); + Obj->SetNumberField(TEXT("purchaseState"), static_cast(PurchaseState)); + + TArray> ProductVals; + for (const FString& P : Products) + { + ProductVals.Add(MakeShared(P)); + } + Obj->SetArrayField(TEXT("products"), ProductVals); + + return JsonObjToString(Obj); + } + +}; \ No newline at end of file diff --git a/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesMethods.h b/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesMethods.h index f1a9237..d89ae97 100644 --- a/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesMethods.h +++ b/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesMethods.h @@ -22,6 +22,9 @@ check(0); \ #define DECLARE_JAVA_METHOD(name) \ static jmethodID name = NULL; +// Global configurable timeout (in seconds) for Google Play Billing async nodes +static float GPB_AsyncTimeoutSeconds = 30.0f; + // ---- Methods ---- DECLARE_JAVA_METHOD(AndroidThunkJava_GPGS_manualSignIn); DECLARE_JAVA_METHOD(AndroidThunkJava_GPGS_getIsSignedIn); diff --git a/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesStructures.h b/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesStructures.h index d19dd5a..6618e99 100644 --- a/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesStructures.h +++ b/Source/EIKLoginMethods/Public/GooglePlayGames/GooglePlayGamesStructures.h @@ -4,7 +4,6 @@ #pragma once #include "CoreMinimal.h" -#include "Json.h" #include "GooglePlayGamesStructures.generated.h" /** @@ -75,16 +74,16 @@ struct FGPGS_Player if (JsonObject.IsValid()) { // Read all fields from the JSON object - JsonObject->TryGetStringField("displayName", Player.DisplayName); - JsonObject->TryGetStringField("playerId", Player.PlayerID); - JsonObject->TryGetNumberField("retrievedTimeStamp", Player.RetrievedTimeStamp); - JsonObject->TryGetBoolField("hasHiResImage", Player.bHasHiResImage); - JsonObject->TryGetBoolField("hasIconImage", Player.bHasIconImage); - JsonObject->TryGetStringField("hiResImageUrl", Player.HiResImageUrl); - JsonObject->TryGetStringField("iconImageUrl", Player.IconImageUrl); - JsonObject->TryGetStringField("title", Player.Title); - JsonObject->TryGetStringField("bannerImageLandscapeUrl", Player.BannerImageLandscapeUrl); - JsonObject->TryGetStringField("bannerImagePortraitUrl", Player.BannerImagePortraitUrl); + JsonObject->TryGetStringField(TEXT("displayName"), Player.DisplayName); + JsonObject->TryGetStringField(TEXT("playerId"), Player.PlayerID); + JsonObject->TryGetNumberField(TEXT("retrievedTimeStamp"), Player.RetrievedTimeStamp); + JsonObject->TryGetBoolField(TEXT("hasHiResImage"), Player.bHasHiResImage); + JsonObject->TryGetBoolField(TEXT("hasIconImage"), Player.bHasIconImage); + JsonObject->TryGetStringField(TEXT("hiResImageUrl"), Player.HiResImageUrl); + JsonObject->TryGetStringField(TEXT("iconImageUrl"), Player.IconImageUrl); + JsonObject->TryGetStringField(TEXT("title"), Player.Title); + JsonObject->TryGetStringField(TEXT("bannerImageLandscapeUrl"), Player.BannerImageLandscapeUrl); + JsonObject->TryGetStringField(TEXT("bannerImagePortraitUrl"), Player.BannerImagePortraitUrl); } return Player; } @@ -99,16 +98,16 @@ struct FGPGS_Player if (JsonObject.IsValid()) { // Read all fields from the JSON object - JsonObject->TryGetStringField("displayName", Player.DisplayName); - JsonObject->TryGetStringField("playerId", Player.PlayerID); - JsonObject->TryGetNumberField("retrievedTimeStamp", Player.RetrievedTimeStamp); - JsonObject->TryGetBoolField("hasHiResImage", Player.bHasHiResImage); - JsonObject->TryGetBoolField("hasIconImage", Player.bHasIconImage); - JsonObject->TryGetStringField("hiResImageUrl", Player.HiResImageUrl); - JsonObject->TryGetStringField("iconImageUrl", Player.IconImageUrl); - JsonObject->TryGetStringField("title", Player.Title); - JsonObject->TryGetStringField("bannerImageLandscapeUrl", Player.BannerImageLandscapeUrl); - JsonObject->TryGetStringField("bannerImagePortraitUrl", Player.BannerImagePortraitUrl); + JsonObject->TryGetStringField(TEXT("displayName"), Player.DisplayName); + JsonObject->TryGetStringField(TEXT("playerId"), Player.PlayerID); + JsonObject->TryGetNumberField(TEXT("retrievedTimeStamp"), Player.RetrievedTimeStamp); + JsonObject->TryGetBoolField(TEXT("hasHiResImage"), Player.bHasHiResImage); + JsonObject->TryGetBoolField(TEXT("hasIconImage"), Player.bHasIconImage); + JsonObject->TryGetStringField(TEXT("hiResImageUrl"), Player.HiResImageUrl); + JsonObject->TryGetStringField(TEXT("iconImageUrl"), Player.IconImageUrl); + JsonObject->TryGetStringField(TEXT("title"), Player.Title); + JsonObject->TryGetStringField(TEXT("bannerImageLandscapeUrl"), Player.BannerImageLandscapeUrl); + JsonObject->TryGetStringField(TEXT("bannerImagePortraitUrl"), Player.BannerImagePortraitUrl); } } return Player; @@ -124,7 +123,7 @@ struct FGPGS_Player { // Get the array of friends const TArray>* FriendsJsonArray; - if (JsonObject->TryGetArrayField("friends", FriendsJsonArray)) + if (JsonObject->TryGetArrayField(TEXT("friends"), FriendsJsonArray)) { // Process each friend in the array for (const TSharedPtr& Value : *FriendsJsonArray) @@ -190,11 +189,11 @@ struct FGPGS_Event { if (JsonObject.IsValid()) { - JsonObject->TryGetStringField("name", Event.Name); - JsonObject->TryGetStringField("description", Event.Description); - JsonObject->TryGetStringField("eventId", Event.EventID); - JsonObject->TryGetNumberField("value", Event.Value); - JsonObject->TryGetBoolField("isVisible", Event.bIsVisible); + JsonObject->TryGetStringField(TEXT("name"), Event.Name); + JsonObject->TryGetStringField(TEXT("description"), Event.Description); + JsonObject->TryGetStringField(TEXT("eventId"), Event.EventID); + JsonObject->TryGetNumberField(TEXT("value"), Event.Value); + JsonObject->TryGetBoolField(TEXT("isVisible"), Event.bIsVisible); } } return Event; @@ -252,15 +251,15 @@ struct FGPGS_PlayerStats { if (JsonObject.IsValid()) { - JsonObject->TryGetNumberField("averageSessionLength", PlayerStats.AverageSessionLength); - JsonObject->TryGetNumberField("daysSinceLastPlayed", PlayerStats.DaysSinceLastPlayed); - JsonObject->TryGetNumberField("numberOfPurchases", PlayerStats.NumberOfPurchases); - JsonObject->TryGetNumberField("numberOfSessions", PlayerStats.NumberOfSessions); - JsonObject->TryGetNumberField("sessionPercentile", PlayerStats.SessionPercentile); - JsonObject->TryGetNumberField("spendPercentile", PlayerStats.SpendPercentile); + JsonObject->TryGetNumberField(TEXT("averageSessionLength"), PlayerStats.AverageSessionLength); + JsonObject->TryGetNumberField(TEXT("daysSinceLastPlayed"), PlayerStats.DaysSinceLastPlayed); + JsonObject->TryGetNumberField(TEXT("numberOfPurchases"), PlayerStats.NumberOfPurchases); + JsonObject->TryGetNumberField(TEXT("numberOfSessions"), PlayerStats.NumberOfSessions); + JsonObject->TryGetNumberField(TEXT("sessionPercentile"), PlayerStats.SessionPercentile); + JsonObject->TryGetNumberField(TEXT("spendPercentile"), PlayerStats.SpendPercentile); } } return PlayerStats; } -}; +}; \ No newline at end of file diff --git a/Source/EOSIntegrationKit/Public/EIKSettings.h b/Source/EOSIntegrationKit/Public/EIKSettings.h index 438cc6f..a042d3f 100644 --- a/Source/EOSIntegrationKit/Public/EIKSettings.h +++ b/Source/EOSIntegrationKit/Public/EIKSettings.h @@ -204,6 +204,10 @@ class EOSINTEGRATIONKIT_API UEIKSettings : UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="EOS Integration Kit Settings", meta = (EditCondition = "bEnableGooglePlayGames")) FString GooglePlayGamesAppID = FString(""); + /** Should Google Play Billing be enabled? Works only on Android. */ + UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="EOS Integration Kit Settings") + bool bEnableGooglePlayBilling = false; + /** Auto-Logins the player into the game. Can be used for testing or games with only 1 type of login */ UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category="EOS Settings|Login Settings|Auto Login") TEnumAsByte AutoLoginType; diff --git a/Source/ThirdParty/GooglePlayBillingLibrary/Android/libs/playbilling.aar b/Source/ThirdParty/GooglePlayBillingLibrary/Android/libs/playbilling.aar new file mode 100644 index 0000000..154a8db Binary files /dev/null and b/Source/ThirdParty/GooglePlayBillingLibrary/Android/libs/playbilling.aar differ diff --git a/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBillingLibrary.Build.cs b/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBillingLibrary.Build.cs new file mode 100644 index 0000000..99eaf4d --- /dev/null +++ b/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBillingLibrary.Build.cs @@ -0,0 +1,78 @@ +// Copyright Betide Studio 2025. +// Written by AvnishGameDev. + +using System; +using System.IO; +using UnrealBuildTool; + +public class GooglePlayBillingLibrary : ModuleRules +{ + public GooglePlayBillingLibrary(ReadOnlyTargetRules Target) : base(Target) + { + Type = ModuleType.External; + + if (Target.Platform == UnrealTargetPlatform.Android) + { + if (IsGooglePlayBillingEnabled(Target)) + { + PublicDefinitions.Add("GOOGLE_PLAY_BILLING_ENABLED=1"); + PublicDependencyModuleNames.AddRange(new string[] { "Launch" }); + + string ModulePath = Utils.MakePathRelativeTo(ModuleDirectory, Target.RelativeEnginePath); + AdditionalPropertiesForReceipt.Add("AndroidPlugin", Path.Combine(ModulePath, "GooglePlayBilling_APL.xml")); + } + else + { + PublicDefinitions.Add("GOOGLE_PLAY_BILLING_ENABLED=0"); + } + } + else + { + PublicDefinitions.Add("GOOGLE_PLAY_BILLING_ENABLED=0"); + } + } + + private bool IsGooglePlayBillingEnabled(ReadOnlyTargetRules Target) + { + // Assume the config file is located in the project's Config folder. + if (Target.ProjectFile != null) + { + string projectDirectory = Path.GetDirectoryName(Target.ProjectFile.FullName); + if (projectDirectory != null) + { + string configFilePath = Path.Combine(projectDirectory, "Config", "DefaultEngine.ini"); + if (!File.Exists(configFilePath)) + { + Console.WriteLine("DefaultEngine.ini not found at: " + configFilePath); + return false; + } + + // Read each line from the ini file. + foreach (string line in File.ReadAllLines(configFilePath)) + { + string trimmedLine = line.Trim(); + + // Skip blank lines and comments. + if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith(";") || trimmedLine.StartsWith("#")) + { + continue; + } + + // Look for the setting. + // For example: bEnableGooglePlayGames=true + if (trimmedLine.StartsWith("bEnableGooglePlayBilling", StringComparison.InvariantCultureIgnoreCase)) + { + string[] parts = trimmedLine.Split('='); + if (parts.Length >= 2 && bool.TryParse(parts[1].Trim(), out bool isEnabled)) + { + return isEnabled; + } + } + } + } + } + + // Default to not enabled if the key wasn't found. + return false; + } +} diff --git a/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBilling_APL.xml b/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBilling_APL.xml new file mode 100644 index 0000000..57a3107 --- /dev/null +++ b/Source/ThirdParty/GooglePlayBillingLibrary/GooglePlayBilling_APL.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + dependencies.implementation(name: 'playbilling', ext: 'aar') + dependencies { + implementation "com.android.billingclient:billing:7.1.1" + implementation 'com.google.code.gson:gson:2.10.1' + } + + + + + + + + + + import com.avnishgamedev.playbilling.*; + import android.widget.Toast; + + + + + + + + + + list, String error) { + String[] jsonArray = (list == null) ? new String[0] : list.stream().map(PBProductDetails::toJson).toArray(String[]::new); + nativeOnProductQueryResponse(bSuccess, jsonArray, error); + } + }); + } + + public void AndroidThunkJava_GPBL_queryPurchases(boolean bSubscription) + { + billingManager.queryPurchases(bSubscription, new PBQueryPurchasesResponseListener() { + @Override + public void onQueryResponse(boolean bSuccess, List purchases, String error) { + String[] jsonArray = (purchases == null) ? new String[0] : purchases.stream().map(PBPurchase::toJson).toArray(String[]::new); + nativeOnPurchasesQueryResponse(bSuccess, jsonArray, error); + } + }); + } + + public void AndroidThunkJava_GPBL_launchPurchaseFlow(String productDetailsJson, String offerToken) + { + PBProductDetails details = PBProductDetails.fromJson(productDetailsJson); + billingManager.launchPurchaseFlow(details, offerToken.isEmpty() ? null : offerToken); + } + + public void AndroidThunkJava_GPBL_consumePurchase(String purchaseToken) + { + billingManager.consumePurchase(purchaseToken, new PBConsumeResponseListener() { + @Override + public void onResponse(boolean bSuccess, String purchaseToken, String error) { + nativeOnConsumeResponse(bSuccess, purchaseToken, error); + } + }); + } + + public void AndroidThunkJava_GPBL_acknowledgePurchase(String purchaseToken) + { + billingManager.acknowledgePurchase(purchaseToken, new PBAcknowledgePurchaseResponseListener() { + @Override + public void onResponse(boolean bSuccess, String purchaseToken, String error) { + nativeOnAcknowledgeResponse(bSuccess, purchaseToken, error); + } + }); + } + ]]> + + + + + + + + + + purchases, String error) { + String[] jsonArray = (purchases == null) ? new String[0] : purchases.stream().map(PBPurchase::toJson).toArray(String[]::new); + nativeOnPurchasesUpdated(bSuccess, jsonArray, error); + } + }); + ]]> + + + + + + + + + + billingManager.onResume(); + + + + + +