package funkin.mobile.util; #if FEATURE_MOBILE_IAP #if android import extension.iapcore.android.IAPAndroid; import extension.iapcore.android.IAPProductDetails; import extension.iapcore.android.IAPPurchase; import extension.iapcore.android.IAPPurchaseState; import extension.iapcore.android.IAPResponseCode; import extension.iapcore.android.IAPResult; #elseif ios import extension.iapcore.ios.IAPError; import extension.iapcore.ios.IAPIOS; import extension.iapcore.ios.IAPProductDetails; import extension.iapcore.ios.IAPPurchase; import extension.iapcore.ios.IAPPurchaseState; #end /** * Provides utility functions for working with in-app purchases. */ @:nullSafety class InAppPurchasesUtil { /** * The product ID used for the "No Ads" in-app purchase upgrade. */ public static final UPGRADE_PRODUCT_ID:String = 'no_ads'; /** * The maximum number of attempts to reconnect in case of a failure. */ static final MAX_RECONNECT_ATTEMPTS:Int = 3; /** * A static variable that holds an array of currently loaded product details for in-app purchases. */ static var currentProductDetails:Array = []; /** * A static variable that holds an array of currently purchased for in-app purchases. */ static var currentPurchased:Array = []; /** * A static variable to track the number of attempts made to reconnect. */ static var reconnectAttempts:Int = 0; /** * Initializes the in-app purchases utility. */ public static function init():Void { #if android IAPAndroid.onLog.add(function(message:String):Void { trace(message); }); IAPAndroid.onBillingSetupFinished.add(function(result:IAPResult):Void { if (result.getResponseCode() != IAPResponseCode.OK) { trace('Billing setup failed "$result"!'); return; } if (reconnectAttempts > 0) { trace('Billing service successfully reconnected!'); reconnectAttempts = 0; } IAPAndroid.queryPurchases(); IAPAndroid.queryProductDetails([UPGRADE_PRODUCT_ID]); }); IAPAndroid.onBillingServiceDisconnected.add(function():Void { trace("Billing service disconnected!"); if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { reconnectAttempts++; trace('Attempting to reconnect... ($reconnectAttempts/$MAX_RECONNECT_ATTEMPTS)'); IAPAndroid.startConnection(); } else { trace('Max reconnect attempts reached.'); } }); IAPAndroid.onProductDetailsResponse.add(function(result:IAPResult, productDetails:Array):Void { if (result.getResponseCode() == IAPResponseCode.OK) currentProductDetails = productDetails; else { trace('Failed to fetch product details: "$result"'); } }); IAPAndroid.onQueryPurchasesResponse.add(function(result:IAPResult, purchases:Array):Void { if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases); else { trace('Failed to query purchases: "$result"'); } }); IAPAndroid.onPurchasesUpdated.add(function(result:IAPResult, purchases:Array):Void { if (result.getResponseCode() == IAPResponseCode.OK) handlePurchases(purchases); else { trace('Failed to update purchases: "$result"'); } }); IAPAndroid.onAcknowledgePurchaseResponse.add(function(result:IAPResult):Void { if (result.getResponseCode() == IAPResponseCode.OK) trace('Purchase acknowledged successfully!'); else { trace('Failed to acknowledge purchase: $result'); } }); IAPAndroid.init(); IAPAndroid.startConnection(); #else IAPIOS.onProductDetailsReceived.add(function(productDetails:Array):Void { if (productDetails != null) currentProductDetails = productDetails; }); IAPIOS.onProductDetailsFailed.add(function(error:IAPError):Void { trace('Failed to request product details: "$error"'); }); IAPIOS.onPurchasesUpdated.add(function(purchases:Array):Void { handlePurchases(purchases); }); IAPIOS.init(); IAPIOS.restorePurchases(); IAPIOS.requestProducts([UPGRADE_PRODUCT_ID]); #end } /** * Restores previously made in-app purchases for the current user. */ public static function restorePurchases():Void { #if android IAPAndroid.queryPurchases(); #else IAPIOS.restorePurchases(); #end } /** * Initiates the purchase process for the specified item. * * @param id The identifier of the item to be purchased. * @param onPurchased The function to be called when the the product is purchased. */ public static function purchase(id:String, onPurchased:Void->Void):Void { for (product in currentProductDetails) { #if android if (product.getProductId() == id) { function purchasesUpdatedEvent(result:IAPResult, purchases:Array):Void { for (purchase in purchases) { if (purchase.getProducts().contains(id)) { if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED) { if (onPurchased != null) onPurchased(); IAPAndroid.onPurchasesUpdated.remove(purchasesUpdatedEvent); } } } } IAPAndroid.onPurchasesUpdated.add(purchasesUpdatedEvent); IAPAndroid.launchPurchaseFlow(product); return; } #elseif ios if (product.getProductIdentifier() == id) { function purchasesUpdatedEvent(purchases:Array):Void { for (purchase in purchases) { if (purchase.getPaymentProductIdentifier() == id) { switch (purchase.getTransactionState()) { case IAPPurchaseState.PURCHASED | IAPPurchaseState.RESTORED: if (onPurchased != null) onPurchased(); IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent); case IAPPurchaseState.FAILED: IAPIOS.onPurchasesUpdated.remove(purchasesUpdatedEvent); default: } } } } IAPIOS.onPurchasesUpdated.add(purchasesUpdatedEvent); IAPIOS.purchaseProduct(product); return; } #end } trace("Didn't find product details for ID: " + id); } /** * Checks if the specified product ID is already purchased. * * @param id The product ID to check. * * @return `true` if the product is already purchased and acknowledged, false otherwise. */ public static function isPurchased(id:String):Bool { for (purchase in currentPurchased) { #if android if (purchase.getProducts().contains(id)) { return true; } #elseif ios if (purchase.getPaymentProductIdentifier() == id) { return true; } #end } return false; } @:noCompletion private static function handlePurchases(purchases:Array):Void { for (purchase in purchases) { #if android if (purchase.getPurchaseState() == IAPPurchaseState.PURCHASED) { if (!purchase.isAcknowledged()) { IAPAndroid.acknowledgePurchase(purchase.getPurchaseToken()); } var alreadyTracked:Bool = false; for (existing in currentPurchased) { if (existing.getPurchaseToken() == purchase.getPurchaseToken()) { alreadyTracked = true; break; } } if (!alreadyTracked) { currentPurchased.push(purchase); trace('Android purchase tracked: ${purchase.getPurchaseToken()}'); } else { trace('Android purchase already tracked: ${purchase.getPurchaseToken()}'); } } else { trace('Android purchase not completed: ${purchase.getPurchaseState()}'); } #elseif ios trace('Transaction ID: ${purchase.getTransactionIdentifier()}'); trace('Transaction Date: ${purchase.getTransactionDate()}'); trace('Transaction Payment Product ID: ${purchase.getPaymentProductIdentifier()}'); var alreadyTracked:Bool = false; for (existing in currentPurchased) { if (existing.getTransactionIdentifier() == purchase.getTransactionIdentifier()) { alreadyTracked = true; break; } } switch (purchase.getTransactionState()) { case IAPPurchaseState.PURCHASING: trace('iOS purchase is in progress.'); case IAPPurchaseState.DEFERRED: trace('iOS purchase is deferred.'); case IAPPurchaseState.FAILED: trace('iOS purchase failed: ${purchase.getTransactionError()}.'); case IAPPurchaseState.PURCHASED | IAPPurchaseState.RESTORED: trace('iOS purchase successful or restored.'); if (!alreadyTracked) { currentPurchased.push(purchase); trace('iOS purchase tracked: ${purchase.getTransactionIdentifier()}'); IAPIOS.finishPurchase(purchase); } else { trace('iOS purchase already tracked: ${purchase.getTransactionIdentifier()}'); } } #end } } } #end