IAP Implementation for Durable Items
Hi, I want to implement Meta Quest IAP workflow with following steps. 1.User will purchase the IAP durable item 2.Durable item is Asset bundle of a scene as in different levels 3.Once purchase is completed i want to start downloading Assets Bundles attached to that IAP as DLC . 4. While downloading i want to see the download progress 5.Once downloaded it should get stored locally 6.It should load the scene on button click. I have tried to implement this flow here is the code using UnityEngine; using Oculus.Platform; using Oculus.Platform.Models; using System.Collections.Generic; using TMPro; using System; using UnityEngine.SceneManagement; using System.Collections; using System.Linq; using System.IO; public class IAPManager : MonoBehaviour { public TextMeshPro debug, purchaseData; private void Start() { // Initialize the Oculus Platform SDK Core.Initialize(); // Fetch available IAPs FetchIAPs(); // Fetch purchased IAPs FetchPurchasedIAPs(); } private void FetchIAPs() { IAP.GetProductsBySKU(new List<string>() {"arcade_range" }.ToArray()).OnComplete(GetProductsCallback); } private void FetchPurchasedIAPs() { Entitlements.IsUserEntitledToApplication().OnComplete(r => { if (r.IsError) { Debug.Log("Error entitling user to the application: " + r.GetError().Message); } }); IAP.GetViewerPurchases().OnComplete(GetPurchasedItemsCallback); } private void GetProductsCallback(Message<ProductList> message) { if (message.IsError) { Debug.LogError("Failed to fetch IAP products: " + message.GetError().Message); // debug.text += "\n"+"Failed to fetch IAP products: " + message.GetError().Message; return; } Debug.Log("Available IAPs:"); foreach (var product in message.Data) { Debug.Log($"Product: {product.Name}, Price: {product.FormattedPrice}"); // debug.text += "\n" + $"Product: {product.Name}, Price: {product.FormattedPrice}"; } } private void GetPurchasedItemsCallback(Message<PurchaseList> message) { if (message.IsError) { Debug.LogError("Failed to fetch purchased IAPs: " + message.GetError().Message); purchaseData.text += "\n" + "Failed to fetch purchased IAPs: " + message.GetError().Message; return; } else { OnPurchasesRetrieved(); // foreach (var purchase in message.Data) //{ // Debug.Log($"Purchase: {purchase.Sku}, GrantTime: {purchase.GrantTime}"); // purchaseData.text += "\n" + $"Purchase: {purchase.Sku},Purchase: {purchase.ID}, GrantTime: {purchase.GrantTime}"; // if (String.Compare(purchase.Sku, "arcade_range") == 0) // { // purchaseData.text += "\n Downloading" + $"Purchase: {purchase.Sku}"; // AssetFile.DownloadByName(purchase.Sku).OnComplete(getDownloadedItemsCallBack); // } // } } //Debug.Log("Purchased IAPs:"); //foreach (var purchase in message.Data) //{ // Debug.Log($"Purchase: {purchase.Sku}, GrantTime: {purchase.GrantTime}"); // purchaseData.text += "\n" + $"Purchase: {purchase.Sku},Purchase: {purchase.ID}, GrantTime: {purchase.GrantTime}"; // // AssetFile.Download(ulong.Parse(purchase.ID)).OnComplete(getDownloadedItemsCallBack); //} } void OnPurchasesRetrieved() { Debug.Log("Finished checking purchased DLC. Retrieving downloadable assets"); GetOculusAssetFileList(); } public void GetOculusAssetFileList() { AssetFile.GetList().OnComplete((Message<AssetDetailsList> msg) => { if (msg.IsError) { Debug.LogError("Error retrieving DLC information: " + msg.GetError().Message); purchaseData.text += "\n" + "Error retrieving DLC information"; return; } List<ulong> assetIds = new List<ulong>(); Dictionary<string, string> newSkuToFilepathMap = new Dictionary<string, string>(); FetchPurchasedProducts((purchasedSkus) => { foreach (var assetDetail in msg.Data) { Debug.Log($"AssetID: {assetDetail.AssetId}, Filepath: {assetDetail.Filepath}"); purchaseData.text += "\n" + $"AssetID: {assetDetail.AssetId}, Filepath: {assetDetail.AssetId}"; // AssetFile.Download(assetDetail.AssetId).OnComplete(getDownloadedItemsCallBack); purchaseData.text += "\n" + Path.GetFileNameWithoutExtension(assetDetail.Filepath); StartCoroutine(LoadSceneFromBundle(assetDetail.Filepath, "ProShooterVR_ArcadeMode")); //if (!string.IsNullOrEmpty(assetDetail.Filepath)) //{ // var filepathParts = assetDetail.Filepath.Split('/'); // var filename = filepathParts.get // var sku = Path.GetFileNameWithoutExtension(filename); // if (thumbnailSKUs.Contains(sku)) // { // newSkuToFilepathMap[sku] = assetDetail.Filepath; // bool isDownloaded = File.Exists(assetDetail.Filepath); // skuDownloadStatus[sku] = isDownloaded; // assetIds.Add(assetDetail.AssetId); // skuToAssetIdMap[sku] = assetDetail.AssetId; // skuPurchaseStatus[sku] = purchasedSkus.Contains(sku); // } //} } //skuToFilepathMap = newSkuToFilepathMap; //dlcAssetIds = assetIds.ToArray(); }); }); } IEnumerator LoadSceneFromBundle(string bundlePath, string sceneName) { if (!System.IO.File.Exists(bundlePath)) { Debug.LogError("Bundle file does not exist: " + bundlePath); purchaseData.text += "\n" + "Bundle file does not exist:"; yield break; } // Load the asset bundle asynchronously AssetBundleCreateRequest bundleLoadRequest = AssetBundle.LoadFromFileAsync(bundlePath); yield return bundleLoadRequest; AssetBundle bundle = bundleLoadRequest.assetBundle; if (bundle == null) { Debug.LogError("Failed to load AssetBundle!"); purchaseData.text += "\n" + "Failed to load AssetBundle!"; yield break; } // Check if the scene is in the bundle if (!bundle.Contains(sceneName)) { Debug.LogError("Scene not found in the bundle!"); purchaseData.text += "\n" + "Scene not found in the bundle!"; yield break; } // Load the scene asynchronously string scenePath = bundle.GetAllScenePaths()[0]; // Assuming the scene is the first one listed in the asset bundle AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(scenePath); // Wait until the scene has loaded while (!asyncLoad.isDone) { yield return null; } // Optionally unload the asset bundle to free memory // bundle.Unload(false); } private void FetchPurchasedProducts(Action<List<string>> onComplete) { IAP.GetViewerPurchases().OnComplete((Message<PurchaseList> msg) => { if (msg.IsError) { Debug.LogError("Error checking purchased DLC: " + msg.GetError().Message); onComplete(new List<string>()); return; } List<string> purchasedSkus = msg.Data.Select(p => p.Sku).ToList(); onComplete(purchasedSkus); }); } /// <summary> /// ////////////////////////////////////////////////////////////// /// </summary> /// <param name="message"></param> private void getDownloadedItemsCallBack(Message<AssetFileDownloadResult> message) { if(message.IsError) { purchaseData.text += "\n" + "Failed to download purchased IAPs: " + message.GetError().Message; return; } else { purchaseData.text += "\n" + "Downloading waait"; purchaseData.text += "\n" + message.GetAppDownloadProgressResult().StatusCode.ToString(); var asset = message.GetAssetDetails(); LoadSceneFromAssetBundle(asset.Filepath); } } /// <summary> /// This Method will call the purchase flow API of Meta /// </summary> public void BuyItem() { IAP.LaunchCheckoutFlow(sku: "arcade_range").OnComplete(BuyItemCallback); } private void BuyItemCallback(Message<Purchase> message) { if (message.IsError) { return; } else { } Purchase p = message.GetPurchase(); Debug.Log("purchased " + p.ID); ulong assetFileId = ulong.Parse(p.ID); // Replace with your actual asset file ID DownloadAssetById(assetFileId); // purchaseData.text = string.Empty; // FetchPurchasedIAPs(); } private void DownloadAssetById(ulong assetFileId) { AssetFile.DownloadById(assetFileId).OnComplete(AssetDownloadComplete); } private void AssetDownloadComplete(Message<AssetFileDownloadResult> message) { if (message.IsError) { Debug.LogError("Failed to download asset file: " + message.GetError().Message); return; } else { purchaseData.text += "\n" + "Downloading waait"; var asset = message.GetAssetDetails(); LoadSceneFromAssetBundle(asset.Filepath); } } private void LoadSceneFromAssetBundle(string assetBundleUri) { StartCoroutine(LoadYourAsyncScene(assetBundleUri)); } IEnumerator LoadYourAsyncScene(string assetBundleUri) { // Load the AssetBundle file from cache if it exists with the specified version number AssetBundle myLoadedAssetBundle = AssetBundle.LoadFromFile(assetBundleUri); if (myLoadedAssetBundle == null) { Debug.Log("Failed to load AssetBundle!"); purchaseData.text += "\n" + "Failed to load AssetBundle"; yield break; } string[] scenePaths = myLoadedAssetBundle.GetAllScenePaths(); string sceneName = System.IO.Path.GetFileNameWithoutExtension(scenePaths[0]); Debug.Log("Loading scene " + sceneName); purchaseData.text += "\n" + "Loading"; // Load the scene asynchronously in the background AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(sceneName); while (!asyncLoad.isDone) { yield return null; } Debug.Log("Scene loaded"); // Optionally, unload the asset bundle to free up memory after the scene has loaded myLoadedAssetBundle.Unload(false); } } //void OnPurchasesRetrieved() //{ // Debug.Log("Finished checking purchased DLC. Retrieving downloadable assets"); // GetOculusAssetFileList(); //} //public void GetOculusAssetFileList() //{ // AssetFile.GetList().OnComplete((Message<AssetDetailsList> msg) => // { // if (msg.IsError) // { // Debug.LogError("Error retrieving DLC information: " + msg.GetError().Message); // loadStatus.text = "Error retrieving DLC information"; // return; // } // List<ulong> assetIds = new List<ulong>(); // Dictionary<string, string> newSkuToFilepathMap = new Dictionary<string, string>(); // FetchPurchasedProducts((purchasedSkus) => // { // foreach (var assetDetail in msg.Data) // { // Debug.Log($"AssetID: {assetDetail.AssetId}, Filepath: {assetDetail.Filepath}"); // if (!string.IsNullOrEmpty(assetDetail.Filepath)) // { // var filepathParts = assetDetail.Filepath.Split('/'); // var filename = filepathParts.LastOrDefault(); // var sku = Path.GetFileNameWithoutExtension(filename); // if (thumbnailSKUs.Contains(sku)) // { // newSkuToFilepathMap[sku] = assetDetail.Filepath; // bool isDownloaded = File.Exists(assetDetail.Filepath); // skuDownloadStatus[sku] = isDownloaded; // assetIds.Add(assetDetail.AssetId); // skuToAssetIdMap[sku] = assetDetail.AssetId; // skuPurchaseStatus[sku] = purchasedSkus.Contains(sku); // } // } // } // skuToFilepathMap = newSkuToFilepathMap; // dlcAssetIds = assetIds.ToArray(); // }); // }); //} //private void FetchPurchasedProducts(Action<List<string>> onComplete) //{ // IAP.GetViewerPurchases().OnComplete((Message<PurchaseList> msg) => // { // if (msg.IsError) // { // Debug.LogError("Error checking purchased DLC: " + msg.GetError().Message); // onComplete(new List<string>()); // return; // } // List<string> purchasedSkus = msg.Data.Select(p => p.Sku).ToList(); // onComplete(purchasedSkus); // }); //} Please Help Us implement this.690Views0likes0CommentsError: "Horizon isn't installed, or you're running an incompatible of home/horizon"
I've almost completed what I think is a pretty solid script to allow the user to download DLC (assetbundle) for my app using Oculus Platform. All goes well from entitlement checks, to retrieving prices, to checking already purchased items, to the purchase itself. Starting the actual download however fails and returns this error: Either the service couldn't be connected to, Horizon isn't installed, or you're running an incompatible of home/horizon What's strange is that I can't find anything about this error. Literally zero results on google. I'm testing the app and it's IAP with test users, which should be fine This error occurs in the build (I have a worldspace canvas showing debug info). Because there's such little information on this error, I think it's because the error comes from the Quest itself and not from any script of my project (and therefore hard to spot). It occurs both when I've installed the apk directly to my Quest 2 and after downloading it from App Lab GPT suggests it has likely something to do with a server or software problem on Oculus' side. However, I just don't buy it since it's a fresh new script and therefore more at risk of being buggy than Oculus. You can't just simply install Horizon as an app, so I installed Horizon Workrooms. Of course didn't solve anything.Solved1.5KViews0likes2CommentsDLC version control
Hi, What is the norm for DLC version control? As in, I want to be able to check if a DLC is updated and then re-download the file if applicable. I can think of two solutions, but I think both are quite crude and don't like them. 1. Save the current version locally on the device using PlayerPrefs and use the meta data field and in the add-on to compare versions, but there must be a better way. 2. Keep a DlcManifest in the streaming assets folder that keeps track of skus and assetIds and then compare if the current assetId from the IAP api matches the assetId in the DlcManifest. Both mean some manual labor, which I don't like because that can easily introduce mistakes. So, what other options are there?776Views0likes1Comment