Skip to content

Re-fetch cached offerings and products after Storefront changes (3/4) #1743

Merged
NachoSoto merged 19 commits into
RevenueCat:mainfrom
Juanpe:refetch_products_after_Storefront_change
Jul 6, 2022
Merged

Re-fetch cached offerings and products after Storefront changes (3/4) #1743
NachoSoto merged 19 commits into
RevenueCat:mainfrom
Juanpe:refetch_products_after_Storefront_change

Conversation

@Juanpe

@Juanpe Juanpe commented Jun 28, 2022

Copy link
Copy Markdown
Contributor

Follow up to #1583. Subtask of #1382

Description

This PR aims to invalidate and re-fetch if appropriate the stuff in the cache when the Storefront changes. I've used both OfferingsManager and ProductsManager to manage the logic of invalidate and re-fetch the data.

Changes:

  • Add invalidateAndReFetchCachedOfferingsIfAppropiate method to OfferingsManager class.
  • Rename clearCachedProducts method to invalidateAndReFetchCachedProductsIfAppropiate in ProductsManager class.
  • Add OfferingsManager as a dependency of PurchasesOrchestrator.
  • Clear cache methods returns the identifiers of the elements that have been removed or an empty list if there was no stuff in the cache.
  • Add tests to cover some scenarios.

}

func clearCachedProducts() {
func invalidateAndReFetchCachedProductsIfAppropiate() {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure here if we should log the error when the re-request fails. WDYT? 🤔

return productIDsFromBackend.subtracting(productIDsFromStore)
}

func invalidateAndReFetchCachedOfferingsIfAppropiate(appUserID: String) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't add any test case to cover this logic. The problem is the OfferingsManager tests file belongs to the UnitTests framework I'm not sure if we should move it to StoreKitTests framework in order to be able to change the Storefront. I also thought about creating another test file called OfferingsManagerSKTests that contains the test cases related to StoreKit. WDYT? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also thought about creating another test file called OfferingsManagerSKTests that contains the test cases related to StoreKit. WDYT?

Yeah that makes sense, we do that for other classes 👍🏻 Maybe OfferingsManagerStoreKitTests

@NachoSoto NachoSoto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! Just a few suggestions.

Comment thread Sources/Purchasing/StoreKit1/ProductsFetcherSK1.swift Outdated
Comment thread Sources/Purchasing/StoreKit2/ProductsFetcherSK2.swift
Comment on lines +49 to +53
let cachedProductIdentifiers = self.cachedProductsByIdentifier.keys
if !cachedProductIdentifiers.isEmpty {
self.cachedProductsByIdentifier.removeAll(keepingCapacity: false)
}
return Set(cachedProductIdentifiers)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was like "this is a race condition!". Then realized this is an actor 😄

await productsFetcherSK2.clearCache()
let removedProductIdentifiers = await productsFetcherSK2.clearCache()
if !removedProductIdentifiers.isEmpty {
_ = try? await self.productsFetcherSK2.products(identifiers: removedProductIdentifiers)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense 👍🏻
Maybe we can add a debug log here? :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean only when the request fails or just to inform that the products have been re-fetched? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both

Comment thread Sources/Purchasing/ProductsManager.swift Outdated
Comment thread Sources/Purchasing/OfferingsManager.swift Outdated
Juanpe and others added 6 commits June 28, 2022 19:15
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
Co-authored-by: NachoSoto <NachoSoto@users.noreply.github.com>
@Juanpe

Juanpe commented Jun 28, 2022

Copy link
Copy Markdown
Contributor Author

This looks great! Just a few suggestions.

Thanks for the feedback 👌🏼 I made the suggested changes. LMK if you find anything else that could be improved 👍🏼

@NachoSoto NachoSoto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool I think this is ready.

// changes to invalidate the cache. The changes are now managed by
// `StoreKit2StorefrontListenerDelegate`.
sk1Fetcher.clearCache()
sk1Fetcher.clearCache { _ in }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to revert this now with the optional completion

// changes to invalidate the cache. The changes are now managed by
// `StoreKit2StorefrontListenerDelegate`.
await sk2Fetcher.clearCache()
_ = await sk2Fetcher.clearCache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

expect(cachedProducts).notTo(beEmpty())

await productsFetcherSK2.clearCache()
_ = await productsFetcherSK2.clearCache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

// changes to invalidate the cache. The changes are now managed by
// `StoreKit2StorefrontListenerDelegate`.
sk1Fetcher.clearCache()
sk1Fetcher.clearCache { _ in }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

// changes to invalidate the cache. The changes are now managed by
// `StoreKit2StorefrontListenerDelegate`.
await sk2Fetcher.clearCache()
_ = await sk2Fetcher.clearCache()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

mockProducts.forEach(self.productsFetcherSK1.cacheProduct)

productsFetcherSK1.clearCache()
productsFetcherSK1.clearCache { _ in }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

let cachedProductIdentifiers = self.cachedProductsByIdentifier.keys
if !cachedProductIdentifiers.isEmpty {
Logger.debug(Strings.offering.product_cache_invalid_for_storefront_change)
self.cachedProductsByIdentifier.removeAll()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add keepingCapacity: false here too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, is there a possibility that there may not be the same number of products in different Storefronts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, it should be the same. I was just suggesting that since you had keepingCapacity: false in the SK2 version. Might make sense to empty cache storage in both cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, well, keepingCapacity: false was already in SK2 and false is the default value, but I think it might be a good idea to explicitly state that we are completely clearing the storage 👍🏼

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done ✅ (0f59e57)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was just asking to be consistent hehe 👍🏻

@NachoSoto

Copy link
Copy Markdown
Contributor

There's a couple lint issues in MockOfferingsManager. Maybe add switlint:ignore identifier_name?

@Juanpe

Juanpe commented Jul 5, 2022

Copy link
Copy Markdown
Contributor Author

There's a couple lint issues in MockOfferingsManager. Maybe add switlint:ignore identifier_name?

Oups, you're right. I've disabled this rule for these properties. (9988fb5)

BTW, my local copy of the project has 35 warnings related to the linter. Is it ok? I checked and I think I have the config files up to date 🤔
Screenshot 2022-07-05 at 14 31 02

@NachoSoto NachoSoto left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird. What version of SwiftLint do you have? I wonder if there a new rule that got added?

var invokedInvalidateAndReFetchCachedOfferingsIfAppropiate = false
var invokedInvalidateAndReFetchCachedOfferingsIfAppropiateCount = 0

// swiftlint:disable identifier_name

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just my technique, if you're disabling a rule next to the code that breaks it I'd either re-enable it afterwards or do disable:next. Otherwise I would move this to the top of the file.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I used this technique because this code is at the end of this file and as far as I know this disable only affects the code below. However, I agree with you that we need to re-enable this rule after code because we may add more code in the future that would be affected by this disable. Let me change it 👍🏼

@Juanpe

Juanpe commented Jul 5, 2022

Copy link
Copy Markdown
Contributor Author

I made the change. Finally, I disable the identifier_name for the entire file. It's a mock and we did the same for other classes, like MockTrialOrIntroPriceEligibilityChecker or MockSubscriberAttributesManager.

BTW, in order to make all the necessary changes...could you please review the comment related to keep the capacity after cleaning the cache?

Thanks for the review and the feedback! 🤙

@NachoSoto NachoSoto merged commit 9f498b8 into RevenueCat:main Jul 6, 2022
@taquitos taquitos mentioned this pull request Jul 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants