From be35b56c0e40b1b30e59e26422a9c851f0fe269d Mon Sep 17 00:00:00 2001 From: Leonardo Bilck Date: Tue, 8 Sep 2026 20:03:55 -0300 Subject: [PATCH] Parse StoreProduct prices culture-invariantly The Android bridge sends price and trialPeriodPrice as strings (BigDecimal.toString()), and Convert.ToDouble(object) parses strings with the device culture, so "199.99" became 19999 on pt-BR / de-DE devices. Parse with the invariant culture and accept both the JSON number the iOS bridge sends and the string the Android bridge sends. --- Runtime/Superwall.cs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/Runtime/Superwall.cs b/Runtime/Superwall.cs index 0cb96d6..a4b81fb 100644 --- a/Runtime/Superwall.cs +++ b/Runtime/Superwall.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using UnityEngine; using Superwall.Internal; @@ -708,6 +709,27 @@ private static Experiment DeserializeExperiment(Dictionary dict) return exp; } + /// + /// Parses a bridge number that may arrive as a JSON number (iOS) or as a string (Android sends + /// BigDecimal.toString(), e.g. "199.99"). Convert.ToDouble(object) parses strings with the + /// device culture, so on a pt-BR or de-DE device "199.99" became 19999. Always invariant. + /// + private static double ToDoubleInvariant(object value) + { + switch (value) + { + case null: return 0; + case double d: return d; + case float f: return f; + case int i: return i; + case long l: return l; + case string text: + return double.TryParse(text, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) ? parsed : 0; + default: + return Convert.ToDouble(value, CultureInfo.InvariantCulture); + } + } + private static StoreProduct DeserializeStoreProduct(Dictionary dict) { if (dict == null) return new StoreProduct(); @@ -734,7 +756,7 @@ private static StoreProduct DeserializeStoreProduct(Dictionary d p.TrialPeriodEndDate = dict.ContainsKey("trialPeriodEndDate") ? dict["trialPeriodEndDate"] as string : null; p.TrialPeriodEndDateString = dict.ContainsKey("trialPeriodEndDateString") ? dict["trialPeriodEndDateString"] as string : null; p.LocalizedTrialPeriodPrice = dict.ContainsKey("localizedTrialPeriodPrice") ? dict["localizedTrialPeriodPrice"] as string : null; - p.TrialPeriodPrice = dict.ContainsKey("trialPeriodPrice") ? Convert.ToDouble(dict["trialPeriodPrice"]) : 0; + p.TrialPeriodPrice = dict.ContainsKey("trialPeriodPrice") ? ToDoubleInvariant(dict["trialPeriodPrice"]) : 0; p.TrialPeriodDays = dict.ContainsKey("trialPeriodDays") ? Convert.ToInt32(dict["trialPeriodDays"]) : 0; p.TrialPeriodDaysString = dict.ContainsKey("trialPeriodDaysString") ? dict["trialPeriodDaysString"] as string : null; p.TrialPeriodWeeks = dict.ContainsKey("trialPeriodWeeks") ? Convert.ToInt32(dict["trialPeriodWeeks"]) : 0; @@ -750,7 +772,7 @@ private static StoreProduct DeserializeStoreProduct(Dictionary d p.CurrencyCode = dict.ContainsKey("currencyCode") ? dict["currencyCode"] as string : null; p.IsFamilyShareable = dict.ContainsKey("isFamilyShareable") && dict["isFamilyShareable"] is bool ifs && ifs; p.RegionCode = dict.ContainsKey("regionCode") ? dict["regionCode"] as string : null; - p.Price = dict.ContainsKey("price") ? Convert.ToDouble(dict["price"]) : 0; + p.Price = dict.ContainsKey("price") ? ToDoubleInvariant(dict["price"]) : 0; if (dict.ContainsKey("entitlements") && dict["entitlements"] is List entList) {