Open
Conversation
Comment on lines
+350
to
+357
| private suspend fun findActiveSubscriptionToken(): String? { | ||
| isConnected.first { it } | ||
| val purchases = | ||
| queryPurchasesOfType(BillingClient.ProductType.SUBS).getOrNull() ?: return null | ||
| return purchases | ||
| .firstOrNull { it.purchaseState == Purchase.PurchaseState.PURCHASED } | ||
| ?.purchaseToken | ||
| } |
Contributor
There was a problem hiding this comment.
Picks arbitrary subscription for replacement
findActiveSubscriptionToken() returns the first subscription with PURCHASED state. When a user has multiple active subscriptions, this may select the wrong one to replace. The replaceProduct method receives productDetails for the new product but has no information about which existing subscription should be replaced.
Consider accepting a target product ID (or the old subscription's product ID) and filtering the purchases to find the matching subscription token. For example:
Suggested change
| private suspend fun findActiveSubscriptionToken(): String? { | |
| isConnected.first { it } | |
| val purchases = | |
| queryPurchasesOfType(BillingClient.ProductType.SUBS).getOrNull() ?: return null | |
| return purchases | |
| .firstOrNull { it.purchaseState == Purchase.PurchaseState.PURCHASED } | |
| ?.purchaseToken | |
| } | |
| private suspend fun findActiveSubscriptionToken(targetProductId: String? = null): String? { | |
| isConnected.first { it } | |
| val purchases = | |
| queryPurchasesOfType(BillingClient.ProductType.SUBS).getOrNull() ?: return null | |
| return purchases | |
| .filter { it.purchaseState == Purchase.PurchaseState.PURCHASED } | |
| .let { active -> | |
| if (targetProductId != null) { | |
| active.firstOrNull { it.products.contains(targetProductId) } | |
| } else { | |
| active.firstOrNull() | |
| } | |
| } | |
| ?.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: 350-357
Comment:
**Picks arbitrary subscription for replacement**
`findActiveSubscriptionToken()` returns the first subscription with `PURCHASED` state. When a user has multiple active subscriptions, this may select the wrong one to replace. The `replaceProduct` method receives `productDetails` for the *new* product but has no information about which *existing* subscription should be replaced.
Consider accepting a target product ID (or the old subscription's product ID) and filtering the purchases to find the matching subscription token. For example:
```suggestion
private suspend fun findActiveSubscriptionToken(targetProductId: String? = null): String? {
isConnected.first { it }
val purchases =
queryPurchasesOfType(BillingClient.ProductType.SUBS).getOrNull() ?: return null
return purchases
.filter { it.purchaseState == Purchase.PurchaseState.PURCHASED }
.let { active ->
if (targetProductId != null) {
active.firstOrNull { it.products.contains(targetProductId) }
} else {
active.firstOrNull()
}
}
?.purchaseToken
}
```
How can I resolve this? If you propose a fix, please make it concise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes in this pull request
Checklist
CHANGELOG.mdfor any breaking changes, enhancements, or bug fixes.ktlintin the main directory and fixed any issues.Greptile Summary
This PR adds subscription product replacement (upgrade/downgrade) support to the Superwall Android SDK via Google Play Billing's
SubscriptionUpdateParams. A newreplace_productmessage from the paywall webview flows through the existing message handling pipeline and ultimately callsBillingClient.launchBillingFlow()with subscription update parameters.ReplacementModeenum mapping Superwall mode names (default,charge_later,charge_now,charge_difference,charge_on_expire) to Play BillingSubscriptionUpdateParams.ReplacementModeinteger constantsReplaceProductpaywall message andInitiateReplacementweb event through the existing message pipelinereplaceProduct()toAutomaticPurchaseControllerwhich buildsSubscriptionUpdateParamsand launches the billing flowfindActiveSubscriptionToken()picks the first active subscription token for replacement, which could select the wrong subscription when a user has multiple active subscriptionsConfidence Score: 3/5
AutomaticPurchaseController.kt— specificallyfindActiveSubscriptionToken()andreplaceProduct()for multi-subscription edge cases.Important Files Changed
ReplaceProductmessage variant and its JSON parser for thereplace_productevent from the webview. Parsing includes proper validation ofreplacement_mode.InitiateReplacementweb event carrying productId, replacementMode, and shouldDismiss. Clean addition following existing patterns.ReplaceProductmessages toInitiateReplacementevents. Correctly includes hidden paywall detection and haptic feedback, consistent with other purchase flows.InitiateReplacementevent by delegating toTransactionManager.purchase()withreplacementMode. Follows the same guard/loading/task pattern asInitiatePurchase.replaceProduct()andfindActiveSubscriptionToken(). The token lookup picks the first active subscription arbitrarily which could replace the wrong subscription when multiple are active.automaticPurchaseControllerproperty via safe cast. Simple, correct addition.replacementModetoPurchaseSource.Internaland branches purchase logic to callreplaceProduct()when replacement mode is set and internal controller is active. Falls back to normal purchase path otherwise.Sequence Diagram
sequenceDiagram participant WV as PaywallWebView participant PMH as PaywallMessageHandler participant SW as Superwall participant TM as TransactionManager participant APC as AutomaticPurchaseController participant GPB as Google Play Billing WV->>PMH: replace_product message PMH->>PMH: Parse ReplaceProduct (productId, replacementMode, shouldDismiss) PMH->>SW: InitiateReplacement event SW->>SW: Guard: purchaseTask == null SW->>SW: Set LoadingPurchase state SW->>TM: purchase(Internal(productId, state, replacementMode)) TM->>TM: Check: replacementMode != null && hasInternalPurchaseController TM->>APC: replaceProduct(activity, productDetails, basePlanId, offerId, replacementMode) APC->>APC: findActiveSubscriptionToken() APC->>GPB: queryPurchasesAsync(SUBS) GPB-->>APC: List of active purchases APC->>APC: Build SubscriptionUpdateParams with oldToken + replacementMode APC->>GPB: launchBillingFlow(flowParams) GPB-->>APC: PurchaseResult via onPurchasesUpdated APC-->>TM: PurchaseResult TM-->>SW: Handle result (success/fail/cancel)Prompt To Fix All With AI
Last reviewed commit: b95aa07