Search Unity

[Solved] IAP Getting renew receipts from subscriptions

Discussion in 'Unity IAP' started by toni_jarvinen, Apr 18, 2016.

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

    toni_jarvinen

    Joined:
    Apr 20, 2015
    Posts:
    5
    Hi,

    I have been struggling with getting auto-renewing subscriptions to work with Unity IAP. It seems that ProcessPurchase is not called on renewals for subscriptions automatically if the original purchase was marked as completed. Do I have to leave a subscription open as pending to get the renewals for processing in ProcessPurchase? (this seemed to get the renewals automatically) I also tried reading the local app receipt but that too doesn't get the auto-renewals without refreshing from Apple which prompts an iTunes login. (not a good user experience)

    There is the option of verifying the receipt with Apple that should get me renewals with old receipt but it seems odd that there is no way to do this with local verification.
     
    Last edited: Apr 18, 2016
  2. toni_jarvinen

    toni_jarvinen

    Joined:
    Apr 20, 2015
    Posts:
    5
    Could this problem be timing based? I tried adding the transaction observer when the UnityPurchasing.m instance is created (before products were returned) and I got the receipts to the updatedTransaction in UnityPurchasing.m. From here the plugin calls ProcessPurchase but as we don't have the products yet, we only get the product id.

    EDIT: I got this to work by editing the UnityPurchasing.m by holding the transactions that come before products were received and handling them after that. But this did require me editing the .m file.
     
    Last edited: Apr 19, 2016
  3. Banderous

    Banderous

    Joined:
    Dec 25, 2011
    Posts:
    669
    This should work fine. Unity IAP does not add a transaction observer immediately because it is awaiting product details. Do you have an XCode trace covering a subscription renewal coming into storekit?
     
  4. toni_jarvinen

    toni_jarvinen

    Joined:
    Apr 20, 2015
    Posts:
    5
    Now it seems that it works again without adding the transaction observer earlier. Testing with sandbox on iOS seems really inconsistent. But we did not receive the UpdateTransactions log from Unity IAP on renewals. The renewals had happened as restore was working as it should have. This may just be some weirdness in sandbox yesterday.

    But thank you for the help, it actually got me to check again. :)
     
    nicholasr likes this.
  5. F.Salka

    F.Salka

    Joined:
    Dec 13, 2013
    Posts:
    43
    If i'm correct, is it possible to check if the user renewed or cancelled their susbscription using Unity IAP without having to ask the user for iTunes credentials etc..? Apple docs has a way to do this locally (no server side) by getting the json reciept usin the app shared secret from iTunes Connect. This is particularly interesting since apple has just opened subscriptions to all developers including the games category.
     
  6. Banderous

    Banderous

    Joined:
    Dec 25, 2011
    Posts:
    669
    Can you link to those docs? As far as I know the shared secret is used when you communicate with Apple from your server using their verification API, which is why it is 'shared', and it is secret so should not be used by the client.

    You can validate the App receipt locally using our validation library, but getting an updated App receipt to the client requires the user to enter their password, when refreshing the app receipt.
     
  7. F.Salka

    F.Salka

    Joined:
    Dec 13, 2013
    Posts:
    43
    My bad, that was a third party tutorial that used local method to communicate with Apple without having to setup a dedicated server for verification.

    "Apple does have an API that, when given a user's local receipt and a “shared secret” generated in iTunes Connect, returns a JSON object of the user's purchase history for your app, including their current subscription information."

    http://savvyapps.com/blog/how-setup-test-auto-renewable-subscription-ios-app

    So as far as i understand, it should be possible to verify if the user cencelled or renewed their subscription without having to setup a dedicated server or ask the user for password using the PostPrecess method with the Apple Root Certificate?

    http://forum.unity3d.com/threads/unity-iap-subscription-expiration-returns-always-wrong-date.394520/
     
  8. Banderous

    Banderous

    Joined:
    Dec 25, 2011
    Posts:
    669
    What does your subscription entitle the user to? Are you just restricting a feature in the App?

    Yes, you can use local verification to see if a subscription is in date although it is trivial for a user to change the clock on their device to bypass the check.
     
    erika_d likes this.
  9. F.Salka

    F.Salka

    Joined:
    Dec 13, 2013
    Posts:
    43
    Thanks, appreciate the prompt reply! The app offers periodic new content (music learning app). We get the time from a Parse open source server so it's more difficult for the user to cheat the time, unfortunately Parse makes it difficult to do reciept verification with background jobs.

    According to WWDC, apps in all categories will be able to implement auto-renewable subscriptions soon. That should make it possible for casual games/non-content apps to move to subscription business model if I'm not mistaken?

    https://developer.apple.com/app-store/subscriptions/whats-new/

    Interested to see how this will affect the app/gaming market.
     
  10. Banderous

    Banderous

    Joined:
    Dec 25, 2011
    Posts:
    669
    I wasn't previously aware that games were prohibited from using subscriptions. I dealt with plenty of Unibill customers who made use of subscriptions, I guess they must have been making non game Apps.
     
  11. erika_d

    erika_d

    Joined:
    Jan 20, 2016
    Posts:
    413
    Hi @MMOERPG,

    This is on iOS, yes? To check for subscriptions without prompting for password, you can read the app receipt off the device storage, find the InAppProduct in the receipt with the id matching the subscription and check it's expiry date with the current time. Here's a link for reading the app receipt: http://docs.unity3d.com/Manual/UnityIAPiOSMAS.html
    NOTE: If you refresh the app receipt, this is when the user will be prompted for a password.

    Also here's some information on validating receipts, which is a good idea to do when reading receipts: http://docs.unity3d.com/Manual/UnityIAPValidatingReceipts.html
     
  12. MMOERPG

    MMOERPG

    Joined:
    Mar 21, 2014
    Posts:
    50
    Hi Erica,

    Thank you for the quick reply! I believe I have set this up properly but now that I am testing, the results are very strange.

    The most frustrating part is that I am not sure if it is an error in my code, or a bug with Apples Sandbox testing environment. I have read many complaints that the Sandbox environment is buggy and some days just doesn't work properly. Somebody shoot me.

    Anyway, when I initiate the purchase, I am prompt to enter my Apple ID password, I do, I confirm the subscription, then nothing. I have a debug panel set up to catch the results but I get no call to the ProcessPurchase function or the OnPurchaseFailed function. Now, I close the app completely, then relaunch it. I check the debug panel before doing anything, and all of my debug information that is called when the purchase is successful is populated, implying that the ProcessPurchase function is being called when the app is launched!?!?!

    This functionality occurs on every test account every time I launch the app after initiating the first auto renew subscription on that account even after the subscription has ended.

    Is this just how auto renew subscriptions work? I must retrieve the receipt myself from the devices hard drive upon purchase attempt and determine whether the subscription was purchased? Then every time the app launches, the subscription purchased code code is called so the check can again be made?

    I post this question on another forum as well:
    http://forum.unity3d.com/threads/un...eturns-always-wrong-date.394520/#post-2707936

    Here is my IAP Script:

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.Purchasing;
    5. using UnityEngine.UI;
    6.  
    7. // Placing the Purchaser class in the CompleteProject namespace allows it to interact with ScoreManager,
    8. // one of the existing Survival Shooter scripts.
    9.  
    10.     // Deriving the Purchaser class from IStoreListener enables it to receive messages from Unity Purchasing.
    11. using UnityEngine.Purchasing.Security;
    12. using System.Collections;
    13.  
    14.  
    15. public class IAPurchaser : MonoBehaviour, IStoreListener
    16. {
    17.     public GameObject DebugText;
    18.     public GameObject debugObj;
    19.     public GameObject DebugInfoPanel;
    20.     private static IStoreController m_StoreController;          // The Unity Purchasing system.
    21.     private static IExtensionProvider m_StoreExtensionProvider; // The store-specific Purchasing subsystems.
    22.  
    23.     // Product identifiers for all products capable of being purchased:
    24.     // "convenience" general identifiers for use with Purchasing, and their store-specific identifier
    25.     // counterparts for use with and outside of Unity Purchasing. Define store-specific identifiers
    26.     // also on each platform's publisher dashboard (iTunes Connect, Google Play Developer Console, etc.)
    27.  
    28.     // General product identifiers for the consumable, non-consumable, and subscription products.
    29.     // Use these handles in the code to reference which product to purchase. Also use these values
    30.     // when defining the Product Identifiers on the store. Except, for illustration purposes, the
    31.     // kProductIDSubscription - it has custom Apple and Google identifiers. We declare their store-
    32.     // specific mapping to Unity Purchasing's AddProduct, below.
    33.     public static string kProductIDConsumable =    "consumable";
    34.     public static string kProductIDNonConsumable = "nonconsumable";
    35.     public static string kProductIDSubscription =  "subscription";
    36.  
    37.     // Apple App Store-specific product identifier for the subscription product.
    38.     private static string kProductNameAppleSubscription =  "com.RobotnikGameLab.iHypnosis.fullSubscription";
    39.  
    40.     // Google Play Store-specific product identifier subscription product.
    41.     private static string kProductNameGooglePlaySubscription =  "com.unity3d.subscription.original";
    42.  
    43.     void Start()
    44.     {
    45.         // If we haven't set up the Unity Purchasing reference
    46.         if (m_StoreController == null)
    47.         {
    48.             // Begin to configure our connection to Purchasing
    49.             InitializePurchasing();
    50.         }
    51.     }
    52.  
    53.     public void InitializePurchasing()
    54.     {
    55.         // If we have already connected to Purchasing ...
    56.         if (IsInitialized())
    57.         {
    58.             // ... we are done here.
    59.             return;
    60.         }
    61.  
    62.         // Create a builder, first passing in a suite of Unity provided stores.
    63.         var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    64.  
    65.         // Add a product to sell / restore by way of its identifier, associating the general identifier
    66.         // with its store-specific identifiers.
    67.         builder.AddProduct(kProductIDConsumable, ProductType.Consumable);
    68.         // Continue adding the non-consumable product.
    69.         builder.AddProduct(kProductIDNonConsumable, ProductType.NonConsumable);
    70.         // And finish adding the subscription product. Notice this uses store-specific IDs, illustrating
    71.         // if the Product ID was configured differently between Apple and Google stores. Also note that
    72.         // one uses the general kProductIDSubscription handle inside the game - the store-specific IDs
    73.         // must only be referenced here.
    74.         builder.AddProduct(kProductIDSubscription, ProductType.Subscription, new IDs(){{ kProductNameAppleSubscription, AppleAppStore.Name },{ kProductNameGooglePlaySubscription, GooglePlay.Name },});
    75.  
    76.         // Kick off the remainder of the set-up with an asynchrounous call, passing the configuration
    77.         // and this class' instance. Expect a response either in OnInitialized or OnInitializeFailed.
    78.         UnityPurchasing.Initialize(this, builder);
    79.     }
    80.  
    81.  
    82.     private bool IsInitialized()
    83.     {
    84.         // Only say we are initialized if both the Purchasing references are set.
    85.         return m_StoreController != null && m_StoreExtensionProvider != null;
    86.     }
    87.  
    88.  
    89.     public void BuyConsumable()
    90.     {
    91.         // Buy the consumable product using its general identifier. Expect a response either
    92.         // through ProcessPurchase or OnPurchaseFailed asynchronously.
    93.         BuyProductID(kProductIDConsumable);
    94.     }
    95.  
    96.  
    97.     public void BuyNonConsumable()
    98.     {
    99.         // Buy the non-consumable product using its general identifier. Expect a response either
    100.         // through ProcessPurchase or OnPurchaseFailed asynchronously.
    101.         BuyProductID(kProductIDNonConsumable);
    102.     }
    103.  
    104.  
    105.     public void BuySubscription()
    106.     {
    107.         // Buy the subscription product using its the general identifier. Expect a response either
    108.         // through ProcessPurchase or OnPurchaseFailed asynchronously.
    109.         // Notice how we use the general product identifier in spite of this ID being mapped to
    110.         // custom store-specific identifiers above.
    111.         BuyProductID(kProductIDSubscription);
    112.     }
    113.  
    114.  
    115.     void BuyProductID(string productId)
    116.     {
    117.         // If Purchasing has been initialized ...
    118.         if (IsInitialized())
    119.         {
    120.             // ... look up the Product reference with the general product identifier and the Purchasing
    121.             // system's products collection.
    122.             Product product = m_StoreController.products.WithID(productId);
    123.  
    124.             // If the look up found a product for this device's store and that product is ready to be sold ...
    125.             if (product != null && product.availableToPurchase)
    126.             {
    127.                 Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));// ... buy the product. Expect a response either through ProcessPurchase or OnPurchaseFailed
    128.                 // asynchronously.
    129.                 m_StoreController.InitiatePurchase(product);
    130.             }
    131.             // Otherwise ...
    132.             else
    133.             {
    134.                 // ... report the product look-up failure situation
    135.                 Debug.Log("BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase");
    136.             }
    137.         }
    138.         // Otherwise ...
    139.         else
    140.         {
    141.             // ... report the fact Purchasing has not succeeded initializing yet. Consider waiting longer or
    142.             // retrying initiailization.
    143.             Debug.Log("BuyProductID FAIL. Not initialized.");
    144.         }
    145.     }
    146.  
    147.  
    148.     // Restore purchases previously made by this customer. Some platforms automatically restore purchases, like Google.
    149.     // Apple currently requires explicit purchase restoration for IAP, conditionally displaying a password prompt.
    150.     public void RestorePurchases()
    151.     {
    152.         // If Purchasing has not yet been set up ...
    153.         if (!IsInitialized())
    154.         {
    155.             // ... report the situation and stop restoring. Consider either waiting longer, or retrying initialization.
    156.             Debug.Log("RestorePurchases FAIL. Not initialized.");
    157.             return;
    158.         }
    159.  
    160.         // If we are running on an Apple device ...
    161.         if (Application.platform == RuntimePlatform.IPhonePlayer ||
    162.             Application.platform == RuntimePlatform.OSXPlayer)
    163.         {
    164.             // ... begin restoring purchases
    165.             Debug.Log("RestorePurchases started ...");
    166.  
    167.             // Fetch the Apple store-specific subsystem.
    168.             var apple = m_StoreExtensionProvider.GetExtension<IAppleExtensions>();
    169.             // Begin the asynchronous process of restoring purchases. Expect a confirmation response in
    170.             // the Action<bool> below, and ProcessPurchase if there are previously purchased products to restore.
    171.             apple.RestoreTransactions((result) => {
    172.                 // The first phase of restoration. If no more responses are received on ProcessPurchase then
    173.                 // no purchases are available to be restored.
    174.                 Debug.Log("RestorePurchases continuing: " + result + ". If no further messages, no purchases available to restore.");
    175.             });
    176.         }
    177.         // Otherwise ...
    178.         else
    179.         {
    180.             // We are not running on an Apple device. No work is necessary to restore purchases.
    181.             Debug.Log("RestorePurchases FAIL. Not supported on this platform. Current = " + Application.platform);
    182.         }
    183.     }
    184.  
    185.  
    186.     //
    187.     // --- IStoreListener
    188.     //
    189.  
    190.     public void OnInitialized(IStoreController controller, IExtensionProvider extensions)
    191.     {
    192.         // Purchasing has succeeded initializing. Collect our Purchasing references.
    193.         Debug.Log("OnInitialized: PASS");
    194.         InstantiateDebugText (DebugInfoPanel, "OnInitialized: PASS");
    195.         // Overall Purchasing system, configured with products for this application.
    196.         m_StoreController = controller;
    197.         // Store specific subsystem, for accessing device-specific store features.
    198.         m_StoreExtensionProvider = extensions;
    199.     }
    200.  
    201.  
    202.     public void OnInitializeFailed(InitializationFailureReason error)
    203.     {
    204.         // Purchasing set-up has not succeeded. Check error for reason. Consider sharing this reason with the user.
    205.         Debug.Log("OnInitializeFailed InitializationFailureReason:" + error);
    206.         InstantiateDebugText (DebugInfoPanel, "OnInitializeFailed InitializationFailureReason:" + error.ToString());
    207.     }
    208.  
    209.  
    210.     public PurchaseProcessingResult ProcessPurchase(PurchaseEventArgs args)
    211.     {
    212.         // A consumable product has been purchased by this user.
    213.         if (String.Equals(args.purchasedProduct.definition.id, kProductIDConsumable, StringComparison.Ordinal))
    214.         {
    215.             Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));// The consumable item has been successfully purchased, add 100 coins to the player's in-game score.
    216.         }
    217.         // Or ... a non-consumable product has been purchased by this user.
    218.         else if (String.Equals(args.purchasedProduct.definition.id, kProductIDNonConsumable, StringComparison.Ordinal))
    219.         {
    220.             Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));// TODO: The non-consumable item has been successfully purchased, grant this item to the player.
    221.         }
    222.         // Or ... a subscription product has been purchased by this user.
    223.         else if (String.Equals(args.purchasedProduct.definition.id, kProductIDSubscription, StringComparison.Ordinal))
    224.         {
    225.             debugObj.GetComponent<Text>().text = "Subscription Purchase was Successful!";
    226.             Debug.Log(string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));// TODO: The subscription item has been successfully purchased, grant this to the player.
    227.             InstantiateDebugText(DebugInfoPanel, string.Format("ProcessPurchase: PASS. Product: '{0}'", args.purchasedProduct.definition.id));
    228.             CheckIfSubscriptionIsActive ();
    229.         }
    230.         // Or ... an unknown product has been purchased by this user. Fill in additional products here....
    231.         else
    232.         {
    233.          
    234.             Debug.Log(string.Format("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    235.             InstantiateDebugText (DebugInfoPanel, string.Format ("ProcessPurchase: FAIL. Unrecognized product: '{0}'", args.purchasedProduct.definition.id));
    236.         }
    237.  
    238.         // Return a flag indicating whether this product has completely been received, or if the application needs
    239.         // to be reminded of this purchase at next app launch. Use PurchaseProcessingResult.Pending when still
    240.         // saving purchased products to the cloud, and when that save is delayed.
    241.         return PurchaseProcessingResult.Complete;
    242.     }
    243.  
    244.     public void CheckIfSubscriptionIsActive(){
    245.         ConfigurationBuilder builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    246.         // Get a reference to IAppleConfiguration during IAP initialization.
    247.         IAppleConfiguration appleConfig = builder.Configure<IAppleConfiguration>();
    248.         if (!string.IsNullOrEmpty (appleConfig.appReceipt)) {
    249.             Debug.Log (appleConfig.appReceipt);
    250. //            InstantiateDebugText (DebugInfoPanel, "APP Receipt Base64 " + appleConfig.appReceipt);
    251.             var receiptData = System.Convert.FromBase64String (appleConfig.appReceipt);
    252. //            InstantiateDebugText (DebugInfoPanel, "receipt Data "+ receiptData);
    253.             AppleReceipt receipt = new AppleValidator (AppleTangle.Data ()).Validate (receiptData);
    254.             InstantiateDebugText (DebugInfoPanel, "Apple receipt " + receipt);
    255.             foreach (AppleInAppPurchaseReceipt productReceipt in receipt.inAppPurchaseReceipts) {
    256.                 Debug.Log ("PRODUCTID: " + productReceipt.productID);
    257.                 Debug.Log ("PURCHASE DATE: " + productReceipt.purchaseDate);
    258.                 Debug.Log ("EXPIRATION DATE: " + productReceipt.subscriptionExpirationDate);
    259.                 Debug.Log ("CANCELDATE DATE: " + productReceipt.cancellationDate);
    260.  
    261.                 InstantiateDebugText (DebugInfoPanel, "PRODUCTID: " + productReceipt.productID);
    262.                 InstantiateDebugText (DebugInfoPanel, "PURCHASE DATE: " + productReceipt.purchaseDate);
    263.                 InstantiateDebugText (DebugInfoPanel, "EXPIRATION DATE: " + productReceipt.subscriptionExpirationDate);
    264.                 InstantiateDebugText (DebugInfoPanel, "CANCELDATE DATE: " + productReceipt.cancellationDate);
    265.             }
    266.         }
    267.     }
    268.  
    269.     public void InstantiateDebugText(GameObject DebugGrid, string debugMessage){
    270.         GameObject debugText = Instantiate (DebugText, Vector3.zero, Quaternion.identity) as GameObject;
    271.         debugText.transform.SetParent (DebugGrid.transform);
    272.         debugText.GetComponent<Text> ().text = debugMessage;
    273.         debugText.GetComponent<RectTransform> ().localPosition = new Vector3 (0, 0, 0);
    274.         debugText.GetComponent<RectTransform> ().localScale = new Vector3 (1, 1, 1);
    275.     }
    276.  
    277.     public void ClearDebugText(){
    278.         for(int i = 0; i < DebugInfoPanel.transform.childCount; i++){
    279.             Destroy (DebugInfoPanel.transform.GetChild(i).gameObject);
    280.         }
    281.     }
    282.  
    283.  
    284.     public void OnPurchaseFailed(Product product, PurchaseFailureReason failureReason)
    285.     {
    286.         debugObj.GetComponent<Text> ().text = "Subscription Purchase Failed";
    287.         // A product purchase attempt did not succeed. Check failureReason for more detail. Consider sharing
    288.         // this reason with the user to guide their troubleshooting actions.
    289.         Debug.Log(string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    290.         InstantiateDebugText (DebugInfoPanel, string.Format("OnPurchaseFailed: FAIL. Product: '{0}', PurchaseFailureReason: {1}", product.definition.storeSpecificId, failureReason));
    291.     }
    292.  
    293. }
     
    Last edited: Jul 13, 2016
  13. erika_d

    erika_d

    Joined:
    Jan 20, 2016
    Posts:
    413
    Hi @MMOERPG,

    Which version of IAP are you on? I notice you're instantiating multiple ConfigurationBuilders...this should be fixed in the latest version of IAP (1.6.0), but this breaks previous versions...perhaps this is causing your issue?
     
    MMOERPG likes this.
  14. MMOERPG

    MMOERPG

    Joined:
    Mar 21, 2014
    Posts:
    50
    I have no idea what you mean by that. How do I check which IAP version I'm running and how do I upgrade to 1.6.0 if I'm not running it? The scripts I'm using are from unity docs and community. If the upgrade breaks something that's fine, I'll fix it.

    Thanks so much for the replies @erika_d @Banderous I'm going nuts tying to get this working.
     
  15. erika_d

    erika_d

    Joined:
    Jan 20, 2016
    Posts:
    413
    @MMOERPG,

    You can find out which version you currently have imported by going to Assets -> Plugins -> UnityPurchasing -> Changelog.md. The first line of the file lists the version of IAP. To upgrade to latest version just go to In-App Purchasing in the Services window of the editor and click Import.

    Can you link me to which docs specifically you used? I want to make sure to update them so they don't suggest multiple builders! Thanks :)
     
    MMOERPG likes this.
  16. MMOERPG

    MMOERPG

    Joined:
    Mar 21, 2014
    Posts:
    50
    Ah, so before I saw your reply I reimport the IAP plugin from the services panel. Not sure if I had the old version or the current but nothing seemed to break. I'm uploading the build now.

    Most of the IAP script is derived from the script in this Unity tutorial:
    https://unity3d.com/learn/tutorials/topics/analytics/integrating-unity-iap-your-game

    And the receipt validation CheckIfSubscriptionIsValid is derived from this community post:
    http://forum.unity3d.com/threads/un...eturns-always-wrong-date.394520/#post-2708419

    I just realized what may have happened thanks to your hints. It appears that both of those scripts instantiate a builder and I didn't know enough to remove one. I'll do that now and see if it helps... Thanks!!!
     
  17. MMOERPG

    MMOERPG

    Joined:
    Mar 21, 2014
    Posts:
    50
    @erika_d @Banderous

    Alright, this is now working properly. I removed the second call to instantiate the builder and I moved the reference for the AppleConfig object to the store initialization function. Perhaps this alone, perhaps also reimporting the IAP plugin fixed the issue.

    Thanks so much for the help!
     
    erika_d likes this.
Thread Status:
Not open for further replies.