Conversation
There was a problem hiding this comment.
I think this is ok... Since we basically won't be getting any value back, but lmk if you have any concerns.
There was a problem hiding this comment.
I moved this log from where it was before to here. Before, it would log success before the request succeeded. I think this is a better place but lmk if you have any concerns
There was a problem hiding this comment.
This is the main problem I had after extracting the logic to this class from the Purchases object. After we fetch customer info from the backend, we store the new value in the cache and we send the updated value to the listeners if it has changed. Both operations are synchronized on the purchases object, so I thought moving them from there could cause concurrency problems.
Since this method is always called from Purchases, I added this callback to perform those 2 operations. Originally, I intended to use the already existing ReceiveCustomerInfoCallback instead of this new callback, but whether we execute the operations or not depends on if the customer info comes from cache or from the backend and we don't have that information on the callback.
This solution seems okish considering it's internal but lmk what you think.
There was a problem hiding this comment.
I see your point...
Are we caching or doing anything else with the customer info in Purchases though? If we are not maybe it's fine to remove this callback and move the cacheCustomerInfoAndSendToDelegateIfChanged to this class and synchronize on it.
If not, I think we don't have any other solution, unless we create a private monitor in Purchases (instead of synchronizing on Purchases's monitor) and pass it to this class?
There was a problem hiding this comment.
Also, I was a bit confused by the name of the callback, maybe we could name it onSuccessfulFetchCallback? That way the name doesn't dictate what the implementation of the callback is supposed to do (caching the customer info).
There was a problem hiding this comment.
Purchases also caches the customer info from some other sources, like when we restore purchases, we sync purchases with the backend or post a purchase to the backend. We could rename the CustomerInfoRetriever to CustomerInfoHelper and have all those cache pathways go through this class.
There is also the listener that is synchronized on the purchases object, and that was harder than the caching problem I would say... One solution I thought of is having the Purchases listener act as a proxy but having the actual listener live on the CustomerInfoRetriever/CustomerInfoHelper but I wasn't sure of this solution either...
Going to give it another thought but lmk if you would prefer something like that.
There was a problem hiding this comment.
I went with the approach I suggested. The PR got pretty big... But I think we should start moving logic away from purchases, so I think it's a good change overall. Lmk what you think.
There was a problem hiding this comment.
With this policy, we will ignore the cache and just fetch from the backend, which is what we were doing before.
There was a problem hiding this comment.
Honestly, not sure why that was marked as nullable... It was always non null, lmk if I missed anything.
There was a problem hiding this comment.
Interesting! I added that specific nullability in #70 . I am not sure why though 🤔
I checked for issues around that date regarding Handler and couldn't find anything either.
Let's remove the nullability and keep an eye on it just in case
There was a problem hiding this comment.
These are covered by other tests, and the fact that we have moved this logic out of here.
There was a problem hiding this comment.
We were not calling clearCachesForAppUserID in here at all. I think this was an outdated test?
There was a problem hiding this comment.
I think it was to prevent someone from adding it? So if someone adds a call mockCache.clearCachesForAppUserID it will fail. Not a super clear test lol
There was a problem hiding this comment.
Ok will add it back and rename it or add a comment so it's a bit clearer 👍
There was a problem hiding this comment.
These 2 are the operations I mentioned on this other comment: https://github.com/RevenueCat/purchases-android/pull/546/files#r896692163
There was a problem hiding this comment.
For the hybrids to work properly we need these error codes to match between iOS and Android. This is because our error handling in the hybrids is based on the premise that both iOS and Android will send the same code for the same error. In iOS error 25 means empty subscriber attributes at the moment https://github.com/RevenueCat/purchases-ios/blob/main/Sources/Error%20Handling/ErrorCode.swift#L49
At the moment, we are mapping errors to a error map that gets sent to the hybrids. See that code here https://github.com/RevenueCat/purchases-hybrid-common/blob/main/android/src/main/java/com/revenuecat/purchases/hybridcommon/common.kt#L343. So developers would get code: 25, readableErrorCode: CustomerInfoNotFoundInCache in Android and something different in iOS.
I know it is very messy and error prone, we should be doing a mapping from those error codes to the hybrids' own error system, like we should be mapping error code 25 in Android to a particular Javascript (or Dart, or whatever language the hybrid framework uses) error and a different error code in iOS. But we don't have anything like that in place yet.
I see we have error 29 in iOS https://github.com/RevenueCat/purchases-ios/blob/main/Sources/Error%20Handling/ErrorCode.swift#L54 which means CustomerInfoError. Maybe we could bring Android's errors to parity with iOS and use that one. I am not sure if that's the error iOS uses for cases when the customer info is not cached.
There was a problem hiding this comment.
Ah interesting thanks for the knowledge dump!
There was a problem hiding this comment.
Interesting! I added that specific nullability in #70 . I am not sure why though 🤔
I checked for issues around that date regarding Handler and couldn't find anything either.
Let's remove the nullability and keep an eye on it just in case
There was a problem hiding this comment.
I see your point...
Are we caching or doing anything else with the customer info in Purchases though? If we are not maybe it's fine to remove this callback and move the cacheCustomerInfoAndSendToDelegateIfChanged to this class and synchronize on it.
If not, I think we don't have any other solution, unless we create a private monitor in Purchases (instead of synchronizing on Purchases's monitor) and pass it to this class?
There was a problem hiding this comment.
Also, I was a bit confused by the name of the callback, maybe we could name it onSuccessfulFetchCallback? That way the name doesn't dictate what the implementation of the callback is supposed to do (caching the customer info).
tonidero
left a comment
There was a problem hiding this comment.
This got bigger than I expected with the latest refactor... Still, I think it's cleaner than the previous approach, with the extra callback. Lmk what you think.
There was a problem hiding this comment.
I didn't add errors 26 and 27, since they seemed more related to iOS... I can port them here, but I believe we won't be using them. Lmk if you have any concerns over this.
There was a problem hiding this comment.
So I was debating with myself on whether to use the same pattern we use with the PurchasesState here and create something like CustomerInfoHelperState or something like that to hold the listener and the lastSentCustomerInfo. Maybe making the state immutable would be better, but I believe it should also work like this, since access to both properties should be synchronized. I went with the simple approach for now. Lmk if you think we should mimic that pattern cc @vegaro
There was a problem hiding this comment.
Separated from that, I was wondering if we've thought about using WeakReference to hold the listener. That way we avoid possible leaks in our users. That said, this might break some of our users implementations, so might not be a good idea.
There was a problem hiding this comment.
I made the listener on the Purchases class just a proxy for the one in the CustomerInfoHelper. Most operations related with customer info now will need to go through there.
There was a problem hiding this comment.
I added this since I was getting confused with a lot of the deprecated warnings we have (since we need to be testing the deprecated code as well)
There was a problem hiding this comment.
| * Get purchaser info from cache or network depending on fetch policy. | |
| * Get customer info from cache or network depending on fetch policy. |
Line 242 also needs to be updated
There was a problem hiding this comment.
Would it make sense to move these three lines to @After?
There was a problem hiding this comment.
Well I think either would work... Will move it to @After since it does seem more of a cleanup task 👍
There was a problem hiding this comment.
I think it was to prevent someone from adding it? So if someone adds a call mockCache.clearCachesForAppUserID it will fail. Not a super clear test lol
There was a problem hiding this comment.
This can be replaced with a @Synchronized annotation on set(value)
NachoSoto
left a comment
There was a problem hiding this comment.
I think this looks good 👍🏻
I'll let others approve it though
There was a problem hiding this comment.
This should be fail("supposed to be a failure")
…foAndSendToDelegateIfChanged
…nctionality from Purchases to this class
…ding a comment for clarification

Description
https://revenuecats.atlassian.net/browse/CSDK-121
New API:
PurchasesgetCustomerInfogets a new overload with aCacheFetchPolicyparameter. More info on the behavior for each case of this policy in the documentation:I moved a lot of the logic dealing with customer info operations to a new class
CustomerInfoHelperin order to move as much logic as possible away fromPurchases.TODO