mirror of
https://github.com/bitwarden/android.git
synced 2026-08-29 10:17:56 -05:00
[PM-33508] feat: Add AuthenticatedBillingApi and BillingService network layer (#6668)
This commit is contained in:
@@ -9,6 +9,7 @@ import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.bitwarden.network.provider.TokenProvider
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.BillingService
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
@@ -70,6 +71,11 @@ interface BitwardenServiceClient {
|
||||
*/
|
||||
val authRequestsService: AuthRequestsService
|
||||
|
||||
/**
|
||||
* Provides access to the Billing service.
|
||||
*/
|
||||
val billingService: BillingService
|
||||
|
||||
/**
|
||||
* Provides access to the Ciphers service.
|
||||
*/
|
||||
|
||||
@@ -15,6 +15,8 @@ import com.bitwarden.network.retrofit.RetrofitsImpl
|
||||
import com.bitwarden.network.service.AccountsServiceImpl
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.AuthRequestsServiceImpl
|
||||
import com.bitwarden.network.service.BillingService
|
||||
import com.bitwarden.network.service.BillingServiceImpl
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.CiphersServiceImpl
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
@@ -115,6 +117,12 @@ internal class BitwardenServiceClientImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override val billingService: BillingService by lazy {
|
||||
BillingServiceImpl(
|
||||
authenticatedBillingApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val ciphersService: CiphersService by lazy {
|
||||
CiphersServiceImpl(
|
||||
azureApi = retrofits.createStaticRetrofit().create(),
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.bitwarden.network.api
|
||||
|
||||
import com.bitwarden.network.model.CheckoutSessionRequestJson
|
||||
import com.bitwarden.network.model.CheckoutSessionResponseJson
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import com.bitwarden.network.model.PortalUrlResponseJson
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines raw calls under the /account/billing API with authentication applied.
|
||||
*/
|
||||
internal interface AuthenticatedBillingApi {
|
||||
|
||||
/**
|
||||
* Creates a Stripe checkout session for premium upgrade.
|
||||
*/
|
||||
@POST("/account/billing/vnext/premium/checkout")
|
||||
suspend fun createCheckoutSession(
|
||||
@Body body: CheckoutSessionRequestJson,
|
||||
): NetworkResult<CheckoutSessionResponseJson>
|
||||
|
||||
/**
|
||||
* Creates a Stripe customer portal session for managing the premium subscription.
|
||||
*/
|
||||
@POST("/account/billing/vnext/portal-session")
|
||||
suspend fun getPortalUrl(): NetworkResult<PortalUrlResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Request object for creating a Stripe checkout session for premium upgrade.
|
||||
*
|
||||
* @property platform The platform identifier (e.g., "android" or "ios").
|
||||
*/
|
||||
@Serializable
|
||||
data class CheckoutSessionRequestJson(
|
||||
@SerialName("platform")
|
||||
val platform: String,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Response object returned when creating a premium checkout session.
|
||||
*
|
||||
* @property checkoutSessionUrl The Stripe checkout URL for premium upgrade.
|
||||
*/
|
||||
@Serializable
|
||||
data class CheckoutSessionResponseJson(
|
||||
@SerialName("checkoutSessionUrl")
|
||||
val checkoutSessionUrl: String,
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Response object returned when requesting a Stripe customer portal session.
|
||||
*
|
||||
* @property url The Stripe customer portal URL.
|
||||
*/
|
||||
@Serializable
|
||||
data class PortalUrlResponseJson(
|
||||
@SerialName("url")
|
||||
val url: String,
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.bitwarden.network.service
|
||||
|
||||
import com.bitwarden.network.model.CheckoutSessionResponseJson
|
||||
import com.bitwarden.network.model.PortalUrlResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for interacting with the billing endpoints.
|
||||
*/
|
||||
interface BillingService {
|
||||
|
||||
/**
|
||||
* Creates a Stripe checkout session for premium upgrade.
|
||||
*/
|
||||
suspend fun createCheckoutSession(): Result<CheckoutSessionResponseJson>
|
||||
|
||||
/**
|
||||
* Creates a Stripe customer portal session for managing the premium subscription.
|
||||
*/
|
||||
suspend fun getPortalUrl(): Result<PortalUrlResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.bitwarden.network.service
|
||||
|
||||
import com.bitwarden.network.api.AuthenticatedBillingApi
|
||||
import com.bitwarden.network.model.CheckoutSessionRequestJson
|
||||
import com.bitwarden.network.model.CheckoutSessionResponseJson
|
||||
import com.bitwarden.network.model.PortalUrlResponseJson
|
||||
import com.bitwarden.network.util.toResult
|
||||
|
||||
private const val PLATFORM = "android"
|
||||
|
||||
/**
|
||||
* The default implementation of the [BillingService].
|
||||
*/
|
||||
internal class BillingServiceImpl(
|
||||
private val authenticatedBillingApi: AuthenticatedBillingApi,
|
||||
) : BillingService {
|
||||
|
||||
override suspend fun createCheckoutSession(): Result<CheckoutSessionResponseJson> =
|
||||
authenticatedBillingApi
|
||||
.createCheckoutSession(
|
||||
body = CheckoutSessionRequestJson(platform = PLATFORM),
|
||||
)
|
||||
.toResult()
|
||||
|
||||
override suspend fun getPortalUrl(): Result<PortalUrlResponseJson> =
|
||||
authenticatedBillingApi
|
||||
.getPortalUrl()
|
||||
.toResult()
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.bitwarden.network.service
|
||||
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.network.api.AuthenticatedBillingApi
|
||||
import com.bitwarden.network.base.BaseServiceTest
|
||||
import com.bitwarden.network.model.CheckoutSessionResponseJson
|
||||
import com.bitwarden.network.model.PortalUrlResponseJson
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.create
|
||||
|
||||
class BillingServiceTest : BaseServiceTest() {
|
||||
|
||||
private val billingApi: AuthenticatedBillingApi = retrofit.create()
|
||||
private val service = BillingServiceImpl(
|
||||
authenticatedBillingApi = billingApi,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `createCheckoutSession when response is Failure should return Failure`() =
|
||||
runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val actual = service.createCheckoutSession()
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createCheckoutSession when response is Success should return Success`() =
|
||||
runTest {
|
||||
val response = MockResponse()
|
||||
.setBody(CHECKOUT_SESSION_RESPONSE_JSON)
|
||||
.setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val actual = service.createCheckoutSession()
|
||||
assertEquals(CHECKOUT_SESSION_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPortalUrl when response is Failure should return Failure`() = runTest {
|
||||
val response = MockResponse().setResponseCode(400)
|
||||
server.enqueue(response)
|
||||
val actual = service.getPortalUrl()
|
||||
assertTrue(actual.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getPortalUrl when response is Success should return Success`() = runTest {
|
||||
val response = MockResponse()
|
||||
.setBody(PORTAL_URL_RESPONSE_JSON)
|
||||
.setResponseCode(200)
|
||||
server.enqueue(response)
|
||||
val actual = service.getPortalUrl()
|
||||
assertEquals(PORTAL_URL_RESPONSE.asSuccess(), actual)
|
||||
}
|
||||
}
|
||||
|
||||
private const val CHECKOUT_SESSION_RESPONSE_JSON = """
|
||||
{
|
||||
"checkoutSessionUrl": "https://checkout.stripe.com/c/pay/test_session_123"
|
||||
}
|
||||
"""
|
||||
|
||||
private val CHECKOUT_SESSION_RESPONSE = CheckoutSessionResponseJson(
|
||||
checkoutSessionUrl = "https://checkout.stripe.com/c/pay/test_session_123",
|
||||
)
|
||||
|
||||
private const val PORTAL_URL_RESPONSE_JSON = """
|
||||
{
|
||||
"url": "https://billing.stripe.com/p/session/test_portal_456"
|
||||
}
|
||||
"""
|
||||
|
||||
private val PORTAL_URL_RESPONSE = PortalUrlResponseJson(
|
||||
url = "https://billing.stripe.com/p/session/test_portal_456",
|
||||
)
|
||||
Reference in New Issue
Block a user