Fetch Products from Galaxy Store#2908
Conversation
|
|
||
| // TODO: Remove this when we figure out how to properly integrate the Samsung SDK | ||
| // Avoid merging Android manifests for JVM unit tests to prevent minSdk conflicts from optional AARs. | ||
| unitTests.isIncludeAndroidResources = false |
There was a problem hiding this comment.
This is a temporary hack that I introduced so that I can run the unit tests locally despite the fact that the Samsung SDK's minSDK != our minSDK requirement.
We should remove this when we properly add in the Samsung SDK as a dependency to our SDK.
| package com.revenuecat.purchases.galaxy.constants | ||
|
|
||
| @SuppressWarnings("MagicNumber") | ||
| internal enum class GalaxyErrorCode(val code: Int, val description: String) { |
There was a problem hiding this comment.
There are more error codes used by the Galaxy Store, we'll add them in as needed
tonidero
left a comment
There was a problem hiding this comment.
Still need to review tests but looking great! Mostly a couple concerns over race conditions.
| val cachedProducts = productIds.mapNotNull(productDataCache::get) | ||
|
|
||
| synchronized(this) { | ||
| this.inFlightRequest = request |
There was a problem hiding this comment.
I guess there is a small chance of a race condition between this being set, and when we check at the beginning of this method. I wonder if we should maybe only check whether there is an inflight request here?
Again, this probably doesn't apply right now since we're executing requests serially though.
There was a problem hiding this comment.
Honestly, I think that all of the synchronization in this class is probably unnecessary since we're executing the requests serially. That said, would it just be safer to make this entire getProductDetails() function synchronized by annotating it with @Synchronized?
There was a problem hiding this comment.
Hmm well, I guess it's dangerous to synchronize on a possibly long block of code. Specially considering that may be doing network requests (the actual fetching of products). So I would be against that. I do agree it's probably unnecessary considering we're executing all of this serially, but just concerning that it's not part of the contract, and this could potentially be executed from somewhere else (though it shouldn't)... Still, I guess I wouldn't consider it a blocker
There was a problem hiding this comment.
Might be worth removing all the synchronous code, and instead create a single thread to execute all these Galaxy operations. We've created single thread executors for backend requests, and we could follow a similar approach. See:
There was a problem hiding this comment.
Would this have any advantages over the SerialRequestExecutor/synchronized model we're using now? I'm not super familiar with the executors, so I asked Codex about it, and it gave me this:
A single-thread ScheduledExecutorService only serializes the submitted runnables. Our requests are async: we enqueue a block, it kicks off work, returns
immediately, and completes later via a callback. That means the executor thread would finish the runnable while the request is still in flight, and the next runnable
would start right away—breaking the one-at-a-time guarantee. SerialRequestExecutor keeps the head request “active” until its finish is called, so even though the work
hops off to other threads, we don’t start the next until the callback signals completion. To drop it, we’d need every request to fully execute and finish within the
executor’s runnable, or refactor to chain scheduling on callback completion, which is what SerialRequestExecutor already does.
There was a problem hiding this comment.
Well, this would be mostly a replacement for the synchronized model we have, not for the SerialRequestExecutor, since I assume the galaxy SDK operations are done in another thread, so that wouldn't protect us against doing multiple requests to the galaxy SDK.
And well, as I mentioned, it could potentially simplify the synchronization. But well, considering we have the SerialRequestExecutor, we can probably get away with removing all that, and documenting well that requests are expected to be performed in serial... Not ideal in case we miss that constraint, since we don't have any compile time guarantees... but might be the simplest for now.
There was a problem hiding this comment.
Sounds good! Removed all synchronized blocks in 43a226f
I also introduced a new @GalaxySerialOperation annotation that I applied to the getProductDetails() function. This annotation acts similarly to how the @ExperimentalPreviewRevenueCatPurchasesAPI works, where it forces any consumers of the annotated functions to explicitly opt-in to it. This will at least ensure that in the future, any other developer that tries to call these functions must acknowledge that the function must be called serially with other annotated functions. Let me know what you think!
| ) | ||
|
|
||
| synchronized(this) { | ||
| this.inFlightRequest = request |
There was a problem hiding this comment.
Same here about possible race conditions.
| ) | ||
|
|
||
| @get:Synchronized | ||
| internal val productDataCache = mutableMapOf<String, ProductVo>() |
There was a problem hiding this comment.
Just wondering, would it be worth caching the StoreProduct itself instead of ProductVo? So we don't need to transform it on every call to get product details. Might be missing a reason why we need to cache the original ProductVo though.
There was a problem hiding this comment.
Oooh, that's a good idea! Honestly, the only reason is so that we can re-use the same handleSuccessfulProductsResponse() function, which takes in ProductVos as a param.
I'll move the code around a little so that we can cache the StoreProduct instead :)
tonidero
left a comment
There was a problem hiding this comment.
I think this looks great. Thanks for addressing everything!! This will be great 💪
| // - An empty string: queries all products | ||
| // - A string with one product ID in it: queries for that one product | ||
| // - A string with multiple product IDs in it, delimited by a comma | ||
| val productIdRequestString = productIds.joinToString(separator = ",") |
There was a problem hiding this comment.
I guess if we have some of the products in the cache, we would still be querying for all of them. Probably a small optimization though, so not a blocker.
There was a problem hiding this comment.
Will add this to a list of future optimizations to make!
| } | ||
| } | ||
|
|
||
| override fun onGetProducts(error: ErrorVo, products: ArrayList<ProductVo>) { |
There was a problem hiding this comment.
Just to check, the API would return either a non-success ErrorVo or a list of products, but not both correct? For example, if part of the productIds requested do not exist, or we're passing an incorrect productId format for some of them, would it return the valid products, without an error? Or would it return the valid ones + an error?
In case it's the second case, we probably should log the error but still return a sucess result I think.
There was a problem hiding this comment.
I just tested this out! If you request 2 valid product IDs and 1 invalid product IDs, it returns:
- A non-error (success)
ErrorVo - An ArrayList with the 2 valid products 😅
I'll see if we can create an error log for this in a follow-up PR
There was a problem hiding this comment.
Created Linear ticket CAT-2148 to track this
| import com.samsung.android.sdk.iap.lib.listener.OnGetProductsDetailsListener | ||
|
|
||
| internal interface IAPHelperProvider { | ||
| fun getProductsDetails( |
There was a problem hiding this comment.
I'm wondering if we should also annotate this method with the @GalaxySerialRequest, so if/when we add other operation helpers, it's easier to remember we need to add that annotation there as well.
| ?: localProperties.getProperty("samsungIapSdkPath") | ||
| }.map { path -> | ||
| val aar = file(path) | ||
| check(aar.exists()) { |
There was a problem hiding this comment.
I guess this will fail to work if the aar is not set somehow... we shouldn't get this merged to main, but it's ok to keep it on the integration branch. Hopefully we can figure out a way to integrate it 🙏
There was a problem hiding this comment.
Yeah, this is 1000% a temporary thing and should not be merged into main. We'll work with @MarkVillacampa at some point to find a proper way to integrate the Samsung SDK into our SDK 😄
Description
Supports fetching products from the Galaxy Store.
Notable Files
GalaxyBillingWrapper: Main abstraction class for the Galaxy Store.SerialRequestExecutor: Responsible for ensuring that we only make one call to the Galaxy Store at a time, since calling multiple functions that make networks requests to the store is unsupported and causes the Samsung SDK to throw an exception.ProductDataHandler: Handles the fetching of products from the Samsung SDK.StoreProductConversions: Handles the conversions from SamsungProductVoinstances to ourGalaxyStoreProductclassSamsung IAP SDK Inclusion
We're now pulling in the Samsung IAP SDK from the local machine while we determine how the SDK should be distributed.
local.propertiesfile:samsungIapSdkPath=/.../SamsungInAppPurchaseSDK_v6.5.0/Libs/samsung-iap-6.5.0.aar