RevenueCat

Build With RevenueCat

Build a customized mobile subscription business with RevenueCat. We do the heavy lifting of normalizing subscribers from any source and maintain a single source of truth for subscription status, so you can get back to building your app.

RevenueCat is a powerful, secure, reliable, and free to use in-app purchase server with global support. All you need to get started is an API key.

Ask A Question

Questions

1

Mac Catalyst - Purchases.shared.entitlements calls callback with empty entitlements

Hi - I have an iOS app running on a Mac using Catalyst in Mac OS 10.15, Xcode 11.2 beta 2 (11B44). When the app calls `Purchases.shared.entitlements`, on iOS, the one entitlement I have set up in RevenueCat is returned. Building/running the same code on a Mac, the callback is called with `error` == `nil` and `entitlements` == `[]`. Purchases.shared.entitlements { (entitlements, error) in // entitlements is a dictionary with 0 elements // error is nil } Debug output shows no errors and looks like it's making the API calls fine: > 2019-10-21 16:06:40.698573-0500 MyApp[20463:252613] [Purchases] - DEBUG: Debug logging enabled. > 2019-10-21 16:06:40.698633-0500 MyApp[20463:252613] [Purchases] - DEBUG: SDK Version - 2.6.0 > 2019-10-21 16:06:40.698673-0500 MyApp[20463:252613] [Purchases] - DEBUG: Initial App User ID - (null) > 2019-10-21 16:06:40.698926-0500 MyApp[20463:252613] [Purchases] - DEBUG: GET /v1/subscribers/ABCDEBFG-OBFUSCATED-ID-CODE-FGHIJKLM > 2019-10-21 16:06:40.699270-0500 MyApp[20463:252613] [Purchases] - DEBUG: GET /v1/subscribers/ABCDEBFG-OBFUSCATED-ID-CODE-FGHIJKLM/products > 2019-10-21 16:06:40.700428-0500 MyApp[20463:252613] [Purchases] - DEBUG: No cached entitlements, fetching > 2019-10-21 16:06:40.703482-0500 MyApp[20463:252613] [Purchases] - DEBUG: Vending purchaserInfo from cache > 2019-10-21 16:06:40.953341-0500 MyApp[20463:252613] [Purchases] - DEBUG: applicationDidBecomeActive > 2019-10-21 16:06:41.056315-0500 MyApp[20463:253189] [Purchases] - DEBUG: GET /v1/subscribers/ABCDEBFG-OBFUSCATED-ID-CODE-FGHIJKLM 200 > 2019-10-21 16:06:41.121544-0500 MyApp[20463:253169] [Purchases] - DEBUG: GET /v1/subscribers/ABCDEBFG-OBFUSCATED-ID-CODE-FGHIJKLM/products 200 I'm using Purchases SDK v2.6.0 installed via CocoaPods. I'm building/running by just selecting "My Mac" as the build device and clicking the play button icon (that is, running the app). Is there some different way we need to run Mac Catalyst apps? Thanks! Grant

Posted by Grant about 8 hours ago

2
ANSWERED

purchaserInfo received in viewDidLoad is not the latest purchaserInfo (Mac Catalyst)

Starting a new question to help clarify the issue in my testing (Mac Catalyst): On the first load of the app after the subscription has expired, I am still getting isActive = true for the app's only entitlement. If I then load the app again, it correctly shows isActive = false, but on the first load of the app after expiration, isActive still shows as true. Similarly, on the first load of the app after an auto-renewal of a subscription, the expiration date on the purchaserInfo received in viewDidLoad still shows as the expiration date from the initial purchase. Only on the next load of the app does it correctly show the updated expiration date for the renewal period. Each time I run the app, the 'purchaserInfo' that is loaded is the previous purchaserInfo as opposed to the most updated one, so it shows as still active when it has expired, or it shows a past expiration date when the expiration date was recently updated due to renewal. Are you seeing the same in your testing of Mac Catalyst apps? Any help would be greatly appreciated! // Get the latest purchaserInfo to see if we have a full version user or not Purchases.shared.purchaserInfo { (purchaserInfo, error) in if let e = error { print(e.localizedDescription) } if let purchaserInfo = purchaserInfo { if purchaserInfo.entitlements["FullVersion"]?.isActive == true { let dateFormatter = DateFormatter() dateFormatter.dateStyle = .full dateFormatter.timeStyle = .long if let purchaseDate = purchaserInfo.purchaseDate(forEntitlement: "FullVersion") { print("Purchase Date: \(dateFormatter.string(from: purchaseDate))") } if let expirationDate = purchaserInfo.expirationDate(forEntitlement: "FullVersion") { print("Expiration Date: \(dateFormatter.string(from: expirationDate))") } } } }

Posted by Ran 11 days ago

1
ANSWERED

My offerings hierarchy and some notes

I'm working on a Mac App Store app, available as a Standard and Pro subscription, with a monthly or yearly renewal interval, with 2-week trials. Here's what I have for my new offerings hierarchy: PRODUCTS (which map directly to the MAS product identifiers) com.mycompany.myapp.pro_1month com.mycompany.myapp.pro_1year com.mycompany.myapp.standard_1month com.mycompany.myapp.standard_1year OFFERINGS default 1. pro_monthly = Professional monthly package = com.mycompany.myapp.pro_1month 2. pro_yearly = Professional yearly package = com.mycompany.myapp.pro_1year 3. standard_monthly = Standard monthly package = com.mycompany.myapp.standard_1month 4. standard_yearly = Standard yearly package = com.mycompany.myapp.pro_1year ENTITLEMENTS pro = Professional features 1. com.mycompany.myapp.pro_1month 2. com.mycompany.myapp.pro_1year standard = Standard features 1. com.mycompany.myapp.standard_1month 2. com.mycompany.myapp.standard_1year On launch, I use purchaserInfoWithCompletionBlock set the delegate which means I also get an immediate didReceiveUpdatedPurchaserInfo call with the latest purchaserInfo. This also means that same delegate method will be called with updated purchaserInfo if any future changes happen to the subscription. That delegate stores the current purchaserInfo then broadcasts out a notification on the main thread so any UI elements can refresh, hide, or appear to reflect the current purchased entitlement, which is simply purchaserInfo.entitlements.active.allValues.firstObject since I'm only looking for "pro" or "standard". I use offeringsWithCompletionBlock to grab the current offering (aka the "default" one listed above) and, while I'm at it, I iterate through offerings.current.availablePackages to grab all the package.product.productIdentifier's so I have a dynamic array of MAS product identifiers instead of hardcoding anything. I then feed that into checkTrialOrIntroductoryPriceEligibility to see which identifiers are eligible for the 2-week trial. If receiveEligibility[productIdentifier].status != RCIntroEligibilityStatusIneligible then I assume a trial is possible so I add that product identifier to a Set for quick lookups. I fill a popup of available packages by iterating over the current offering's availablePackages: [self.plansPopUp addItemWithTitle:[NSString stringWithFormat:NSLocalizedString(@"%@ %@", nil), package.product.localizedDescription, package.localizedPriceString] representedObject:package]; So I have popup entries like "Professional (1 Month) $6.99", for example. The current active plan in the popup is where package.product.productIdentifier equals activeEntitlementInfo.productIdentifier. And when they make the purchase I just grab the popup item's representedObject, which is that item's package, to send to purchasePackage. There's some quickly logic to form the various fine-print text based on what they choose to show trial period (if eligible), rate, interval, boilerplate text, etc. But, long story short, this is working beautifully! I can start with a monthly Standard, let it auto-renew for a bit, switch to a monthly Pro, auto-renew, self-expire after a bit (due to sandbox), which switches the app instantly to a read-only mode, make a new purchase to get going again, etc. It's all working brilliantly! I'll continue testing but I'm loving how straightforward this has been! Congrats to the RC team!

Posted by George Browning 12 days ago