diff --git a/.circleci/config.yml b/.circleci/config.yml index 6616e7c1..39ef5fde 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -84,8 +84,10 @@ commands: command: | mv IntegrationTests/Assets/Editor/CIEditorScript.cs IntegrationTests/Assets/Editor/CIEditorScript.cs.break mv IntegrationTests/Assets/Main.cs IntegrationTests/Assets/Main.cs.break - # rename all the .cs files in APITests to .cs.break - for file in IntegrationTests/Assets/APITests/*.cs ; do mv "$file" "${file%%}.break" ; done + # Hide API fixtures and tests until the SDK assemblies have been imported. + find IntegrationTests/Assets/APITests IntegrationTests/Assets/Tests \ + -type f \( -name "*.cs" -o -name "*.asmdef" \) \ + -exec sh -c 'for file do mv "$file" "$file.break"; done' sh {} + - run: @@ -117,8 +119,9 @@ commands: command: | mv IntegrationTests/Assets/Editor/CIEditorScript.cs.break IntegrationTests/Assets/Editor/CIEditorScript.cs mv IntegrationTests/Assets/Main.cs.break IntegrationTests/Assets/Main.cs - # rename all the .cs.break files in APITests to .cs - for file in IntegrationTests/Assets/APITests/*.cs.break ; do mv "$file" "${file%.*}" ; done + find IntegrationTests/Assets/APITests IntegrationTests/Assets/Tests \ + -type f -name "*.break" \ + -exec sh -c 'for file do mv "$file" "${file%.break}"; done' sh {} + perform-build: description: "Builds Unity project" @@ -352,6 +355,29 @@ jobs: - store_artifacts: path: IntegrationTests/Builds/Android/Android.apk + test-edit-mode: + executor: unity + steps: + - checkout + - attach_workspace: + at: . + + - unity/prepare-env: + project-path: IntegrationTests + + - import-package + + - unity/test: + project-path: IntegrationTests + test-platform: editmode + custom-parameters: -disable-assembly-updater + + - store_artifacts: + path: IntegrationTests/editmode-results.xml + + - store_artifacts: + path: IntegrationTests/editmode-junit-results.xml + build-integration-tests-ios: executor: unity-ios parameters: @@ -518,6 +544,10 @@ workflows: context: unity requires: - export-package + - test-edit-mode: + context: unity + requires: + - export-package - build-integration-tests-ios: variant: spm context: unity @@ -543,6 +573,7 @@ workflows: requires: - check-android-keep-annotations - build-integration-tests-android + - test-edit-mode - build-subtester-android - build-subtester-ios - archive-ios diff --git a/IntegrationTests/Assets/Tests.meta b/IntegrationTests/Assets/Tests.meta new file mode 100644 index 00000000..3bcc8ae1 --- /dev/null +++ b/IntegrationTests/Assets/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 8057e675b41f47818c383db9afa1668e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode.meta b/IntegrationTests/Assets/Tests/EditMode.meta new file mode 100644 index 00000000..c477ba34 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ecb0fe24604f4e649d25c13636f86a72 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs b/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs new file mode 100644 index 00000000..a7e6efba --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs @@ -0,0 +1,532 @@ +using System.Collections.Generic; +using System.Reflection; +using NUnit.Framework; +using RevenueCat.SimpleJSON; +using UnityEngine; +using UnityEngine.TestTools; + +namespace RevenueCat.Tests +{ + public class CallbackResponseTests + { + private const string MinimalCustomerInfoJson = + "{\"entitlements\":{\"all\":{},\"active\":{}}," + + "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + + "\"requestDateMillis\":1700000000001,\"allExpirationDatesMillis\":{}," + + "\"allPurchaseDatesMillis\":{},\"nonSubscriptionTransactions\":[]," + + "\"subscriptionsByProductIdentifier\":{}}"; + + private const string ErrorJson = + "{\"error\":{\"message\":\"Something went wrong\",\"code\":3," + + "\"underlyingErrorMessage\":\"Backend error\",\"readableErrorCode\":\"UNKNOWN_ERROR\"}}"; + + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void GetProductsForwardsArgumentsAndDeliversResponseOnce() + { + var identifiers = new[] { "monthly", "annual" }; + List receivedProducts = null; + Purchases.Error receivedError = null; + var callbackCount = 0; + + _purchases.GetProducts(identifiers, (products, error) => + { + callbackCount++; + receivedProducts = products; + receivedError = error; + }, "inapp"); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.GetProducts), 2); + Assert.That(invocation.Arguments[0], Is.SameAs(identifiers)); + Assert.That(invocation.Arguments[1], Is.EqualTo("inapp")); + + const string response = + "{\"products\":[{\"title\":\"Lifetime\",\"identifier\":\"lifetime\"," + + "\"description\":\"Lifetime access\",\"price\":99.99,\"priceString\":\"$99.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"NON_SUBSCRIPTION\"}]}"; + SendNativeResponse("_receiveProducts", response); + SendNativeResponse("_receiveProducts", response); + + Assert.That(callbackCount, Is.EqualTo(1)); + Assert.That(receivedError, Is.Null); + Assert.That(receivedProducts, Has.Count.EqualTo(1)); + Assert.That(receivedProducts[0].Identifier, Is.EqualTo("lifetime")); + } + + [Test] + public void GetProductsDeliversNativeError() + { + List receivedProducts = null; + Purchases.Error receivedError = null; + + _purchases.GetProducts(new[] { "missing" }, (products, error) => + { + receivedProducts = products; + receivedError = error; + }); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.GetProducts), 2); + Assert.That(invocation.Arguments[1], Is.EqualTo("subs")); + + SendNativeResponse("_receiveProducts", + "{\"error\":{\"message\":\"Product missing\",\"code\":5," + + "\"underlyingErrorMessage\":\"Store returned no product\"," + + "\"readableErrorCode\":\"PRODUCT_NOT_AVAILABLE_FOR_PURCHASE_ERROR\"}}"); + + Assert.That(receivedProducts, Is.Null); + Assert.That(receivedError, Is.Not.Null); + Assert.That(receivedError.Code, Is.EqualTo(5)); + Assert.That(receivedError.Message, Is.EqualTo("Product missing")); + } + + [Test] + public void GetOfferingsCallsWrapperAndDeliversResponseOnce() + { + Purchases.Offerings receivedOfferings = null; + Purchases.Error receivedError = null; + var callbackCount = 0; + + _purchases.GetOfferings((offerings, error) => + { + callbackCount++; + receivedOfferings = offerings; + receivedError = error; + }); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetOfferings), 0); + + const string response = "{\"offerings\":{\"all\":{},\"current\":null}}"; + SendNativeResponse("_getOfferings", response); + SendNativeResponse("_getOfferings", response); + + Assert.That(callbackCount, Is.EqualTo(1)); + Assert.That(receivedError, Is.Null); + Assert.That(receivedOfferings, Is.Not.Null); + Assert.That(receivedOfferings.All, Is.Empty); + Assert.That(receivedOfferings.Current, Is.Null); + } + + [Test] + public void CanMakePaymentsNormalizesNullFeaturesAndDeliversError() + { + var receivedCanMakePayments = true; + Purchases.Error receivedError = null; + + _purchases.CanMakePayments(null, (canMakePayments, error) => + { + receivedCanMakePayments = canMakePayments; + receivedError = error; + }); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.CanMakePayments), 1); + Assert.That((Purchases.BillingFeature[])invocation.Arguments[0], Is.Empty); + + SendNativeResponse("_canMakePayments", + "{\"error\":{\"message\":\"Billing unavailable\",\"code\":2," + + "\"underlyingErrorMessage\":\"No store\",\"readableErrorCode\":\"STORE_PROBLEM_ERROR\"}}"); + + Assert.That(receivedCanMakePayments, Is.False); + Assert.That(receivedError, Is.Not.Null); + Assert.That(receivedError.Code, Is.EqualTo(2)); + } + + [Test] + public void GetCustomerInfoDeliversResponse() + { + Purchases.CustomerInfo receivedInfo = null; + Purchases.Error receivedError = null; + + _purchases.GetCustomerInfo((info, error) => + { + receivedInfo = info; + receivedError = error; + }); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetCustomerInfo), 0); + + SendNativeResponse("_getCustomerInfo", "{\"customerInfo\":" + MinimalCustomerInfoJson + "}"); + + Assert.That(receivedError, Is.Null); + Assert.That(receivedInfo, Is.Not.Null); + Assert.That(receivedInfo.OriginalAppUserId, Is.EqualTo("user_1")); + } + + [Test] + public void GetCustomerInfoDeliversNativeError() + { + Purchases.CustomerInfo receivedInfo = null; + Purchases.Error receivedError = null; + + _purchases.GetCustomerInfo((info, error) => + { + receivedInfo = info; + receivedError = error; + }); + + SendNativeResponse("_getCustomerInfo", ErrorJson); + + Assert.That(receivedInfo, Is.Null); + Assert.That(receivedError, Is.Not.Null); + Assert.That(receivedError.Code, Is.EqualTo(3)); + } + + [Test] + public void LogInForwardsAppUserIdAndDeliversCreatedFlag() + { + Purchases.CustomerInfo receivedInfo = null; + var receivedCreated = false; + Purchases.Error receivedError = null; + + _purchases.LogIn("new_user", (info, created, error) => + { + receivedInfo = info; + receivedCreated = created; + receivedError = error; + }); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.LogIn), 1); + Assert.That(invocation.Arguments[0], Is.EqualTo("new_user")); + + SendNativeResponse("_logIn", "{\"customerInfo\":" + MinimalCustomerInfoJson + ",\"created\":true}"); + + Assert.That(receivedError, Is.Null); + Assert.That(receivedCreated, Is.True); + Assert.That(receivedInfo, Is.Not.Null); + } + + [Test] + public void LogInDeliversNativeError() + { + Purchases.CustomerInfo receivedInfo = null; + var receivedCreated = true; + Purchases.Error receivedError = null; + + _purchases.LogIn("new_user", (info, created, error) => + { + receivedInfo = info; + receivedCreated = created; + receivedError = error; + }); + + SendNativeResponse("_logIn", ErrorJson); + + Assert.That(receivedInfo, Is.Null); + Assert.That(receivedCreated, Is.False); + Assert.That(receivedError, Is.Not.Null); + } + + [Test] + public void LogOutDeliversCustomerInfo() + { + Purchases.CustomerInfo receivedInfo = null; + + _purchases.LogOut((info, error) => receivedInfo = info); + + AssertLastInvocation(nameof(IPurchasesWrapper.LogOut), 0); + + SendNativeResponse("_logOut", "{\"customerInfo\":" + MinimalCustomerInfoJson + "}"); + + Assert.That(receivedInfo, Is.Not.Null); + } + + [Test] + public void RestorePurchasesDeliversCustomerInfo() + { + Purchases.CustomerInfo receivedInfo = null; + + _purchases.RestorePurchases((info, error) => receivedInfo = info); + + AssertLastInvocation(nameof(IPurchasesWrapper.RestorePurchases), 0); + + SendNativeResponse("_restorePurchases", "{\"customerInfo\":" + MinimalCustomerInfoJson + "}"); + + Assert.That(receivedInfo, Is.Not.Null); + } + + [Test] + public void SyncPurchasesWithCallbackDeliversCustomerInfo() + { + Purchases.CustomerInfo receivedInfo = null; + + _purchases.SyncPurchases((info, error) => receivedInfo = info); + + AssertLastInvocation(nameof(IPurchasesWrapper.SyncPurchases), 0); + + SendNativeResponse("_syncPurchases", "{\"customerInfo\":" + MinimalCustomerInfoJson + "}"); + + Assert.That(receivedInfo, Is.Not.Null); + } + + [Test] + public void SyncAttributesAndOfferingsIfNeededDeliversOfferings() + { + Purchases.Offerings receivedOfferings = null; + + _purchases.SyncAttributesAndOfferingsIfNeeded((offerings, error) => receivedOfferings = offerings); + + AssertLastInvocation(nameof(IPurchasesWrapper.SyncAttributesAndOfferingsIfNeeded), 0); + + SendNativeResponse("_syncAttributesAndOfferingsIfNeeded", "{\"offerings\":{\"all\":{},\"current\":null}}"); + + Assert.That(receivedOfferings, Is.Not.Null); + Assert.That(receivedOfferings.All, Is.Empty); + } + + [Test] + public void CheckTrialOrIntroductoryPriceEligibilityDeliversDictionary() + { + // IPurchasesWrapper.CheckTrialOrIntroductoryPriceEligibility takes a single array parameter, so the + // spy's `params object[]` forwarding aliases the array itself as Invocations.Arguments rather than + // wrapping it as a single element. + var identifiers = new[] { "monthly", "annual" }; + Dictionary receivedEligibility = null; + + _purchases.CheckTrialOrIntroductoryPriceEligibility(identifiers, eligibility => receivedEligibility = eligibility); + + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, + Is.EqualTo(nameof(IPurchasesWrapper.CheckTrialOrIntroductoryPriceEligibility))); + Assert.That(_wrapper.LastInvocation.Arguments, Is.SameAs(identifiers)); + + SendNativeResponse("_checkTrialOrIntroductoryPriceEligibility", + "{\"monthly\":{\"status\":1,\"description\":\"eligible\"}}"); + + Assert.That(receivedEligibility, Is.Not.Null); + Assert.That((int)receivedEligibility["monthly"].Status, Is.EqualTo(1)); + Assert.That(receivedEligibility["monthly"].Description, Is.EqualTo("eligible")); + } + + [Test] + public void GetStorefrontDeliversPopulatedStorefront() + { + Purchases.Storefront receivedStorefront = null; + + _purchases.GetStorefront(storefront => receivedStorefront = storefront); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetStorefront), 0); + + SendNativeResponse("_receiveStorefront", "{\"countryCode\":\"US\"}"); + + Assert.That(receivedStorefront, Is.Not.Null); + Assert.That(receivedStorefront.CountryCode, Is.EqualTo("US")); + } + + [Test] + public void GetStorefrontReturnsNullForEmptyObject() + { + Purchases.Storefront receivedStorefront = new Purchases.Storefront("non-null-sentinel"); + + _purchases.GetStorefront(storefront => receivedStorefront = storefront); + + SendNativeResponse("_receiveStorefront", "{}"); + + Assert.That(receivedStorefront, Is.Null); + } + + [Test] + public void GetStorefrontReturnsNullWhenCountryCodeMissing() + { + Purchases.Storefront receivedStorefront = new Purchases.Storefront("non-null-sentinel"); + + _purchases.GetStorefront(storefront => receivedStorefront = storefront); + + LogAssert.Expect(LogType.Error, "StorefrontCallback received null countryCode"); + SendNativeResponse("_receiveStorefront", "{\"foo\":\"bar\"}"); + + Assert.That(receivedStorefront, Is.Null); + } + + [Test] + public void GetPromotionalOfferDeliversOffer() + { + var storeProduct = CreateStoreProduct(); + var discount = CreateDiscount(); + Purchases.PromotionalOffer receivedOffer = null; + + _purchases.GetPromotionalOffer(storeProduct, discount, (offer, error) => receivedOffer = offer); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.GetPromotionalOffer), 2); + Assert.That(invocation.Arguments[0], Is.EqualTo(storeProduct.Identifier)); + Assert.That(invocation.Arguments[1], Is.EqualTo(discount.Identifier)); + + SendNativeResponse("_getPromotionalOffer", + "{\"identifier\":\"promo\",\"keyIdentifier\":\"key\",\"nonce\":\"nonce\"," + + "\"signature\":\"sig\",\"timestamp\":1700000000000}"); + + Assert.That(receivedOffer, Is.Not.Null); + Assert.That(receivedOffer.Identifier, Is.EqualTo("promo")); + } + + [Test] + public void GetPromotionalOfferDeliversNativeError() + { + var storeProduct = CreateStoreProduct(); + var discount = CreateDiscount(); + Purchases.PromotionalOffer receivedOffer = null; + Purchases.Error receivedError = null; + + _purchases.GetPromotionalOffer(storeProduct, discount, (offer, error) => + { + receivedOffer = offer; + receivedError = error; + }); + + SendNativeResponse("_getPromotionalOffer", ErrorJson); + + Assert.That(receivedOffer, Is.Null); + Assert.That(receivedError, Is.Not.Null); + } + + [Test] + public void GetCurrentOfferingForPlacementDeliversNullWhenNoOfferingKey() + { + Purchases.Offering receivedOffering = CreateOffering(); + Purchases.Error receivedError = new Purchases.Error(JSONNode.Parse( + "{\"message\":\"m\",\"code\":1,\"underlyingErrorMessage\":\"u\",\"readableErrorCode\":\"r\"}")); + + _purchases.GetCurrentOfferingForPlacement("onboarding", (offering, error) => + { + receivedOffering = offering; + receivedError = error; + }); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetCurrentOfferingForPlacement), 1); + + SendNativeResponse("_getCurrentOfferingForPlacement", "{}"); + + Assert.That(receivedOffering, Is.Null); + Assert.That(receivedError, Is.Null); + } + + [Test] + public void GetCurrentOfferingForPlacementDeliversOffering() + { + Purchases.Offering receivedOffering = null; + + _purchases.GetCurrentOfferingForPlacement("onboarding", (offering, error) => receivedOffering = offering); + + SendNativeResponse("_getCurrentOfferingForPlacement", + "{\"offering\":{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[]}}"); + + Assert.That(receivedOffering, Is.Not.Null); + Assert.That(receivedOffering.Identifier, Is.EqualTo("default")); + } + + [Test] + public void GetCurrentOfferingForPlacementDeliversErrorWhenOfferingKeyPresent() + { + Purchases.Offering receivedOffering = CreateOffering(); + Purchases.Error receivedError = null; + + _purchases.GetCurrentOfferingForPlacement("onboarding", (offering, error) => + { + receivedOffering = offering; + receivedError = error; + }); + + SendNativeResponse("_getCurrentOfferingForPlacement", + "{\"offering\":{},\"error\":{\"message\":\"No offering\",\"code\":9," + + "\"underlyingErrorMessage\":\"none\",\"readableErrorCode\":\"NOT_FOUND\"}}"); + + Assert.That(receivedOffering, Is.Null); + Assert.That(receivedError, Is.Not.Null); + Assert.That(receivedError.Code, Is.EqualTo(9)); + } + + [Test] + public void GetAmazonLWAConsentStatusDeliversConsent() + { + var receivedConsent = false; + Purchases.Error receivedError = null; + + _purchases.GetAmazonLWAConsentStatus((hasConsented, error) => + { + receivedConsent = hasConsented; + receivedError = error; + }); + + AssertLastInvocation(nameof(IPurchasesWrapper.GetAmazonLWAConsentStatus), 0); + + SendNativeResponse("_getAmazonLWAConsentStatus", "{\"amazonLWAConsentStatus\":true}"); + + Assert.That(receivedConsent, Is.True); + Assert.That(receivedError, Is.Null); + } + + [Test] + public void GetAmazonLWAConsentStatusDeliversNativeError() + { + var receivedConsent = true; + Purchases.Error receivedError = null; + + _purchases.GetAmazonLWAConsentStatus((hasConsented, error) => + { + receivedConsent = hasConsented; + receivedError = error; + }); + + SendNativeResponse("_getAmazonLWAConsentStatus", ErrorJson); + + Assert.That(receivedConsent, Is.False); + Assert.That(receivedError, Is.Not.Null); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + + private void SendNativeResponse(string method, string response) + { + var receiver = typeof(Purchases).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(receiver, Is.Not.Null, $"Native response receiver {method} does not exist"); + receiver.Invoke(_purchases, new object[] { response }); + } + + private static Purchases.StoreProduct CreateStoreProduct() + { + return new Purchases.StoreProduct(JSONNode.Parse( + "{\"title\":\"Monthly\",\"identifier\":\"monthly\"," + + "\"description\":\"Monthly access\",\"price\":9.99,\"priceString\":\"$9.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"SUBSCRIPTION\"}" + )); + } + + private static Purchases.Discount CreateDiscount() + { + return new Purchases.Discount(JSONNode.Parse( + "{\"identifier\":\"intro\",\"price\":4.99,\"priceString\":\"$4.99\",\"cycles\":1," + + "\"period\":\"P1M\",\"periodUnit\":\"MONTH\",\"periodNumberOfUnits\":1}" + )); + } + + private static Purchases.Offering CreateOffering() + { + return new Purchases.Offering(JSONNode.Parse( + "{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[]}" + )); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs.meta new file mode 100644 index 00000000..435ae38e --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/CallbackResponseTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: f734293e1d524b9f9c4f096f2b0d896a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs new file mode 100644 index 00000000..8e66042c --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs @@ -0,0 +1,266 @@ +using System; +using NUnit.Framework; +using RevenueCat.SimpleJSON; + +namespace RevenueCat.Tests +{ + public class JsonModelTests + { + private const string MinimalCustomerInfoJson = + "{\"entitlements\":{\"all\":{},\"active\":{}}," + + "\"activeSubscriptions\":[],\"allPurchasedProductIdentifiers\":[]," + + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + + "\"requestDateMillis\":1700000000001,\"allExpirationDatesMillis\":{}," + + "\"allPurchaseDatesMillis\":{},\"nonSubscriptionTransactions\":[]," + + "\"subscriptionsByProductIdentifier\":{}}"; + + [Test] + public void VirtualCurrenciesParsesCurrenciesByCode() + { + var response = JSONNode.Parse( + "{\"all\":{" + + "\"COIN\":{\"balance\":120,\"name\":\"Coins\",\"code\":\"COIN\",\"serverDescription\":\"Earned in game\"}," + + "\"GEM\":{\"balance\":4,\"name\":\"Gems\",\"code\":\"GEM\",\"serverDescription\":null}" + + "}}" + ); + + var virtualCurrencies = new Purchases.VirtualCurrencies(response); + + Assert.That(virtualCurrencies.All.Keys, Is.EquivalentTo(new[] { "COIN", "GEM" })); + Assert.That(virtualCurrencies.All["COIN"].Balance, Is.EqualTo(120)); + Assert.That(virtualCurrencies.All["COIN"].Name, Is.EqualTo("Coins")); + Assert.That(virtualCurrencies.All["COIN"].ServerDescription, Is.EqualTo("Earned in game")); + Assert.That(virtualCurrencies.All["GEM"].Balance, Is.EqualTo(4)); + Assert.That(virtualCurrencies.All["GEM"].ServerDescription, Is.Null); + } + + [Test] + public void SubscriptionPriceSupportsValuesAboveInt32Range() + { + var response = JSONNode.Parse( + "{\"formatted\":\"$4,294.97\",\"amountMicros\":4294970000,\"currencyCode\":\"USD\"}" + ); + + var price = new Purchases.SubscriptionOption.Price(response); + + Assert.That(price.AmountMicros, Is.EqualTo(4294970000L)); + } + + [Test] + public void StoreProductFallsBackToUnknownProductCategory() + { + var response = JSONNode.Parse( + "{\"title\":\"Lifetime\",\"identifier\":\"lifetime\",\"description\":\"Lifetime access\"," + + "\"price\":99.99,\"priceString\":\"$99.99\",\"currencyCode\":\"USD\"," + + "\"productCategory\":\"UNRECOGNIZED\"}" + ); + + var product = new Purchases.StoreProduct(response); + + Assert.That(product.Identifier, Is.EqualTo("lifetime")); + Assert.That(product.ProductCategory, Is.EqualTo(Purchases.ProductCategory.UNKNOWN)); + Assert.That(product.DefaultOption, Is.Null); + Assert.That(product.Discounts, Is.Null); + } + + [Test] + public void CustomerInfoParsesFullPayload() + { + var response = JSONNode.Parse( + "{\"entitlements\":{" + + "\"all\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + + "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + + "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + + "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}," + + "\"active\":{\"premium\":{\"identifier\":\"premium\",\"isActive\":true,\"willRenew\":true," + + "\"periodType\":\"NORMAL\",\"latestPurchaseDateMillis\":1700000000000," + + "\"originalPurchaseDateMillis\":1690000000000,\"store\":\"APP_STORE\"," + + "\"productIdentifier\":\"monthly\",\"isSandbox\":false}}}," + + "\"activeSubscriptions\":[\"monthly\"]," + + "\"allPurchasedProductIdentifiers\":[\"monthly\",\"lifetime\"]," + + "\"firstSeenMillis\":1700000000000,\"originalAppUserId\":\"user_1\"," + + "\"requestDateMillis\":1700000000001," + + "\"originalPurchaseDateMillis\":1690000000000," + + "\"latestExpirationDateMillis\":1720000000000," + + "\"managementURL\":\"https://mgmt\"," + + "\"allExpirationDatesMillis\":{\"monthly\":1720000000000,\"lifetime\":0}," + + "\"allPurchaseDatesMillis\":{\"monthly\":1700000000000,\"lifetime\":0}," + + "\"originalApplicationVersion\":\"1.0\"," + + "\"nonSubscriptionTransactions\":[{\"transactionIdentifier\":\"txn_1\"," + + "\"productIdentifier\":\"lifetime\",\"purchaseDateMillis\":1700000000000}]," + + "\"subscriptionsByProductIdentifier\":{\"monthly\":{\"productIdentifier\":\"monthly\"," + + "\"purchaseDate\":\"2023-01-01T00:00:00Z\",\"store\":\"APP_STORE\",\"isSandbox\":false," + + "\"periodType\":\"NORMAL\",\"isActive\":true,\"willRenew\":true}}}" + ); + + var customerInfo = new Purchases.CustomerInfo(response); + + Assert.That(customerInfo.OriginalAppUserId, Is.EqualTo("user_1")); + Assert.That(customerInfo.Entitlements.All["premium"].IsActive, Is.True); + Assert.That(customerInfo.Entitlements.Active.ContainsKey("premium"), Is.True); + Assert.That(customerInfo.ActiveSubscriptions, Is.EquivalentTo(new[] { "monthly" })); + Assert.That(customerInfo.AllPurchasedProductIdentifiers, Has.Count.EqualTo(2)); + Assert.That(customerInfo.OriginalPurchaseDate, Is.Not.Null); + Assert.That(customerInfo.LatestExpirationDate, Is.Not.Null); + Assert.That(customerInfo.ManagementURL, Is.EqualTo("https://mgmt")); + Assert.That(customerInfo.OriginalApplicationVersion, Is.EqualTo("1.0")); + Assert.That(customerInfo.NonSubscriptionTransactions, Has.Count.EqualTo(1)); + Assert.That(customerInfo.NonSubscriptionTransactions[0].ProductIdentifier, Is.EqualTo("lifetime")); + Assert.That(customerInfo.SubscriptionsByProductIdentifier["monthly"].ProductIdentifier, + Is.EqualTo("monthly")); + + // A millis value of exactly 0 is indistinguishable from an absent date and is parsed as null. + Assert.That(customerInfo.AllExpirationDates["monthly"], Is.Not.Null); + Assert.That(customerInfo.AllExpirationDates["lifetime"], Is.Null); + Assert.That(customerInfo.AllPurchaseDates["monthly"], Is.Not.Null); + Assert.That(customerInfo.AllPurchaseDates["lifetime"], Is.Null); + } + + [Test] + public void SubscriptionOptionParsesFullPayloadWithPricingPhases() + { + const string pricingPhaseJson = + "{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + + "\"recurrenceMode\":\"INFINITE_RECURRING\",\"billingCycleCount\":0," + + "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + + "\"offerPaymentMode\":\"SINGLE_PAYMENT\"}"; + + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[\"tag1\"],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + + "\"pricingPhases\":[" + pricingPhaseJson + "]," + + "\"fullPricePhase\":" + pricingPhaseJson + "," + + "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}," + + "\"installmentsInfo\":{\"commitmentPaymentsCount\":3,\"renewalCommitmentPaymentsCount\":1}}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.Tags, Is.EquivalentTo(new[] { "tag1" })); + Assert.That(option.PricingPhases, Has.Length.EqualTo(1)); + Assert.That(option.FullPricePhase, Is.Not.Null); + Assert.That(option.FullPricePhase.RecurrenceMode, + Is.EqualTo(Purchases.SubscriptionOption.RecurrenceMode.INFINITE_RECURRING)); + Assert.That(option.PresentedOfferingContext, Is.Not.Null); + Assert.That(option.PresentedOfferingContext.OfferingIdentifier, Is.EqualTo("default")); + Assert.That(option.OptionInstallmentsInfo, Is.Not.Null); + Assert.That(option.OptionInstallmentsInfo.CommitmentPaymentsCount, Is.EqualTo(3)); + } + + [Test] + public void SubscriptionOptionParsesMinimalPayloadWithoutOptionalFields() + { + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.PricingPhases, Is.Null); + Assert.That(option.FullPricePhase, Is.Null); + Assert.That(option.FreePhase, Is.Null); + Assert.That(option.IntroPhase, Is.Null); + Assert.That(option.PresentedOfferingContext, Is.Null); + Assert.That(option.OptionInstallmentsInfo, Is.Null); + } + + [Test] + public void SubscriptionOptionPricingPhaseFallsBackToUnknownForUnrecognizedEnums() + { + var response = JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false," + + "\"fullPricePhase\":{\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"}," + + "\"recurrenceMode\":\"BOGUS\",\"billingCycleCount\":0," + + "\"price\":{\"formatted\":\"$9.99\",\"amountMicros\":9990000,\"currencyCode\":\"USD\"}," + + "\"offerPaymentMode\":\"BOGUS\"}}" + ); + + var option = new Purchases.SubscriptionOption(response); + + Assert.That(option.FullPricePhase.RecurrenceMode, + Is.EqualTo(Purchases.SubscriptionOption.RecurrenceMode.UNKNOWN)); + Assert.That(option.FullPricePhase.OfferPaymentMode, + Is.EqualTo(Purchases.SubscriptionOption.OfferPaymentMode.UNKNOWN)); + } + + [Test] + public void WebPurchaseRedemptionWrapsRedemptionLink() + { + var redemption = new Purchases.WebPurchaseRedemption("https://rev.cat/redeem/abc"); + + Assert.That(redemption.RedemptionLink, Is.EqualTo("https://rev.cat/redeem/abc")); + } + + [Test] + public void WebPurchaseRedemptionResultParsesSuccessVariant() + { + var response = JSONNode.Parse( + "{\"result\":\"SUCCESS\",\"customerInfo\":" + MinimalCustomerInfoJson + "}" + ); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.Success)result).CustomerInfo, Is.Not.Null); + } + + [Test] + public void WebPurchaseRedemptionResultParsesErrorVariant() + { + var response = JSONNode.Parse( + "{\"result\":\"ERROR\",\"error\":{\"message\":\"Redemption failed\",\"code\":11," + + "\"underlyingErrorMessage\":\"Backend rejected\",\"readableErrorCode\":\"UNKNOWN_ERROR\"}}" + ); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.RedemptionError)result).Error.Code, Is.EqualTo(11)); + } + + [Test] + public void WebPurchaseRedemptionResultParsesInvalidTokenVariant() + { + var response = JSONNode.Parse("{\"result\":\"INVALID_TOKEN\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.SameAs(Purchases.WebPurchaseRedemptionResult.InvalidToken.Instance)); + } + + [Test] + public void WebPurchaseRedemptionResultParsesExpiredVariant() + { + var response = JSONNode.Parse("{\"result\":\"EXPIRED\",\"obfuscatedEmail\":\"a***@b.com\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((Purchases.WebPurchaseRedemptionResult.Expired)result).ObfuscatedEmail, + Is.EqualTo("a***@b.com")); + } + + [Test] + public void WebPurchaseRedemptionResultParsesPurchaseBelongsToOtherUserVariant() + { + var response = JSONNode.Parse("{\"result\":\"PURCHASE_BELONGS_TO_OTHER_USER\"}"); + + var result = Purchases.WebPurchaseRedemptionResult.FromJson(response); + + Assert.That(result, Is.SameAs(Purchases.WebPurchaseRedemptionResult.PurchaseBelongsToOtherUser.Instance)); + } + + [Test] + public void WebPurchaseRedemptionResultThrowsForUnrecognizedResultType() + { + var response = JSONNode.Parse("{\"result\":\"SOMETHING_ELSE\"}"); + + Assert.Throws(() => Purchases.WebPurchaseRedemptionResult.FromJson(response)); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs.meta new file mode 100644 index 00000000..5f35c933 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/JsonModelTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 90c6103250844a23b9bc88e85907bf37 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs b/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs new file mode 100644 index 00000000..3354ea50 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs @@ -0,0 +1,36 @@ +using NUnit.Framework; +using RevenueCat.SimpleJSON; + +namespace RevenueCat.Tests +{ + public class PresentedOfferingContextTests + { + [Test] + public void JsonRoundTripPreservesAllContext() + { + var originalJson = JSONNode.Parse( + "{\"offeringIdentifier\":\"default\",\"placementIdentifier\":\"onboarding\"," + + "\"targetingContext\":{\"revision\":7,\"ruleId\":\"rule-id\"}}" + ); + + var context = new Purchases.PresentedOfferingContext(originalJson); + var serializedContext = JSONNode.Parse(context.ToJsonString()); + + Assert.That(serializedContext["offeringIdentifier"].Value, Is.EqualTo("default")); + Assert.That(serializedContext["placementIdentifier"].Value, Is.EqualTo("onboarding")); + Assert.That(serializedContext["targetingContext"]["revision"].AsInt, Is.EqualTo(7)); + Assert.That(serializedContext["targetingContext"]["ruleId"].Value, Is.EqualTo("rule-id")); + } + + [Test] + public void OfferingIdentifierConstructorOmitsOptionalContext() + { + var context = new Purchases.PresentedOfferingContext("default"); + var serializedContext = JSONNode.Parse(context.ToJsonString()); + + Assert.That(serializedContext["offeringIdentifier"].Value, Is.EqualTo("default")); + Assert.That(serializedContext.HasKey("placementIdentifier"), Is.False); + Assert.That(serializedContext.HasKey("targetingContext"), Is.False); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs.meta new file mode 100644 index 00000000..bdcb2171 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PresentedOfferingContextTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6a3791f9b503416eafacfbd847ecec71 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs b/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs new file mode 100644 index 00000000..4a489aa1 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs @@ -0,0 +1,289 @@ +using System.Reflection; +using NUnit.Framework; +using RevenueCat.SimpleJSON; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class PurchaseCallTests + { + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void PurchaseProductForwardsArgumentsAndDeliversResponse() + { + Purchases.PurchaseResult receivedResult = null; + + _purchases.PurchaseProduct( + "monthly", + result => receivedResult = result, + "subs", + "old_monthly", + Purchases.ProrationMode.ImmediateAndChargeFullPrice, + true + ); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchaseProduct), 7); + Assert.That(invocation.Arguments[0], Is.EqualTo("monthly")); + Assert.That(invocation.Arguments[1], Is.EqualTo("subs")); + Assert.That(invocation.Arguments[2], Is.EqualTo("old_monthly")); + Assert.That(invocation.Arguments[3], Is.EqualTo(Purchases.ProrationMode.ImmediateAndChargeFullPrice)); + Assert.That(invocation.Arguments[4], Is.True); + Assert.That(invocation.Arguments[5], Is.Null); + Assert.That(invocation.Arguments[6], Is.Null); + + SendNativeResponse("_makePurchase", + "{\"transaction\":{\"transactionIdentifier\":\"transaction_id\"," + + "\"productIdentifier\":\"monthly\",\"purchaseDateMillis\":1700000000000}," + + "\"userCancelled\":false}"); + + Assert.That(receivedResult, Is.Not.Null); + Assert.That(receivedResult.ProductIdentifier, Is.EqualTo("monthly")); + Assert.That(receivedResult.StoreTransaction.TransactionIdentifier, Is.EqualTo("transaction_id")); + Assert.That(receivedResult.UserCancelled, Is.False); + Assert.That(receivedResult.Error, Is.Null); + } + + [Test] + public void PurchasePackageForwardsArguments() + { + var package = CreatePackage(); + + _purchases.PurchasePackage( + package, + _ => { }, + "old_monthly", + Purchases.ProrationMode.ImmediateWithTimeProration, + true + ); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchasePackage), 5); + Assert.That(invocation.Arguments[0], Is.SameAs(package)); + Assert.That(invocation.Arguments[1], Is.EqualTo("old_monthly")); + Assert.That(invocation.Arguments[2], Is.EqualTo(Purchases.ProrationMode.ImmediateWithTimeProration)); + Assert.That(invocation.Arguments[3], Is.True); + Assert.That(invocation.Arguments[4], Is.Null); + } + + [Test] + public void PurchaseDiscountedProductForwardsDiscountAndDeliversResponse() + { + var discount = CreatePromotionalOffer(); + Purchases.PurchaseResult receivedResult = null; + + _purchases.PurchaseDiscountedProduct("monthly", discount, result => receivedResult = result); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchaseProduct), 7); + Assert.That(invocation.Arguments[0], Is.EqualTo("monthly")); + Assert.That(invocation.Arguments[1], Is.EqualTo("subs")); + Assert.That(invocation.Arguments[2], Is.Null); + Assert.That(invocation.Arguments[3], + Is.EqualTo(Purchases.ProrationMode.UnknownSubscriptionUpgradeDowngradePolicy)); + Assert.That(invocation.Arguments[4], Is.False); + Assert.That(invocation.Arguments[5], Is.Null); + Assert.That(invocation.Arguments[6], Is.SameAs(discount)); + + SendNativeResponse("_makePurchase", + "{\"transaction\":{\"transactionIdentifier\":\"transaction_id\"," + + "\"productIdentifier\":\"monthly\",\"purchaseDateMillis\":1700000000000}," + + "\"userCancelled\":false}"); + + Assert.That(receivedResult, Is.Not.Null); + Assert.That(receivedResult.ProductIdentifier, Is.EqualTo("monthly")); + } + + [Test] + public void PurchaseDiscountedPackageForwardsDiscountAndDeliversResponse() + { + var package = CreatePackage(); + var discount = CreatePromotionalOffer(); + Purchases.PurchaseResult receivedResult = null; + + _purchases.PurchaseDiscountedPackage(package, discount, result => receivedResult = result); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchasePackage), 5); + Assert.That(invocation.Arguments[0], Is.SameAs(package)); + Assert.That(invocation.Arguments[1], Is.Null); + Assert.That(invocation.Arguments[2], + Is.EqualTo(Purchases.ProrationMode.UnknownSubscriptionUpgradeDowngradePolicy)); + Assert.That(invocation.Arguments[3], Is.False); + Assert.That(invocation.Arguments[4], Is.SameAs(discount)); + + SendNativeResponse("_makePurchase", + "{\"transaction\":{\"transactionIdentifier\":\"transaction_id\"," + + "\"productIdentifier\":\"$rc_monthly\",\"purchaseDateMillis\":1700000000000}," + + "\"userCancelled\":false}"); + + Assert.That(receivedResult, Is.Not.Null); + } + + [Test] + public void PurchaseSubscriptionOptionForwardsArguments() + { + var subscriptionOption = CreateSubscriptionOption(); + var googleProductChangeInfo = + new Purchases.GoogleProductChangeInfo("old_monthly", Purchases.ProrationMode.ImmediateWithTimeProration); + + _purchases.PurchaseSubscriptionOption(subscriptionOption, _ => { }, googleProductChangeInfo, true); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchaseSubscriptionOption), 3); + Assert.That(invocation.Arguments[0], Is.SameAs(subscriptionOption)); + Assert.That(invocation.Arguments[1], Is.SameAs(googleProductChangeInfo)); + Assert.That(invocation.Arguments[2], Is.True); + } + + [Test] + public void PurchaseProductWithWinBackOfferForwardsArgumentsAndDeliversResponse() + { + var storeProduct = CreateStoreProduct(); + var winBackOffer = CreateWinBackOffer(); + Purchases.PurchaseResult receivedResult = null; + + _purchases.PurchaseProductWithWinBackOffer(storeProduct, winBackOffer, result => receivedResult = result); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchaseProductWithWinBackOffer), 2); + Assert.That(invocation.Arguments[0], Is.SameAs(storeProduct)); + Assert.That(invocation.Arguments[1], Is.SameAs(winBackOffer)); + + SendNativeResponse("_purchaseProductWithWinBackOffer", + "{\"transaction\":{\"transactionIdentifier\":\"transaction_id\"," + + "\"productIdentifier\":\"monthly\",\"purchaseDateMillis\":1700000000000}," + + "\"userCancelled\":false}"); + + Assert.That(receivedResult, Is.Not.Null); + Assert.That(receivedResult.ProductIdentifier, Is.EqualTo("monthly")); + } + + [Test] + public void PurchasePackageWithWinBackOfferForwardsArgumentsAndDeliversResponse() + { + var package = CreatePackage(); + var winBackOffer = CreateWinBackOffer(); + Purchases.PurchaseResult receivedResult = null; + + _purchases.PurchasePackageWithWinBackOffer(package, winBackOffer, result => receivedResult = result); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.PurchasePackageWithWinBackOffer), 2); + Assert.That(invocation.Arguments[0], Is.SameAs(package)); + Assert.That(invocation.Arguments[1], Is.SameAs(winBackOffer)); + + SendNativeResponse("_purchasePackageWithWinBackOffer", + "{\"transaction\":{\"transactionIdentifier\":\"transaction_id\"," + + "\"productIdentifier\":\"$rc_monthly\",\"purchaseDateMillis\":1700000000000}," + + "\"userCancelled\":false}"); + + Assert.That(receivedResult, Is.Not.Null); + } + + [Test] + public void PurchaseResultParsesUserCancellationWithoutTransactionOrError() + { + Purchases.PurchaseResult receivedResult = null; + _purchases.PurchaseProduct("monthly", result => receivedResult = result); + + SendNativeResponse("_makePurchase", "{\"userCancelled\":true}"); + + Assert.That(receivedResult, Is.Not.Null); + Assert.That(receivedResult.UserCancelled, Is.True); + Assert.That(receivedResult.CustomerInfo, Is.Null); + Assert.That(receivedResult.StoreTransaction, Is.Null); + Assert.That(receivedResult.ProductIdentifier, Is.Null); + Assert.That(receivedResult.Error, Is.Null); + } + + [Test] + public void PurchaseResultParsesErrorOnlyResponse() + { + Purchases.PurchaseResult receivedResult = null; + _purchases.PurchaseProduct("monthly", result => receivedResult = result); + + SendNativeResponse("_makePurchase", + "{\"error\":{\"message\":\"Purchase failed\",\"code\":7," + + "\"underlyingErrorMessage\":\"Store declined\",\"readableErrorCode\":\"STORE_PROBLEM_ERROR\"}}"); + + Assert.That(receivedResult, Is.Not.Null); + Assert.That(receivedResult.Error, Is.Not.Null); + Assert.That(receivedResult.Error.Code, Is.EqualTo(7)); + Assert.That(receivedResult.UserCancelled, Is.False); + Assert.That(receivedResult.CustomerInfo, Is.Null); + Assert.That(receivedResult.StoreTransaction, Is.Null); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + + private void SendNativeResponse(string method, string response) + { + var receiver = typeof(Purchases).GetMethod(method, BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(receiver, Is.Not.Null, $"Native response receiver {method} does not exist"); + receiver.Invoke(_purchases, new object[] { response }); + } + + private static Purchases.Package CreatePackage() + { + return new Purchases.Package(JSONNode.Parse( + "{\"identifier\":\"$rc_monthly\",\"packageType\":\"MONTHLY\"," + + "\"product\":{\"title\":\"Monthly\",\"identifier\":\"monthly\"," + + "\"description\":\"Monthly access\",\"price\":9.99,\"priceString\":\"$9.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"SUBSCRIPTION\"}," + + "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}}" + )); + } + + private static Purchases.StoreProduct CreateStoreProduct() + { + return new Purchases.StoreProduct(JSONNode.Parse( + "{\"title\":\"Monthly\",\"identifier\":\"monthly\"," + + "\"description\":\"Monthly access\",\"price\":9.99,\"priceString\":\"$9.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"SUBSCRIPTION\"}" + )); + } + + private static Purchases.SubscriptionOption CreateSubscriptionOption() + { + return new Purchases.SubscriptionOption(JSONNode.Parse( + "{\"id\":\"monthly:base\",\"storeProductId\":\"monthly\",\"productId\":\"monthly\"," + + "\"tags\":[],\"isBasePlan\":true," + + "\"billingPeriod\":{\"unit\":\"MONTH\",\"value\":1,\"iso8601\":\"P1M\"},\"isPrepaid\":false}" + )); + } + + private static Purchases.PromotionalOffer CreatePromotionalOffer() + { + return new Purchases.PromotionalOffer(JSONNode.Parse( + "{\"identifier\":\"promo\",\"keyIdentifier\":\"key\",\"nonce\":\"nonce\"," + + "\"signature\":\"sig\",\"timestamp\":1700000000000}" + )); + } + + private static Purchases.WinBackOffer CreateWinBackOffer() + { + return new Purchases.WinBackOffer(JSONNode.Parse( + "{\"identifier\":\"winback\",\"price\":4.99,\"priceString\":\"$4.99\",\"cycles\":1," + + "\"period\":\"P1M\",\"periodUnit\":\"MONTH\",\"periodNumberOfUnits\":1}" + )); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs.meta new file mode 100644 index 00000000..254fb6d7 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchaseCallTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ada6988a429349738c5ac2c7a3d73d9c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs b/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs new file mode 100644 index 00000000..ef9f59ad --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs @@ -0,0 +1,74 @@ +using NUnit.Framework; +using RevenueCat.SimpleJSON; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class PurchasesCallTests + { + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void ConfigureForwardsEveryConfigurationValue() + { + var configuration = Purchases.PurchasesConfiguration.Builder + .Init("test_api_key") + .SetAppUserId("app_user_id") + .SetPurchasesAreCompletedBy(Purchases.PurchasesAreCompletedBy.MyApp, + Purchases.StoreKitVersion.StoreKit2) + .SetUserDefaultsSuiteName("suite_name") + .SetUseAmazon(true) + .SetDangerousSettings(new Purchases.DangerousSettings(false)) + .SetShouldShowInAppMessagesAutomatically(true) + .SetEntitlementVerificationMode(Purchases.EntitlementVerificationMode.Informational) + .SetPendingTransactionsForPrepaidPlansEnabled(true) + .SetDiagnosticsEnabled(true) + .SetAutomaticDeviceIdentifierCollectionEnabled(false) + .SetPreferredUILocaleOverride("de_DE") + .Build(); + + _purchases.Configure(configuration); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.Setup), 14); + Assert.That(invocation.Arguments[0], Is.EqualTo(_gameObject.name)); + Assert.That(invocation.Arguments[1], Is.EqualTo("test_api_key")); + Assert.That(invocation.Arguments[2], Is.EqualTo("app_user_id")); + Assert.That(invocation.Arguments[3], Is.EqualTo(Purchases.PurchasesAreCompletedBy.MyApp)); + Assert.That(invocation.Arguments[4], Is.EqualTo(Purchases.StoreKitVersion.StoreKit2)); + Assert.That(invocation.Arguments[5], Is.EqualTo("suite_name")); + Assert.That(invocation.Arguments[6], Is.True); + Assert.That(JSONNode.Parse((string)invocation.Arguments[7])["AutoSyncPurchases"].AsBool, Is.False); + Assert.That(invocation.Arguments[8], Is.True); + Assert.That(invocation.Arguments[9], Is.EqualTo(Purchases.EntitlementVerificationMode.Informational)); + Assert.That(invocation.Arguments[10], Is.True); + Assert.That(invocation.Arguments[11], Is.True); + Assert.That(invocation.Arguments[12], Is.False); + Assert.That(invocation.Arguments[13], Is.EqualTo("de_DE")); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs.meta new file mode 100644 index 00000000..24237f03 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesCallTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 30b46e0ec5314d2b9e881bb88b801d64 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs b/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs new file mode 100644 index 00000000..33a3648c --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs @@ -0,0 +1,75 @@ +using NUnit.Framework; + +namespace RevenueCat.Tests +{ + public class PurchasesConfigurationTests + { + [Test] + public void BuildUsesDocumentedDefaults() + { + var configuration = Purchases.PurchasesConfiguration.Builder + .Init("test_api_key") + .Build(); + + Assert.That(configuration.ApiKey, Is.EqualTo("test_api_key")); + Assert.That(configuration.AppUserId, Is.Null); + Assert.That(configuration.PurchasesAreCompletedBy, Is.EqualTo(default(Purchases.PurchasesAreCompletedBy))); + Assert.That(configuration.UserDefaultsSuiteName, Is.Null); + Assert.That(configuration.UseAmazon, Is.False); + Assert.That(configuration.DangerousSettings.AutoSyncPurchases, Is.True); + Assert.That(configuration.StoreKitVersion, Is.EqualTo(default(Purchases.StoreKitVersion))); + Assert.That(configuration.ShouldShowInAppMessagesAutomatically, Is.False); + Assert.That(configuration.EntitlementVerificationMode, Is.EqualTo(default(Purchases.EntitlementVerificationMode))); + Assert.That(configuration.PendingTransactionsForPrepaidPlansEnabled, Is.False); + Assert.That(configuration.DiagnosticsEnabled, Is.False); + Assert.That(configuration.AutomaticDeviceIdentifierCollectionEnabled, Is.True); + Assert.That(configuration.PreferredUILocaleOverride, Is.Null); + } + + [Test] + public void BuildUsesConfiguredValues() + { + var dangerousSettings = new Purchases.DangerousSettings(false); + + var configuration = Purchases.PurchasesConfiguration.Builder + .Init("test_api_key") + .SetAppUserId("app_user_id") + .SetPurchasesAreCompletedBy(Purchases.PurchasesAreCompletedBy.MyApp, Purchases.StoreKitVersion.StoreKit2) + .SetUserDefaultsSuiteName("suite_name") + .SetUseAmazon(true) + .SetDangerousSettings(dangerousSettings) + .SetShouldShowInAppMessagesAutomatically(true) + .SetEntitlementVerificationMode(Purchases.EntitlementVerificationMode.Informational) + .SetPendingTransactionsForPrepaidPlansEnabled(true) + .SetDiagnosticsEnabled(true) + .SetAutomaticDeviceIdentifierCollectionEnabled(false) + .SetPreferredUILocaleOverride("de_DE") + .Build(); + + Assert.That(configuration.AppUserId, Is.EqualTo("app_user_id")); + Assert.That(configuration.PurchasesAreCompletedBy, Is.EqualTo(Purchases.PurchasesAreCompletedBy.MyApp)); + Assert.That(configuration.StoreKitVersion, Is.EqualTo(Purchases.StoreKitVersion.StoreKit2)); + Assert.That(configuration.UserDefaultsSuiteName, Is.EqualTo("suite_name")); + Assert.That(configuration.UseAmazon, Is.True); + Assert.That(configuration.DangerousSettings, Is.SameAs(dangerousSettings)); + Assert.That(configuration.ShouldShowInAppMessagesAutomatically, Is.True); + Assert.That(configuration.EntitlementVerificationMode, Is.EqualTo(Purchases.EntitlementVerificationMode.Informational)); + Assert.That(configuration.PendingTransactionsForPrepaidPlansEnabled, Is.True); + Assert.That(configuration.DiagnosticsEnabled, Is.True); + Assert.That(configuration.AutomaticDeviceIdentifierCollectionEnabled, Is.False); + Assert.That(configuration.PreferredUILocaleOverride, Is.EqualTo("de_DE")); + } + + [Test] + public void BuildRestoresDefaultDangerousSettingsWhenSetToNull() + { + var configuration = Purchases.PurchasesConfiguration.Builder + .Init("test_api_key") + .SetDangerousSettings(null) + .Build(); + + Assert.That(configuration.DangerousSettings, Is.Not.Null); + Assert.That(configuration.DangerousSettings.AutoSyncPurchases, Is.True); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs.meta new file mode 100644 index 00000000..a6a7ac6c --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesConfigurationTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6566b7a4beb347cda9e2e33f4924819f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs b/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs new file mode 100644 index 00000000..6933748e --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs @@ -0,0 +1,209 @@ +using System.Collections.Generic; +using RevenueCat; + +namespace RevenueCat.Tests +{ + internal sealed class PurchasesWrapperSpy : IPurchasesWrapper + { + internal sealed class Invocation + { + internal readonly string Method; + internal readonly object[] Arguments; + + internal Invocation(string method, object[] arguments) + { + Method = method; + Arguments = arguments; + } + } + + internal readonly List Invocations = new List(); + internal Invocation LastInvocation => Invocations[Invocations.Count - 1]; + + private void Record(string method, params object[] arguments) + { + Invocations.Add(new Invocation(method, arguments)); + } + + public void Setup(string gameObject, string apiKey, string appUserId, + Purchases.PurchasesAreCompletedBy purchasesAreCompletedBy, Purchases.StoreKitVersion storeKitVersion, + string userDefaultsSuiteName, bool useAmazon, string dangerousSettingsJson, + bool shouldShowInAppMessagesAutomatically, bool pendingTransactionsForPrepaidPlansEnabled, + bool diagnosticsEnabled, bool automaticDeviceIdentifierCollectionEnabled, string preferredUILocaleOverride) + { + Record(nameof(Setup), gameObject, apiKey, appUserId, purchasesAreCompletedBy, storeKitVersion, + userDefaultsSuiteName, useAmazon, dangerousSettingsJson, shouldShowInAppMessagesAutomatically, + pendingTransactionsForPrepaidPlansEnabled, diagnosticsEnabled, + automaticDeviceIdentifierCollectionEnabled, preferredUILocaleOverride); + } + + public void Setup(string gameObject, string apiKey, string appUserId, + Purchases.PurchasesAreCompletedBy purchasesAreCompletedBy, Purchases.StoreKitVersion storeKitVersion, + string userDefaultsSuiteName, bool useAmazon, string dangerousSettingsJson, + bool shouldShowInAppMessagesAutomatically, + Purchases.EntitlementVerificationMode entitlementVerificationMode, + bool pendingTransactionsForPrepaidPlansEnabled, bool diagnosticsEnabled, + bool automaticDeviceIdentifierCollectionEnabled, string preferredUILocaleOverride) + { + Record(nameof(Setup), gameObject, apiKey, appUserId, purchasesAreCompletedBy, storeKitVersion, + userDefaultsSuiteName, useAmazon, dangerousSettingsJson, shouldShowInAppMessagesAutomatically, + entitlementVerificationMode, pendingTransactionsForPrepaidPlansEnabled, diagnosticsEnabled, + automaticDeviceIdentifierCollectionEnabled, preferredUILocaleOverride); + } + + public void GetStorefront() => Record(nameof(GetStorefront)); + public void GetProducts(string[] productIdentifiers, string type = "subs") => + Record(nameof(GetProducts), productIdentifiers, type); + + public void PurchaseProduct(string productIdentifier, string type = "subs", string oldSku = null, + Purchases.ProrationMode prorationMode = + Purchases.ProrationMode.UnknownSubscriptionUpgradeDowngradePolicy, + bool googleIsPersonalizedPrice = false, string presentedOfferingIdentifier = null, + Purchases.PromotionalOffer discount = null) => + Record(nameof(PurchaseProduct), productIdentifier, type, oldSku, prorationMode, + googleIsPersonalizedPrice, presentedOfferingIdentifier, discount); + + public void PurchasePackage(Purchases.Package packageToPurchase, string oldSku = null, + Purchases.ProrationMode prorationMode = + Purchases.ProrationMode.UnknownSubscriptionUpgradeDowngradePolicy, + bool googleIsPersonalizedPrice = false, Purchases.PromotionalOffer discount = null) => + Record(nameof(PurchasePackage), packageToPurchase, oldSku, prorationMode, + googleIsPersonalizedPrice, discount); + + public void PurchaseSubscriptionOption(Purchases.SubscriptionOption subscriptionOption, + Purchases.GoogleProductChangeInfo googleProductChangeInfo = null, + bool googleIsPersonalizedPrice = false) => + Record(nameof(PurchaseSubscriptionOption), subscriptionOption, googleProductChangeInfo, + googleIsPersonalizedPrice); + + public void RestorePurchases() => Record(nameof(RestorePurchases)); + public void LogIn(string appUserId) => Record(nameof(LogIn), appUserId); + public void LogOut() => Record(nameof(LogOut)); + public void SetAllowSharingStoreAccount(bool allow) => Record(nameof(SetAllowSharingStoreAccount), allow); + public void SetDebugLogsEnabled(bool enabled) => Record(nameof(SetDebugLogsEnabled), enabled); + public void SetLogLevel(Purchases.LogLevel level) => Record(nameof(SetLogLevel), level); + public void SetLogHandler() => Record(nameof(SetLogHandler)); + public void SetProxyURL(string proxyURL) => Record(nameof(SetProxyURL), proxyURL); + public string GetAppUserId() + { + Record(nameof(GetAppUserId)); + return null; + } + + public void GetCustomerInfo() => Record(nameof(GetCustomerInfo)); + public void GetOfferings() => Record(nameof(GetOfferings)); + public void GetCurrentOfferingForPlacement(string placementIdentifier) => + Record(nameof(GetCurrentOfferingForPlacement), placementIdentifier); + + public void SyncAttributesAndOfferingsIfNeeded() => Record(nameof(SyncAttributesAndOfferingsIfNeeded)); + public void SyncPurchases() => Record(nameof(SyncPurchases)); + + public void SyncAmazonPurchase(string productID, string receiptID, string amazonUserID, + string isoCurrencyCode, double price) => + Record(nameof(SyncAmazonPurchase), productID, receiptID, amazonUserID, isoCurrencyCode, price); + + public void GetAmazonLWAConsentStatus() => Record(nameof(GetAmazonLWAConsentStatus)); + public void EnableAdServicesAttributionTokenCollection() => + Record(nameof(EnableAdServicesAttributionTokenCollection)); + + public bool IsAnonymous() + { + Record(nameof(IsAnonymous)); + return false; + } + + public bool IsConfigured() + { + Record(nameof(IsConfigured)); + return false; + } + + public void CheckTrialOrIntroductoryPriceEligibility(string[] productIdentifiers) => + Record(nameof(CheckTrialOrIntroductoryPriceEligibility), productIdentifiers); + + public void InvalidateCustomerInfoCache() => Record(nameof(InvalidateCustomerInfoCache)); + public void OverridePreferredUILocale(string locale) => Record(nameof(OverridePreferredUILocale), locale); + public void PresentCodeRedemptionSheet() => Record(nameof(PresentCodeRedemptionSheet)); + public void RecordPurchase(string productID) => Record(nameof(RecordPurchase), productID); + public void SetSimulatesAskToBuyInSandbox(bool enabled) => + Record(nameof(SetSimulatesAskToBuyInSandbox), enabled); + + public void SetAttributes(string attributesJson) => Record(nameof(SetAttributes), attributesJson); + public void SetEmail(string email) => Record(nameof(SetEmail), email); + public void SetPhoneNumber(string phoneNumber) => Record(nameof(SetPhoneNumber), phoneNumber); + public void SetDisplayName(string displayName) => Record(nameof(SetDisplayName), displayName); + public void SetPushToken(string token) => Record(nameof(SetPushToken), token); + public void SetAdjustID(string adjustID) => Record(nameof(SetAdjustID), adjustID); + public void SetAppsflyerID(string appsflyerID) => Record(nameof(SetAppsflyerID), appsflyerID); + public void SetFBAnonymousID(string fbAnonymousID) => Record(nameof(SetFBAnonymousID), fbAnonymousID); + public void SetMparticleID(string mparticleID) => Record(nameof(SetMparticleID), mparticleID); + public void SetOnesignalID(string onesignalID) => Record(nameof(SetOnesignalID), onesignalID); + public void SetOnesignalUserID(string onesignalUserID) => Record(nameof(SetOnesignalUserID), onesignalUserID); + public void SetAirshipChannelID(string airshipChannelID) => + Record(nameof(SetAirshipChannelID), airshipChannelID); + + public void SetCleverTapID(string cleverTapID) => Record(nameof(SetCleverTapID), cleverTapID); + public void SetMixpanelDistinctID(string mixpanelDistinctID) => + Record(nameof(SetMixpanelDistinctID), mixpanelDistinctID); + + public void SetFirebaseAppInstanceID(string firebaseAppInstanceID) => + Record(nameof(SetFirebaseAppInstanceID), firebaseAppInstanceID); + + public void SetMediaSource(string mediaSource) => Record(nameof(SetMediaSource), mediaSource); + public void SetCampaign(string campaign) => Record(nameof(SetCampaign), campaign); + public void SetAdGroup(string adGroup) => Record(nameof(SetAdGroup), adGroup); + public void SetAd(string ad) => Record(nameof(SetAd), ad); + public void SetKeyword(string keyword) => Record(nameof(SetKeyword), keyword); + public void SetCreative(string creative) => Record(nameof(SetCreative), creative); + public void SetAppsFlyerConversionData(string conversionDataJson) => + Record(nameof(SetAppsFlyerConversionData), conversionDataJson); + + public void CollectDeviceIdentifiers() => Record(nameof(CollectDeviceIdentifiers)); + public void CanMakePayments(Purchases.BillingFeature[] features) => + Record(nameof(CanMakePayments), features); + + public void GetPromotionalOffer(string productIdentifier, string discountIdentifier) => + Record(nameof(GetPromotionalOffer), productIdentifier, discountIdentifier); + + public void ShowInAppMessages(Purchases.InAppMessageType[] messageTypes) => + Record(nameof(ShowInAppMessages), messageTypes); + + public void ParseAsWebPurchaseRedemption(string urlString) => + Record(nameof(ParseAsWebPurchaseRedemption), urlString); + + public void RedeemWebPurchase(Purchases.WebPurchaseRedemption webPurchaseRedemption) => + Record(nameof(RedeemWebPurchase), webPurchaseRedemption); + + public void GetVirtualCurrencies() => Record(nameof(GetVirtualCurrencies)); + + public string GetCachedVirtualCurrencies() + { + Record(nameof(GetCachedVirtualCurrencies)); + return null; + } + + public void InvalidateVirtualCurrenciesCache() => Record(nameof(InvalidateVirtualCurrenciesCache)); + public void GetEligibleWinBackOffersForProduct(Purchases.StoreProduct storeProduct) => + Record(nameof(GetEligibleWinBackOffersForProduct), storeProduct); + + public void GetEligibleWinBackOffersForPackage(Purchases.Package package) => + Record(nameof(GetEligibleWinBackOffersForPackage), package); + + public void PurchaseProductWithWinBackOffer(Purchases.StoreProduct storeProduct, + Purchases.WinBackOffer winBackOffer) => + Record(nameof(PurchaseProductWithWinBackOffer), storeProduct, winBackOffer); + + public void PurchasePackageWithWinBackOffer(Purchases.Package package, + Purchases.WinBackOffer winBackOffer) => + Record(nameof(PurchasePackageWithWinBackOffer), package, winBackOffer); + + public void TrackCustomPaywallImpression(Purchases.CustomPaywallImpressionParams parameters) => + Record(nameof(TrackCustomPaywallImpression), parameters); + + public void TrackAdDisplayed(AdDisplayedData data) => Record(nameof(TrackAdDisplayed), data); + public void TrackAdOpened(AdOpenedData data) => Record(nameof(TrackAdOpened), data); + public void TrackAdRevenue(AdRevenueData data) => Record(nameof(TrackAdRevenue), data); + public void TrackAdLoaded(AdLoadedData data) => Record(nameof(TrackAdLoaded), data); + public void TrackAdFailedToLoad(AdFailedToLoadData data) => Record(nameof(TrackAdFailedToLoad), data); + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs.meta b/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs.meta new file mode 100644 index 00000000..935aa898 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/PurchasesWrapperSpy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 536c0eb613ec41f68eba862bd85dc4a8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef b/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef new file mode 100644 index 00000000..9cab16e1 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef @@ -0,0 +1,21 @@ +{ + "name": "RevenueCat.Tests.EditMode", + "rootNamespace": "RevenueCat.Tests", + "references": [ + "revenuecat.purchases-unity" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": false, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false, + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef.meta b/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef.meta new file mode 100644 index 00000000..cc6a30f9 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/RevenueCat.Tests.EditMode.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 22b4de0286b948bca3f72ee4897af9b8 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs new file mode 100644 index 00000000..1ba8fa2a --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs @@ -0,0 +1,261 @@ +using NUnit.Framework; +using RevenueCat; +using RevenueCat.SimpleJSON; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class TrackingTests + { + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void TrackCustomPaywallImpressionWithoutParametersCreatesEmptyParameters() + { + _purchases.TrackCustomPaywallImpression(); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.Null); + Assert.That(parameters.OfferingId, Is.Null); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithPaywallIdOnlySetsPaywallIdOnly() + { + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams("paywall_1")); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.Null); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithOfferingResolvesOfferingIdAndContext() + { + var offering = CreateOfferingWithPackage(); + + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams(offering)); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.Null); + Assert.That(parameters.Offering, Is.SameAs(offering)); + Assert.That(parameters.OfferingId, Is.EqualTo(offering.Identifier)); + } + + [Test] + public void CustomPaywallImpressionParamsResolvesPresentedOfferingContextFromFirstPackage() + { + var offering = CreateOfferingWithPackage(); + + var parameters = new Purchases.CustomPaywallImpressionParams(offering); + + Assert.That(parameters.PresentedOfferingContext, Is.Not.Null); + Assert.That(parameters.PresentedOfferingContext.OfferingIdentifier, Is.EqualTo("default")); + } + + [Test] + public void CustomPaywallImpressionParamsHasNullPresentedOfferingContextWhenOfferingHasNoPackages() + { + var offering = new Purchases.Offering(JSONNode.Parse( + "{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[]}")); + + var parameters = new Purchases.CustomPaywallImpressionParams(offering); + + Assert.That(parameters.PresentedOfferingContext, Is.Null); + } + + [Test] + public void TrackCustomPaywallImpressionWithPaywallIdAndOfferingSetsBoth() + { + var offering = CreateOfferingWithPackage(); + + _purchases.TrackCustomPaywallImpression(new Purchases.CustomPaywallImpressionParams("paywall_1", offering)); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.EqualTo(offering.Identifier)); + } + + [Test] + [System.Obsolete] + public void TrackCustomPaywallImpressionDeprecatedCtorUsesExplicitOfferingId() + { + _purchases.TrackCustomPaywallImpression( + new Purchases.CustomPaywallImpressionParams("paywall_1", "offering_1")); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackCustomPaywallImpression), 1); + var parameters = (Purchases.CustomPaywallImpressionParams)invocation.Arguments[0]; + Assert.That(parameters.PaywallId, Is.EqualTo("paywall_1")); + Assert.That(parameters.OfferingId, Is.EqualTo("offering_1")); + Assert.That(parameters.Offering, Is.Null); + } + + [Test] + public void TrackAdDisplayedForwardsDataToWrapper() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdDisplayed(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdDisplayed), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdOpenedForwardsDataToWrapper() + { + var data = new AdOpenedData(AdTracker.MediatorName.AppLovin, AdTracker.Format.Interstitial, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdOpened(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdOpened), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdRevenueForwardsDataToWrapper() + { + var data = new AdRevenueData(AdTracker.MediatorName.AdMob, AdTracker.Format.Rewarded, "unit_1", + "impression_1", 1500000L, "USD", AdTracker.Precision.Exact); + + _purchases.AdTracker.TrackAdRevenue(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdRevenue), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdLoadedForwardsDataToWrapper() + { + var data = new AdLoadedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Native, "unit_1", "impression_1"); + + _purchases.AdTracker.TrackAdLoaded(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdLoaded), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void TrackAdFailedToLoadForwardsDataToWrapper() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1"); + + _purchases.AdTracker.TrackAdFailedToLoad(data); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.TrackAdFailedToLoad), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(data)); + } + + [Test] + public void AdDisplayedDataToJsonStringOmitsOptionalFieldsWhenNull() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", "impression_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["mediatorName"].Value, Is.EqualTo("AdMob")); + Assert.That(json["adFormat"].Value, Is.EqualTo("banner")); + Assert.That(json["adUnitId"].Value, Is.EqualTo("unit_1")); + Assert.That(json["impressionId"].Value, Is.EqualTo("impression_1")); + Assert.That(json.HasKey("networkName"), Is.False); + Assert.That(json.HasKey("placement"), Is.False); + } + + [Test] + public void AdDisplayedDataToJsonStringIncludesOptionalFieldsWhenPresent() + { + var data = new AdDisplayedData(AdTracker.MediatorName.AppLovin, AdTracker.Format.Interstitial, "unit_1", + "impression_1", "network_1", "placement_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["networkName"].Value, Is.EqualTo("network_1")); + Assert.That(json["placement"].Value, Is.EqualTo("placement_1")); + } + + [Test] + public void AdRevenueDataToJsonStringIncludesRevenueFields() + { + var data = new AdRevenueData(AdTracker.MediatorName.AdMob, AdTracker.Format.Rewarded, "unit_1", + "impression_1", 1500000L, "USD", AdTracker.Precision.Exact); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["revenueMicros"].AsLong, Is.EqualTo(1500000L)); + Assert.That(json["currency"].Value, Is.EqualTo("USD")); + Assert.That(json["precision"].Value, Is.EqualTo("exact")); + Assert.That(json.HasKey("networkName"), Is.False); + } + + [Test] + public void AdFailedToLoadDataToJsonStringOmitsOptionalFieldsWhenAbsent() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1"); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["mediatorName"].Value, Is.EqualTo("AdMob")); + Assert.That(json["adFormat"].Value, Is.EqualTo("banner")); + Assert.That(json["adUnitId"].Value, Is.EqualTo("unit_1")); + Assert.That(json.HasKey("impressionId"), Is.False); + Assert.That(json.HasKey("placement"), Is.False); + Assert.That(json.HasKey("mediatorErrorCode"), Is.False); + } + + [Test] + public void AdFailedToLoadDataToJsonStringIncludesOptionalFieldsWhenPresent() + { + var data = new AdFailedToLoadData(AdTracker.MediatorName.AdMob, AdTracker.Format.Banner, "unit_1", + "placement_1", 42); + + var json = JSONNode.Parse(data.ToJsonString()); + + Assert.That(json["placement"].Value, Is.EqualTo("placement_1")); + Assert.That(json["mediatorErrorCode"].AsInt, Is.EqualTo(42)); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + + private static Purchases.Offering CreateOfferingWithPackage() + { + return new Purchases.Offering(JSONNode.Parse( + "{\"identifier\":\"default\",\"serverDescription\":\"desc\",\"availablePackages\":[" + + "{\"identifier\":\"$rc_monthly\",\"packageType\":\"MONTHLY\"," + + "\"product\":{\"title\":\"Monthly\",\"identifier\":\"monthly\"," + + "\"description\":\"Monthly access\",\"price\":9.99,\"priceString\":\"$9.99\"," + + "\"currencyCode\":\"USD\",\"productCategory\":\"SUBSCRIPTION\"}," + + "\"presentedOfferingContext\":{\"offeringIdentifier\":\"default\"}}" + + "]}" + )); + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta new file mode 100644 index 00000000..ad1eb1ad --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/TrackingTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 31362b5f5071458e9ba438e105bbbdee +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs new file mode 100644 index 00000000..1a3bc7b6 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs @@ -0,0 +1,158 @@ +using System.Collections.Generic; +using NUnit.Framework; +using RevenueCat.SimpleJSON; +using UnityEngine; + +namespace RevenueCat.Tests +{ + public class WrapperPassthroughTests + { + private GameObject _gameObject; + private Purchases _purchases; + private PurchasesWrapperSpy _wrapper; + + [SetUp] + public void SetUp() + { + _gameObject = new GameObject("RevenueCatTests"); + _purchases = _gameObject.AddComponent(); + _wrapper = new PurchasesWrapperSpy(); + _purchases.SetWrapper(_wrapper); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(_gameObject); + } + + [Test] + public void SetAttributesSerializesStringsAndNulls() + { + _purchases.SetAttributes(new Dictionary + { + ["plan"] = "premium", + ["nickname"] = null + }); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAttributes), 1); + var attributes = JSONNode.Parse((string)invocation.Arguments[0]); + Assert.That(attributes["plan"].Value, Is.EqualTo("premium")); + Assert.That(attributes["nickname"].IsNull, Is.True); + } + + [Test] + public void ShowInAppMessagesForwardsMessageTypes() + { + var messageTypes = new[] { Purchases.InAppMessageType.BillingIssue }; + + _purchases.ShowInAppMessages(messageTypes); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.ShowInAppMessages), 1); + Assert.That(invocation.Arguments[0], Is.SameAs(messageTypes)); + } + + [Test] + public void ShowInAppMessagesForwardsNullWhenNotSpecified() + { + _purchases.ShowInAppMessages(); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.ShowInAppMessages), 1); + Assert.That(invocation.Arguments[0], Is.Null); + } + + [Test] + public void OverridePreferredUILocaleForwardsLocale() + { + _purchases.OverridePreferredUILocale("de_DE"); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.OverridePreferredUILocale), 1); + Assert.That(invocation.Arguments[0], Is.EqualTo("de_DE")); + } + + [Test] + public void InvalidateCustomerInfoCacheCallsWrapper() + { + _purchases.InvalidateCustomerInfoCache(); + + AssertLastInvocation(nameof(IPurchasesWrapper.InvalidateCustomerInfoCache), 0); + } + + [Test] + public void InvalidateVirtualCurrenciesCacheCallsWrapper() + { + _purchases.InvalidateVirtualCurrenciesCache(); + + AssertLastInvocation(nameof(IPurchasesWrapper.InvalidateVirtualCurrenciesCache), 0); + } + + [Test] + public void SetAppsFlyerConversionDataSerializesNestedValues() + { + _purchases.SetAppsFlyerConversionData(new Dictionary + { + ["af_status"] = "Organic", + ["media_source"] = null, + ["click_count"] = 3, + ["nested"] = new Dictionary { ["key"] = "value" }, + ["list"] = new List { "a", "b" } + }); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAppsFlyerConversionData), 1); + var conversionData = JSONNode.Parse((string)invocation.Arguments[0]); + Assert.That(conversionData["af_status"].Value, Is.EqualTo("Organic")); + Assert.That(conversionData["media_source"].IsNull, Is.True); + Assert.That(conversionData["click_count"].AsInt, Is.EqualTo(3)); + Assert.That(conversionData["nested"]["key"].Value, Is.EqualTo("value")); + Assert.That(conversionData["list"][0].Value, Is.EqualTo("a")); + Assert.That(conversionData["list"][1].Value, Is.EqualTo("b")); + } + + [Test] + public void SyncAmazonPurchaseForwardsArgumentsInOrder() + { + _purchases.SyncAmazonPurchase("product_1", "receipt_1", "amazon_user_1", "USD", 9.99); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SyncAmazonPurchase), 5); + Assert.That(invocation.Arguments[0], Is.EqualTo("product_1")); + Assert.That(invocation.Arguments[1], Is.EqualTo("receipt_1")); + Assert.That(invocation.Arguments[2], Is.EqualTo("amazon_user_1")); + Assert.That(invocation.Arguments[3], Is.EqualTo("USD")); + Assert.That(invocation.Arguments[4], Is.EqualTo(9.99)); + } + + [Test] + public void CollectDeviceIdentifiersCallsWrapper() + { + _purchases.CollectDeviceIdentifiers(); + + AssertLastInvocation(nameof(IPurchasesWrapper.CollectDeviceIdentifiers), 0); + } + + [Test] + public void SetSimulatesAskToBuyInSandboxForwardsFlag() + { + _purchases.SetSimulatesAskToBuyInSandbox(true); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetSimulatesAskToBuyInSandbox), 1); + Assert.That(invocation.Arguments[0], Is.True); + } + + [Test] + public void SetAdjustIdForwardsValue() + { + _purchases.SetAdjustID("adjust_id_1"); + + var invocation = AssertLastInvocation(nameof(IPurchasesWrapper.SetAdjustID), 1); + Assert.That(invocation.Arguments[0], Is.EqualTo("adjust_id_1")); + } + + private PurchasesWrapperSpy.Invocation AssertLastInvocation(string method, int argumentCount) + { + Assert.That(_wrapper.Invocations, Has.Count.EqualTo(1)); + Assert.That(_wrapper.LastInvocation.Method, Is.EqualTo(method)); + Assert.That(_wrapper.LastInvocation.Arguments, Has.Length.EqualTo(argumentCount)); + return _wrapper.LastInvocation; + } + } +} diff --git a/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs.meta b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs.meta new file mode 100644 index 00000000..37eb4dc1 --- /dev/null +++ b/IntegrationTests/Assets/Tests/EditMode/WrapperPassthroughTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a8f77d15bb8d4c6e9bf89fe3ba126533 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/IntegrationTests/README.md b/IntegrationTests/README.md index ebb3fa4d..b2e8adfb 100644 --- a/IntegrationTests/README.md +++ b/IntegrationTests/README.md @@ -9,3 +9,12 @@ In order to use it: 1. Set the RevenueCat API key in the Purchases object 1. Set the appUserId if needed in the Purchases object 1. Set the Parent Panel, Button Prefab and Customer Info Label objects in the PurchasesListener. + +### Automated Edit Mode tests + +Executable tests live in `Assets/Tests/EditMode`. After importing `Purchases.unitypackage` and +`PurchasesUI.unitypackage` into this project, run them from **Window > General > Test Runner** by selecting +**EditMode** and **Run All**. + +CircleCI exports and imports both packages before running the same suite, so the tests cover the packaged SDK +that users install. diff --git a/RevenueCat/Scripts/AssemblyInfo.cs b/RevenueCat/Scripts/AssemblyInfo.cs new file mode 100644 index 00000000..a5345f79 --- /dev/null +++ b/RevenueCat/Scripts/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("RevenueCat.Tests.EditMode")] diff --git a/RevenueCat/Scripts/AssemblyInfo.cs.meta b/RevenueCat/Scripts/AssemblyInfo.cs.meta new file mode 100644 index 00000000..309d2935 --- /dev/null +++ b/RevenueCat/Scripts/AssemblyInfo.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0ad31db8e8574a72a9df2dd9140a8738 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/RevenueCat/Scripts/Purchases.cs b/RevenueCat/Scripts/Purchases.cs index 72eb4e6e..af736061 100644 --- a/RevenueCat/Scripts/Purchases.cs +++ b/RevenueCat/Scripts/Purchases.cs @@ -101,16 +101,21 @@ public partial class Purchases : MonoBehaviour /// Experimental: this API is unstable and may change in a future release. public RevenueCat.AdTracker AdTracker { get; private set; } + internal void SetWrapper(IPurchasesWrapper wrapper) + { + _wrapper = wrapper ?? throw new ArgumentNullException(nameof(wrapper)); + AdTracker = new RevenueCat.AdTracker(_wrapper); + } + private void Start() { #if UNITY_ANDROID && !UNITY_EDITOR - _wrapper = new PurchasesWrapperAndroid(); + SetWrapper(new PurchasesWrapperAndroid()); #elif (UNITY_IOS || UNITY_VISIONOS) && !UNITY_EDITOR - _wrapper = new PurchasesWrapperiOS(); + SetWrapper(new PurchasesWrapperiOS()); #else - _wrapper = new PurchasesWrapperNoop(); + SetWrapper(new PurchasesWrapperNoop()); #endif - AdTracker = new RevenueCat.AdTracker(_wrapper); if (!string.IsNullOrEmpty(proxyURL)) { _wrapper.SetProxyURL(proxyURL);