Skip to content

Acknowledge Galaxy Subscription Purchases#2939

Merged
fire-at-will merged 16 commits into
samsung-devfrom
acknowledge-purchases
Dec 17, 2025
Merged

Acknowledge Galaxy Subscription Purchases#2939
fire-at-will merged 16 commits into
samsung-devfrom
acknowledge-purchases

Conversation

@fire-at-will

@fire-at-will fire-at-will commented Dec 15, 2025

Copy link
Copy Markdown
Contributor

Description

Adds support for acknowledging Galaxy subscription purchases.

@fire-at-will fire-at-will requested a review from a team as a code owner December 15, 2025 19:47
@fire-at-will fire-at-will added the pr:feat A new feature label Dec 15, 2025
""".trimIndent()

return ProductVo(json)
return mockk<ProductVo>(relaxed = true).also { productVo ->

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 started having some issues instantiating ProductVo and PurchaseVo directly, so I decided to mock them out instead

@tonidero tonidero 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.

Looking good! Just some comments but almost there!

clearInFlightRequest()
onError?.invoke(purchasesError)
return
} else if (acknowledgementResults.count() > 1) {

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.

Hmm so I guess you can acknowledge multiple purchases simultaneously? Interesting... but yeah I think it's fine to do one per request as we're doing now, so I imagine this should never happen indeed 👍

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.

Yeah, you can acknowledge/consume multiple purchases at once. In fact, their docs even strongly recommend that you don't acknowledge them serially:

We recommend reporting purchased consumable items immediately after verifying their purchase and reporting all unreported products in one consumePurchasedItems() or acknowledgePurchases() method call in order to avoid system overload or malfuction.

However, our system is only architected today to handle acknowleding/consuming one purchase at a time - see the BillingAbstract.consumeAndSave function's signature:

abstract fun consumeAndSave(
        finishTransactions: Boolean,
        purchase: StoreTransaction,
        shouldConsume: Boolean,
        initiationSource: PostReceiptInitiationSource,
    )

I think that this is okay for the purchase scenario - the Galaxy Store doesn't have multi-line subscriptions, so only one purchase should go through at a time. The only case I can think of where we might have multiple to do at once is in a restore scenario, or maybe when multiple distinct POST /receipt requests have failed, and the SDK needs to re-POST receipts. Even in that scenario though, we'll be calling this endpoint serially, so I think it should be fine, what do you think?

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.

Yeah, I think it should be fine... probably worth seeing how it behaves in the wild though, hopefully it's not rate limited on their side 😬

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.

cc @MarkVillacampa this might be a good thing to ask the Samsung team

initiationSource: PostReceiptInitiationSource,
) {
warnLog { "Unimplemented: GalaxyBillingWrapper.consumeAndSave" }
if (!finishTransactions || purchase.type == ProductType.UNKNOWN) return

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.

In these cases, I think we should still call addSuccessfullyPostedToken so we're not posting the same tokens over and over? As long as it's not in a pending state though.

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.

It looks like we aren't calling addSuccessfullyPostedToken for UNKNOWN product types on the the Amazon or Play Store today:

Google:

if (purchase.type == ProductType.UNKNOWN || purchase.purchaseState == PurchaseState.PENDING) {

Amazon:

if (!shouldFinishTransactions() || purchase.type == RevenueCatProductType.UNKNOWN) return

Where those two stores differ is whether they call addSuccessfullyPostedToken in observer mode 😅

  • We don't for Amazon
  • We do for the Play Store

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 Amazon is wrong in this case... If we don't add it, we will be posting the token on every app open I think?

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, I believe you're correct 😅 I'll add the token for the Galaxy Store for both UNKNOWN product types and when observer mode is enabled

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.

Updated in 7dbaab9 to store the token when the product type is Unknown or when observer mode is enabled 👍


@Test
fun `consumeAndSave does nothing when finishTransactions is false`() {
val wrapper = spyk(createWrapper())

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.

Hmm not a super fan of creating a spy just to verify that a method of the class we're testing is not called... I think the proper thing to do would be to verify whether the expected side effects (in this case, calling the IAPHelper consume/acknowledge methods are not called)

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.

Removed the spies from the tests here! 🚫🕵️‍♂️

d45f206

@fire-at-will fire-at-will changed the title Acknowledge & Consume Galaxy IAPs Acknowledge Galaxy Subscription Purchases Dec 16, 2025
@fire-at-will

fire-at-will commented Dec 16, 2025

Copy link
Copy Markdown
Contributor Author

@tonidero Since your last review, I've made the following changes to this PR:

  • GalaxyBillingWrapper.consumeAndSave only handles acknowledging subscriptions. OTPs are no longer consumed or acknowledged. I kept a backup of the old code so implementing this behavior will be easier in the future :)
    • This includes removing the ConsumePurchaseHandler and associated code
  • GalaxyBillingWrapper.consumeAndSave adds the purchase token to the cache's list of successfully posted tokens when finishTransactions is false or when the purchase's product type is UNKNOWN
  • Fixed the bug where we wouldn't call the onError closure in AcknowledgePurchaseHandler. Added a unit test to ensure that it gets called :)
  • Reworked those GalaxyBillingWrapper tests to not use a spy

@tonidero tonidero 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 good to me!

private val deviceCache: DeviceCache,
val billingMode: GalaxyBillingMode,
private val iapHelperProvider: IAPHelperProvider = DefaultIAPHelperProvider(
private val iapHelper: IAPHelperProvider = DefaultIAPHelperProvider(

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'm wondering if we should call it IAPHelperWrapper instead of Provider... but that should probably be in a separate PR.

}

override fun onGetProducts(error: ErrorVo, products: ArrayList<ProductVo>) {
override fun onGetProducts(error: ErrorVo, products: ArrayList<ProductVo?>) {

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.

It's quite interesting this can return a nullable product list... Any idea in what scenarios this would return a list with null elements?

@fire-at-will fire-at-will Dec 17, 2025

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 cant' think of any scenarios where this list should contain null elements, but because of the way that the function's signature is written in Java, it's a scenario that is allowed by the API. The signature in Java looks like this:

public interface OnGetProductsDetailsListener {
    void onGetProducts(@NotNull ErrorVo var1, @NotNull ArrayList<ProductVo> var2);
}

var2 is marked as @NotNull, which means that var2 itself is not null, but it doesn't guarantee that the objects inside the list are non-null. In fact, when we use Android Studio's auto complete to write the function, it includes the ? on the ProductVo. If we didn't mark ProductVo as optional in this signature, I think that we would get a crash when the code bridges from Java to Kotlin if there are any null elements in the list 😬

I realized this while working on the acknowledgement listener, which also receives an ArrayList of nullable objects, so I went back and fixed it here before I forgot about it :)

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 right 😓 Makes sense. thanks for explaining!!

@fire-at-will fire-at-will merged commit 62f6c26 into samsung-dev Dec 17, 2025
0 of 2 checks passed
@fire-at-will fire-at-will deleted the acknowledge-purchases branch December 17, 2025 13:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:feat A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants