[PM-33510] feat: Add Play Billing Library dependency and PlayBillingManager#6680
[PM-33510] feat: Add Play Billing Library dependency and PlayBillingManager#6680SaintPatrck wants to merge 1 commit intomainfrom
Conversation
|
Great job! No new security vulnerabilities introduced in this pull request |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6680 +/- ##
==========================================
+ Coverage 85.57% 85.73% +0.16%
==========================================
Files 885 937 +52
Lines 59189 59691 +502
Branches 8406 8426 +20
==========================================
+ Hits 50650 51176 +526
+ Misses 5606 5567 -39
- Partials 2933 2948 +15
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
81de98d to
4e6c5a7
Compare
b8796c3 to
2aaac13
Compare
4e6c5a7 to
8fd77ba
Compare
|
Claude finished @SaintPatrck's task in 2m 22s —— View job Code Review: PlayBillingManager (PR #6680)
SummaryNo new issues found — this is a clean, well-structured PR. The existing review feedback from @david-livefront has already identified the actionable items (narrowing the Key observations:
No security, performance, or architectural concerns identified. |
fb9683d to
10562f1
Compare
dd9fd33 to
d692519
Compare
10562f1 to
b785449
Compare
e71988b to
5323470
Compare
| .enableOneTimeProducts() | ||
| .build(), | ||
| ) | ||
| .build() |
There was a problem hiding this comment.
Would it make sense to inject this as a singleton or do we want a new BillingClient for each request?
There was a problem hiding this comment.
Keeping it around is not needed. Maybe if we add more billing related functionality in the future, but right now we only need this one-off check that can be done on init.
| } catch (e: IllegalStateException) { | ||
| e.asFailure() | ||
| } finally { | ||
| billingClient.endConnection() |
There was a problem hiding this comment.
The startConnection and endConnection calls make me feel like we don't want to inject it 😄
There was a problem hiding this comment.
Agreed. That's another reason I chose to go with creating a new instance every time.
| return BillingException( | ||
| "Connection failed: ${connectionResult.debugMessage}", | ||
| ) | ||
| .asFailure() |
There was a problem hiding this comment.
Is there a specific part of this code that throws?
It would be nice if we could pull some of this stuff out of the try-catch block.
There was a problem hiding this comment.
A few calls do. I can isolate them.
app/src/standard/kotlin/com/x8bit/bitwarden/data/billing/manager/PlayBillingManagerImpl.kt
Outdated
Show resolved
Hide resolved
app/src/standard/kotlin/com/x8bit/bitwarden/data/billing/manager/PlayBillingManagerImpl.kt
Outdated
Show resolved
Hide resolved
5323470 to
1c8c1e5
Compare
1c8c1e5 to
6acd1f5
Compare
| .build() | ||
|
|
||
| return try { | ||
| val connectionResult = billingClient.connect() |
There was a problem hiding this comment.
What do you think of a reusable function like this?
private suspend fun <T> BillingClient.useConnection(
block: suspend BillingResult.() -> Result<T>,
): Result<T> = try {
block(
suspendCancellableCoroutine { continuation ->
startConnection(
object : BillingClientStateListener {
override fun onBillingSetupFinished(
billingResult: BillingResult,
) {
if (continuation.isActive) {
continuation.resume(billingResult)
}
}
override fun onBillingServiceDisconnected() {
// No-op: connect-per-call lifecycle, no reconnection.
}
},
)
},
)
} catch (e: IllegalStateException) {
e.asFailure()
} finally {
endConnection()
}
}Then we can do this:
return billingClient.useConnection {
if (responseCode != BillingClient.BillingResponseCode.OK) {
return@useConnection BillingException("Connection failed: $debugMessage")
.asFailure()
}
val (configResult, billingConfig) = billingClient.getBillingConfig()
if (configResult.responseCode != BillingClient.BillingResponseCode.OK ||
billingConfig == null
) {
BillingException("Config query failed: ${configResult.debugMessage}").asFailure()
} else {
billingConfig.countryCode.asSuccess()
}
}
}You might even be able to move the responseCode check into the helper method
6acd1f5 to
e0d8808
Compare

🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-33510
📔 Objective
Introduces a flavor-specific
PlayBillingManagerwrapping the Google Play Billing Library to determine whether in-app billing is available on the current device.com.android.billingclient:billingv8.3.0 as astandardImplementationdependencyPlayBillingManagerinterface withisInAppBillingSupportedFlow: StateFlow<Boolean>BillingClient.getBillingConfigAsync()with a connect-per-call lifecycle to probe billing availabilitytrue(F-Droid users are eligible for the upgrade flow)PlayBillingManagerintoBillingRepositoryImpl, delegatingisInAppBillingSupportedFlowthrough the repository layerBillingModuleto providePlayBillingManageras a singleton