Skip to content

Conversation

@ianrumac
Copy link
Collaborator

@ianrumac ianrumac commented Jan 26, 2026

Changes in this pull request

  • Refactor option picking for full ID usage and provide a type-safe base plan type

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run ktlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Overview

Greptile Summary

Refactored product option selection from string-based to type-safe sealed classes (BasePlanType and OfferType), introducing support for sw-auto and sw-none special values.

Key changes:

  • Introduced BasePlanType sealed class (Auto, Specific) to replace nullable string basePlanId
  • Introduced OfferType sealed class (Auto, None, Specific) with toOffer() conversion method
  • Updated DecomposedProductIds to parse and use these type-safe representations
  • Enhanced one-time purchase handling to support purchase options with offer tokens
  • Added comprehensive test coverage for the new types (199 new test lines in DecomposedProductIdsTest, 135 in OfferTypeTest, 246 in PaywallProductIdsTest)
  • Simplified offer conversion logic in StoreManager using the new toOffer() method
  • Updated Paywall.productIds to fallback to products_v3 when products_v2 is empty

Additional changes:

  • Added consume calls for in-app purchases in AutomaticPurchaseController.syncSubscriptionStatusAndWait() and queryPurchasesOfType()
  • Updated offer token logic for one-time purchases to handle purchase options correctly

Confidence Score: 4/5

  • This PR is safe to merge with one area requiring verification around in-app purchase consumption behavior
  • The refactoring is well-structured with comprehensive test coverage (580+ new test lines). The type-safe sealed classes improve code safety and clarity. However, the added consume calls for in-app purchases in AutomaticPurchaseController warrant verification to ensure they don't inadvertently consume purchases that should persist (e.g., non-consumable in-app purchases or purchases that need to be validated server-side first).
  • Pay close attention to superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt - verify the in-app purchase consumption behavior is intentional and won't affect non-consumable products

Important Files Changed

Filename Overview
superwall/src/main/java/com/superwall/sdk/store/abstractions/product/StoreProduct.kt Added BasePlanType and OfferType sealed classes for type-safe product option selection, replacing string-based approach
superwall/src/main/java/com/superwall/sdk/store/abstractions/product/RawStoreProduct.kt Refactored to use BasePlanType instead of basePlanId string; updated one-time purchase logic to handle purchase options with offer tokens
superwall/src/main/java/com/superwall/sdk/billing/DecomposedProductIds.kt Updated to use BasePlanType and OfferType sealed classes for parsing product IDs with sw-auto and sw-none support
superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt Updated to use BasePlanType/OfferType; added offer token logic for one-time purchases with purchase options; added consume calls for in-app purchases
superwall/src/test/java/com/superwall/sdk/billing/DecomposedProductIdsTest.kt Comprehensive test suite for DecomposedProductIds parsing with all permutations of base plan and offer types
superwall/src/test/java/com/superwall/sdk/store/abstractions/product/OfferTypeTest.kt New test suite for OfferType sealed class, covering factory methods, conversions, and equality checks

Sequence Diagram

sequenceDiagram
    participant Client as SDK Client
    participant QueryUseCase as QueryProductDetailsUseCase
    participant Decomposer as DecomposedProductIds
    participant RawProduct as RawStoreProduct
    participant OfferSelection as Offer Selection Logic
    participant BillingClient as Google BillingClient

    Client->>QueryUseCase: Query products (productId:basePlan:offer)
    QueryUseCase->>Decomposer: Parse product ID
    Decomposer->>Decomposer: Split by ":"
    Decomposer->>Decomposer: Parse basePlan (sw-auto → Auto, specific ID → Specific)
    Decomposer->>Decomposer: Parse offer (sw-auto → Auto, sw-none → None, specific ID → Specific)
    Decomposer-->>QueryUseCase: DecomposedProductIds(basePlanType, offerType)
    
    QueryUseCase->>BillingClient: Query product details from Google Play
    BillingClient-->>QueryUseCase: ProductDetails
    
    QueryUseCase->>RawProduct: Create RawStoreProduct(details, basePlanType, offerType)
    RawProduct->>OfferSelection: getSelectedOfferDetails()
    
    alt One-Time Purchase Product
        OfferSelection->>OfferSelection: Check offerType
        alt offerType is None (sw-none)
            OfferSelection->>OfferSelection: Filter purchase options without offers
            OfferSelection->>OfferSelection: Select cheapest option
        else offerType is Specific
            OfferSelection->>OfferSelection: Filter by specific offer ID
        else offerType is Auto
            OfferSelection->>OfferSelection: Select option with lowest price
        end
        OfferSelection-->>RawProduct: SelectedOfferDetails.OneTime
    else Subscription Product
        OfferSelection->>OfferSelection: Check basePlanType
        alt basePlanType is Specific
            OfferSelection->>OfferSelection: Filter offers by base plan ID
        else basePlanType is Auto
            OfferSelection->>OfferSelection: Use first base plan
        end
        
        OfferSelection->>OfferSelection: Check offerType
        alt offerType is Auto
            OfferSelection->>OfferSelection: Find longest free trial
            OfferSelection->>OfferSelection: Or find cheapest offer
        else offerType is None
            OfferSelection->>OfferSelection: Use base plan only
        else offerType is Specific
            OfferSelection->>OfferSelection: Find offer by ID
        end
        OfferSelection-->>RawProduct: SelectedOfferDetails.Subscription
    end
    
    RawProduct-->>QueryUseCase: StoreProduct with selected offer
    QueryUseCase-->>Client: List<StoreProduct>
Loading

@ianrumac ianrumac merged commit 023c503 into develop Jan 26, 2026
1 check failed
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

1 file reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +382 to +386
purchasesList.forEach {
scope.launch {
Superwall.instance.consume(it.purchaseToken)
}
}
Copy link

Choose a reason for hiding this comment

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

this consumes ALL purchases including subscriptions - queryPurchasesOfType is called with both SUBS and INAPP product types at line 317-318, so subscription purchases will incorrectly be consumed

Suggested change
purchasesList.forEach {
scope.launch {
Superwall.instance.consume(it.purchaseToken)
}
}
// Only consume in-app purchases, not subscriptions
if (productType == BillingClient.ProductType.INAPP) {
purchasesList.forEach {
scope.launch {
Superwall.instance.consume(it.purchaseToken)
}
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: superwall/src/main/java/com/superwall/sdk/store/AutomaticPurchaseController.kt
Line: 382:386

Comment:
this consumes ALL purchases including subscriptions - `queryPurchasesOfType` is called with both `SUBS` and `INAPP` product types at line 317-318, so subscription purchases will incorrectly be consumed

```suggestion
            // Only consume in-app purchases, not subscriptions
            if (productType == BillingClient.ProductType.INAPP) {
                purchasesList.forEach {
                    scope.launch {
                        Superwall.instance.consume(it.purchaseToken)
                    }
                }
            }
```

How can I resolve this? If you propose a fix, please make it concise.

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