Search Unity

Resolved unity iap localized price incorrect currency ios

Discussion in 'Unity IAP' started by arivasaim, Jun 22, 2021.

Thread Status:
Not open for further replies.
  1. arivasaim

    arivasaim

    Joined:
    May 24, 2021
    Posts:
    2
    hi ! I have a problem with currency in IOs
    the same email account on different devices in the same region show different currencies
    How is this possible? is there any solution?
    im using Purchasing api
    Thanks!
     
  2. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Can you share your debug output? Or some screenshots? What specific devices? This should not be possible. Please Debug.Log the values and provide the XCode logs as an attachment here, specify what I would be looking for in the logs (the syntax of your Debug.Log statement for example) https://forum.unity.com/threads/how-to-capturing-device-logs-on-ios.529920/ . Also please attach your purchasing script.
     
  3. akiba_unity427

    akiba_unity427

    Joined:
    Jun 23, 2020
    Posts:
    4
    I am Japanese.
    Please excuse my bad English.

    It seems to be a similar problem, so I will post it here.

    I'm currently testing on iOS13.7 and iPadOS14.3.

    In some cases, USD is returned for the currency code obtained by using the IAP plugin function on iOS devices.
    The value returned here is expected to be JPY.

    Tap the icon in the upper right corner of the App Store to open the account dialog.
    User Name (top item) > Country or Region Name > Change Country or Region > Japan

    Make the above settings on each device.
    On iOS13.7, the expected value of JPY will be returned.
    On iPad OS 14.3, the wrong value of USD is returned.

    Is this difference in the OS version?
    Or is there a problem with the device specific settings?

    It is difficult to provide the source code due to the public nature of the site.

    Thank you.
     

    Attached Files:

    ilmario likes this.
  4. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    What version of IAP? It sounds like a possible issue with the iOS version, we will need to check. If we find the cause and are able to address it, we would include the update in a future release.
     
  5. akiba_unity427

    akiba_unity427

    Joined:
    Jun 23, 2020
    Posts:
    4
    The version of IAP used in the delivered app is "com.unity.purchasing": "2.0.6".

    2.x.x latest 2.2.2
    3.x.x latest 3.2.2

    I have tried to check the above versions up in order, but the result is the same and does not return the expected value in iPadOS14.3.
     
  6. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    You will want to upgrade IAP and release a new version of your app. The latest version is 3.2.3. How are you checking the version of IAP on the released app?
     
  7. akiba_unity427

    akiba_unity427

    Joined:
    Jun 23, 2020
    Posts:
    4
    I was on vacation last week, so it took me a while to check and respond.

    IAP version information was checked in ". /Packages/manifest.json" for the IAP version information.

    In the released application ". /Packages/manifest.json" has the following.
    "com.unity.purchasing": "2.0.6",

    However, the StandardPurchasingModule class was defined as 1.20.0.
    public const string k_PackageVersion = "1.20.0";


    I upgraded the IAP plugin to 3.2.3 today and checked it out.
    However, when I ran it on iPadOS 14.3, it did not return the expected values.
    In iOS13.7, I have not checked all countries, but it returns values like Australia "AUD", Canada "CAD", and so on.
    In iPad OS 14.3, the value "USD" is always returned when selecting the same as in iOS 13.7.

    In this case, the following variables are being logged to the console.
    StandardPurchasingModule.Instance().Version

    It appears that IAP version 3.2.3 is being used, but the situation is that the expected value is not being returned.

    I'm checking this by building an app that just adds logs to the released app.

    Is there anything else I should check?

    The string that cannot be published in the attachment is set to "***".
     

    Attached Files:

    ilmario likes this.
  8. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
  9. akiba_unity427

    akiba_unity427

    Joined:
    Jun 23, 2020
    Posts:
    4
    To be precise, I'm building it on a build server machine.
    I'm outputting the XCode project in Unity, building it with the "Provisioning Profile" for Distribution, and uploading the resulting ".ipa" file to DeployGate.
    I don't think I can check "Debug.Log" from XCode because I install and check it from DeployGate.
    Distribution cannot be built on the development terminal provided by the company to the individual.
    As far as I know, "Provisioning Profile" must be built in Distribution.
    The "Provisioning Profile" for Development fails in the initialization process of the billing plugin.
    For this reason, we do not build directly from XCode to iPad.
    Currently, it's difficult to check the build from XCode from the console.

    For Unity's build options, I have it set to development build and set it as follows.
    Code (CSharp):
    1. BuildPlayerOptions options = new BuildPlayerOptions();
    2. options.locationPathName = "bin/ios";
    3. options.scenes = GetScenes();
    4. options.target = BuildTarget.iOS;
    5. options.targetGroup = BuildTargetGroup.iOS;
    6. options.options = BuildOptions.SymlinkLibraries | BuildOptions.Development | BuildOptions.ConnectWithProfiler;
    7. BuildReport buildReport = BuildPipeline.BuildPlayer(options);
    How do you currently check it?
    By using NSLog in Objective-C, I can see the contents output by "Debug.Log" in the Mac console.
    The output log is filtered by [Purchase].

    In the attached file, 3.2.3 at the end of the third line is the IAP version value, but is it different from what you want me to check?
    iPadOS14.3_Problem_log_IAP_v3.2.3.txt

    Here is the code used, to the best of my ability.

    BaseUnityIAPService.cs
    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Purchasing;
    3.  
    4. public class BaseUnityIAPService : MonoBehaviour, IPurchaseService, IStoreListener {
    5.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions) {
    6.         Debug.Log($"[Purchase]BaseUnityIAPService.OnInitialized Start IAP version: {UnityEngine.Purchasing.StandardPurchasingModule.Instance().Version}");
    7.         storeController = controller;
    8.         storeExtensionProvider = extensions;
    9.  
    10.         #if UNITY_IOS
    11.         var products = storeController.products.all;
    12.         if (products != null && products.Length > 0 && products [0].metadata.isoCurrencyCode != null) {
    13.             this.manager.CountryCode = products [0].metadata.isoCurrencyCode;
    14.         }
    15.         Debug.Log(string.Concat("[Purchase]", $"*** CountryCode: {this.manager.CountryCode} {((products == null) ? "products is null" : "products != null")}"));
    16.         Debug.Log(string.Concat("[Purchase]", $"*** localizedPriceString: {products [0].metadata.localizedPriceString}"));
    17.  
    18.         if (products != null) {
    19.             Debug.Log(string.Concat("[Purchase]", $"products.Length: {products.Length}"));
    20.             if (products.Length > 0) {
    21.                 for(var i = 0; i < products.Length; ++i) {
    22.                     var outputStr = $"{((products[i].metadata.isoCurrencyCode == null) ? $"products[i].metadata.isoCurrencyCode is null" : $"prfoducts[i].metadata.isoCurrencyCode != null")}\n";
    23.                     outputStr += string.Concat($"products[{i}].metadata.localizedTitle: " + products[i].metadata.localizedTitle, "\n");
    24.                     outputStr += string.Concat($"products[{i}].metadata.localizedDescription: " + products[i].metadata.localizedDescription, "\n");
    25.                     outputStr += string.Concat($"products[{i}].metadata.localizedPriceString: " + products[i].metadata.localizedPriceString, "\n");
    26.                     Debug.Log(string.Concat("[Purchase]", outputStr));
    27.                 }
    28.             }
    29.         }
    30.         #endif
    31.  
    32.         this.manager.ReceivedtPurchaseProductListForUnityIAP ();
    33.  
    34.         Debug.Log("[Purchase]Purchasing***");
    35.     }
    36. }
    Debug.cs
    Code (CSharp):
    1. #if !UNITY_EDITOR
    2. #define DEBUG_LOG_OVERWRAP
    3. #endif
    4.  
    5. using System;
    6. using System.Runtime.InteropServices;
    7. using UnityEngine;
    8.  
    9. #if DEBUG_LOG_OVERWRAP
    10. public static class Debug
    11. {
    12.     static public void Log( object message ){
    13.         if( IsEnable() ){
    14.             UnityEngine.Debug.Log( message );
    15. #if UNITY_IOS
    16.             UnityNSLog( message.ToString() );
    17. #endif
    18.         }
    19.     }
    20.  
    21.     static bool IsEnable(){ return UnityEngine.Debug.isDebugBuild; }
    22.  
    23. #if UNITY_IOS
    24.     [DllImport("__Internal")]
    25.     private static extern void UnityNSLog(string message);
    26. #endif
    27. }
    28. #endif
    MyAppController.mm
    Code (CSharp):
    1. #import "UnityAppController.h"
    2. #import <Foundation/Foundation.h>
    3. #include "Adjust.h"
    4.  
    5. @interface MyAppController : UnityAppController
    6. + (void)RemoveUrlUserDefaults;
    7. @end
    8.  
    9. extern "C"{
    10.     void UnityNSLog(const char* message) {
    11.         NSLog (@"%s", message);
    12.     }
    13. }
    Because Japanese is included in the prohibited terms, the corresponding parts are marked with "***".
     
  10. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @akiba_unity427 Thank for the info, we will check. If we can confirm the issue, we will likely include an update in an upcoming release.
     
  11. GG_JamesBattersby

    GG_JamesBattersby

    Joined:
    Jan 3, 2021
    Posts:
    8
    Hi. Is there any update on this as I see a similar issue.

    Using Unity IAP 4.0.3, Unity 2020.3.2f1:
    • on iPad OS 14.7.1 prices are returned in USD with USD values
    • on iPhone OS 14.2 prices are returned in GBP with USD values
    Afterthought: my IAP products "Ready to Submit". Is that likely to cause issues?
     
    Last edited: Sep 3, 2021
  12. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    "Ready to Submit" is the proper status. This looks to be an Apple issue, we have contacted them. Can you share the code you are using and steps to reproduce, to confirm?
     
  13. GG_JamesBattersby

    GG_JamesBattersby

    Joined:
    Jan 3, 2021
    Posts:
    8
    @JeffDUnity3D Here's the relevant code. This works fine on Android so I think you're correct that this is an Apple issue. This code is used in conjunction with Playfab so the ItemIds are set through that system. I've confirmed that the names are identical. I submitted a beta build to Apple thinking that the "Ready to Submit" was an indicator that the products needed to be in place for them to work. It was rejected however under Section 2.1:

    We found that your in-app purchase products exhibited one or more bugs when reviewed on iPad running iOS 14.7.1 on Wi-Fi.

    Specifically, we were unable to make a purchase due to an error.


    Code (CSharp):
    1. namespace gg.core.server
    2. {
    3.   public abstract class ServerInterfacePlayFab : ServerInterface
    4. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    5.     , IStoreListener
    6. #endif
    7.   {
    8. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    9.     private IStoreController storeController;
    10.     private IExtensionProvider extensions;
    11.     private bool initialisingStoreController;
    12.     protected Action<IAPResponse> onPurchase;
    13. #endif
    14.     public ServerInterfacePlayFab()
    15.     {
    16. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    17.       initialisingStoreController = false;
    18.       onPurchase = null;
    19. #endif
    20.     }
    21.    
    22.     public override void RefreshIAP(string[] storeIds, Action<Response, IAPStore[]> onRefresh)
    23.     {    
    24.       IAPStore[] stores = new IAPStore[storeIds.Length];
    25.  
    26.       IEnumerator GetStores()
    27.       {
    28. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    29.         ConfigurationBuilder builder = null;
    30.         if (!initialisingStoreController && storeController == null)
    31.         {
    32.           initialisingStoreController = true;
    33.           builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    34.         }
    35. #endif
    36.         Dictionary<string, ItemInfo> itemInfos = new Dictionary<string, ItemInfo>();
    37.         bool gettingCatalogItems = true;
    38.         GetCatalogItemsRequest getCatalogRequest = new GetCatalogItemsRequest()
    39.         {
    40.           CatalogVersion = "default"
    41.         };
    42.         Action<GetCatalogItemsResult> getCatalogSuccess = (GetCatalogItemsResult result) =>
    43.         {
    44.           for (int i = 0; i < result.Catalog.Count; ++i)
    45.           {
    46.             ItemInfo itemInfo = new ItemInfo(result.Catalog[i]);
    47.             //TODO subscriptions
    48.             itemInfos.Add(result.Catalog[i].ItemId, itemInfo);
    49.           }
    50.           gettingCatalogItems = false;
    51.         };
    52.         Action<PlayFabError> getCatalogError = (PlayFabError error) =>
    53.         {
    54.           gettingCatalogItems = false;
    55.         };
    56.         PlayFabClientAPI.GetCatalogItems(getCatalogRequest, getCatalogSuccess, getCatalogError);
    57.         yield return new WaitUntil(() => !gettingCatalogItems);
    58.  
    59.         Response getStoreResponse = Response.None;
    60.         for (int i = 0; i < storeIds.Length; ++i)
    61.         {
    62.           bool gettingStore = true;
    63.           getStoreResponse = Response.None;
    64.  
    65.           GetStoreItemsRequest request = new GetStoreItemsRequest()
    66.           {
    67.             StoreId = storeIds[i],
    68.             CatalogVersion = "default"
    69.           };
    70.           Action<GetStoreItemsResult> onGetItemsSuccess = (GetStoreItemsResult result) =>
    71.           {
    72.             IAPStore store = new IAPStore(storeIds[i]);
    73.             for (int j = 0; j < result.Store.Count; ++j)
    74.             {
    75.               StoreItem storeItem = result.Store[j];
    76.               string costItemId = null;
    77.               uint costAmount = 0;
    78.               float realCurrencyPrice = -1;
    79.               ItemInfo itemInfo;
    80.               itemInfos.TryGetValue(storeItem.ItemId, out itemInfo);            
    81.               if (storeItem.VirtualCurrencyPrices.Count > 0)
    82.               {
    83.                 Dictionary<string, uint>.Enumerator it = storeItem.VirtualCurrencyPrices.GetEnumerator();
    84.                 while (it.MoveNext())
    85.                 {
    86.                   costItemId = it.Current.Key;
    87.                   costAmount = it.Current.Value;
    88.                   break;
    89.                 }
    90.               }
    91.               else
    92.               {
    93.                 continue;
    94.               }
    95.               IAPItem item = new IAPItem(storeIds[i], storeItem.ItemId, new IAPCost(costItemId, costAmount, realCurrencyPrice), itemInfo?.consumeImmediately ?? false);
    96.               store.AddItem(item);
    97. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    98.               if (builder != null && !item.Cost.IsVirtualCurrency())
    99.               {
    100.                 builder.AddProduct(item.ItemId, itemInfo?.productType ?? ProductType.Consumable, new IDs {
    101. #if UNITY_ANDROID && GOOGLEPLAY
    102.                   { item.ItemId, GooglePlay.Name }
    103. #elif APPSTORE
    104. #if UNITY_IOS
    105.                   { item.ItemId,  AppleAppStore.Name }
    106. #else
    107.                   { item.ItemId,  MacAppStore.Name }
    108. #endif
    109. #endif
    110.                 });
    111.               }
    112. #endif
    113.              }
    114.  
    115.             stores[i] = store;
    116.             getStoreResponse = Response.Success;
    117.             gettingStore = false;
    118.           };
    119.           Action<PlayFabError> onGetItemsFail = (PlayFabError error) =>
    120.           {
    121.             if (error.Error != PlayFabErrorCode.StoreNotFound)
    122.             {
    123.               getStoreResponse = ToResponse(error);
    124.               onRefresh(getStoreResponse, null);
    125.             }
    126.             gettingStore = false;
    127.           };
    128.           PlayFabClientAPI.GetStoreItems(request, onGetItemsSuccess, onGetItemsFail);
    129.           yield return new WaitUntil(() => !gettingStore);
    130.           if (!getStoreResponse)
    131.           {
    132.             break;
    133.           }
    134.         }
    135. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    136.         if (builder != null)
    137.         {
    138.           UnityPurchasing.Initialize(this, builder);
    139.         }
    140. #endif
    141.         onRefresh(Response.Success, stores);
    142.       }
    143.       App.Instance.StartCoroutine(GetStores());
    144.     }
    145.    
    146. #if (UNITY_ANDROID && GOOGLEPLAY) || ((UNITY_IOS || UNITY_STANDALONE_OSX) && APPSTORE)
    147.     public override bool IAPInitializing
    148.     {
    149.       get
    150.       {
    151.         return initialisingStoreController;
    152.       }
    153.     }
    154.  
    155.     public override bool IAPInitialized
    156.     {
    157.       get
    158.       {
    159.         return !initialisingStoreController && storeController != null;
    160.       }
    161.     }
    162.  
    163.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    164.     {
    165.       storeController = controller;
    166.       this.extensions = extensions;
    167.       initialisingStoreController = false;
    168.     }
    169.  
    170.     public void OnInitializeFailed(InitializationFailureReason error)
    171.     {
    172.       initialisingStoreController = false;
    173.     }
    174.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs purchaseEvent)
    175.     {
    176.       if (storeController == null
    177.         || purchaseEvent.purchasedProduct == null
    178.         || string.IsNullOrEmpty(purchaseEvent.purchasedProduct.receipt))
    179.       {
    180.         onPurchase(new IAPResponse(Error.Unknown, "IAP Not Initialized"));
    181.         return PurchaseProcessingResult.Complete;
    182.       }
    183.       ValidatePurchase(purchaseEvent.purchasedProduct);
    184.       return PurchaseProcessingResult.Complete;
    185.     }
    186.  
    187.     protected abstract void ValidatePurchase(Product product);
    188.  
    189.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    190.     {
    191.       onPurchase(new IAPResponse(ToErrorCode(failureReason), failureReason.ToString()));
    192.     }
    193.  
    194.     public override string GetLocalizedPriceString(string itemId)
    195.     {
    196.       if (!IAPInitialized)
    197.       {
    198.         return null;
    199.       }
    200.       return storeController.products.WithStoreSpecificID(itemId)?.metadata.localizedPriceString ?? null;
    201.     }
    202. #endif
    203.   }
    204. }
     
  14. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    @GG_JamesBattersby You are raising an exception in ProcessPurchase yet returning Complete? I might suggest instead to use IAPManager.cs in the Sample IAP Project v2. Also, is Unity IAP on iOS working for you in TestFlight? If not, then don't publish. I would recommend to get documented scripts working first, before adding your customizations like you have. If the basics don't work, something more complex won't either.
     
  15. GG_JamesBattersby

    GG_JamesBattersby

    Joined:
    Jan 3, 2021
    Posts:
    8
    @JeffDUnity3D Last I checked it had been working, at least within a Sandbox environment. I'll check in TestFlight tomorrow now that I've cleared the current workload.

    As for returning Complete, the only alternative is Pending. My interpretation was that the purchase process was "complete", regardless of whether it was successful or not.
     
  16. JeffDUnity3D

    JeffDUnity3D

    Joined:
    May 2, 2017
    Posts:
    14,446
    Perhaps I don't follow. You're in ProcessPurchase, a success callback that can only happen when you have a valid connection with the store.
     
  17. GG_JamesBattersby

    GG_JamesBattersby

    Joined:
    Jan 3, 2021
    Posts:
    8
    @JeffDUnity3D I can confirm that the purchases go through on TestFlight - TLDR it's functioning but not as expected.

    The reason the prices were appearing as USD whilst in the UK was because the testing account wasn't signed into their Apple ID so it defaults to USD. Once signed in it fetches the prices in GBP, presumably Apple indicates that's the region it should be associated with. Presumably there isn't a way to fetch localised prices based on the device region if an Apple account isn't available?

    As for the strange prices there seems to be some inconsistencies but it is at least consistent between the iOS platforms.
    • Some prices appear to be set on the Alternate Tiers in Apple's pricing system.
      • Tiers 1, 2, 3 and 5 are returning as Alternate Tier 1, 2, 3 and 5 respectively
    • Some prices appear to be returning different tier prices
      • Tier 12 is returning as Tier 14, 30 as 33, 54 as 55, 60 as 62
    The interesting thing is that the decimal values of these prices match the USD value of the tier that I have selected for these products. This is also probably the cause of some strange behaviour when validating the receipts on Playfab but that's a separate issue.
     
  18. r_at_pg

    r_at_pg

    Joined:
    Feb 18, 2021
    Posts:
    2
    @arivasaim and @akiba_unity427, the problem may have been with your Sandbox Apple ID. When testing development builds on iOS, the device's region setting is not used for in-app purchases. Instead, the Sandbox Apple ID's region is used. You can set your Sandbox Apple ID's region in App Store Connect.

    See Apple's documentation:
    To test in-app purchases in multiple regions using the same Sandbox Apple ID, change the Sandbox Apple ID's App Store Country or Region setting in App Store Connect.​
     
    unity_key4 and JeffDUnity3D like this.
  19. unity_key4

    unity_key4

    Joined:
    May 19, 2022
    Posts:
    6
    this helps me too
     
  20. Arnaud_Gorain

    Arnaud_Gorain

    Unity Technologies

    Joined:
    Jun 28, 2022
    Posts:
    183
    This thread is now closed. Feel free to reach out via a new thread if you encounter further issues.
    Thanks!
     
Thread Status:
Not open for further replies.