Convert donations apis to WebSocket.

This commit is contained in:
Cody Henthorne
2025-04-02 10:58:43 -04:00
committed by Michelle Tang
parent 2f9692a1a0
commit ecb040ce98
27 changed files with 575 additions and 392 deletions

View File

@@ -21,8 +21,6 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
@@ -42,12 +40,10 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.NetworkResult
import java.math.BigDecimal
import java.util.Currency
@@ -72,11 +68,6 @@ class MessageBackupsCheckoutActivityTest {
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit
mockkObject(SignalNetwork)
every { SignalNetwork.archive } returns mockk {
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
}
mockkStatic(RemoteConfig::class)
every { RemoteConfig.messageBackups } returns true
}

View File

@@ -11,7 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import io.mockk.every
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -20,15 +20,13 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Delete
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.actions.RecyclerViewScrollToBottomAction
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
@@ -118,32 +116,28 @@ class CheckoutFlowActivityTest__RecurringDonations {
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
AppDependencies.donationsApi.apply {
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
},
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
Thread.sleep(10000)
MockResponse().success()
}
)
)
every { deleteSubscription(subscriber.subscriberId) } returns NetworkResult.Success(Unit)
}
}
private fun initialisePendingSubscription() {
@@ -160,27 +154,25 @@ class CheckoutFlowActivityTest__RecurringDonations {
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"incomplete",
"STRIPE",
"CARD",
false
),
null
)
AppDependencies.donationsApi.apply {
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"incomplete",
"STRIPE",
"CARD",
false
),
null
)
}
)
)
}
}
}

View File

@@ -26,10 +26,13 @@ import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.archive.ArchiveApi
import org.whispersystems.signalservice.api.attachment.AttachmentApi
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.message.MessageApi
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
@@ -122,6 +125,14 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
return recipientCache
}
override fun provideArchiveApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ArchiveApi {
return mockk()
}
override fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi {
return mockk()
}
override fun provideSignalServiceMessageSender(
protocolStore: SignalServiceDataStore,
pushServiceSocket: PushServiceSocket,

View File

@@ -6,11 +6,13 @@
package org.thoughtcrime.securesms.testing
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import io.mockk.every
import org.junit.rules.ExternalResource
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Locale
/**
* Sets up some common infrastructure for on-device InAppPayment testing
@@ -23,29 +25,25 @@ class InAppPaymentsRule : ExternalResource() {
}
private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
val response = assets.open("inAppPaymentsTests/configuration.json").use { stream ->
NetworkResult.Success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
AppDependencies.donationsApi.apply {
every { getDonationsConfiguration(any()) } returns response
}
}
private fun initialisePutSubscription() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/subscription/") {
MockResponse().success()
}
)
AppDependencies.donationsApi.apply {
every { putSubscription(any()) } returns NetworkResult.Success(Unit)
}
}
private fun initialiseSetArchiveBackupId() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/archives/backupid") {
MockResponse().success()
}
)
AppDependencies.archiveApi.apply {
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
}
}
}

View File

@@ -105,9 +105,13 @@ class DeleteAccountRepository {
try {
AppDependencies.getSignalServiceAccountManager().deleteAccount();
} catch (IOException e) {
Log.w(TAG, "deleteAccount: failed to delete account from signal service", e);
onDeleteAccountEvent.accept(DeleteAccountEvent.ServerDeletionFailed.INSTANCE);
return;
if (e instanceof NonSuccessfulResponseCodeException && ((NonSuccessfulResponseCodeException) e).code == 4401) {
Log.i(TAG, "deleteAccount: WebSocket closed with expected status after delete account, moving forward as delete was successful");
} else {
Log.w(TAG, "deleteAccount: failed to delete account from signal service, bail", e);
onDeleteAccountEvent.accept(DeleteAccountEvent.ServerDeletionFailed.INSTANCE);
return;
}
}
Log.i(TAG, "deleteAccount: successfully removed account from server");

View File

@@ -48,6 +48,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
import org.whispersystems.signalservice.api.calling.CallingApi
import org.whispersystems.signalservice.api.cds.CdsApi
import org.whispersystems.signalservice.api.certificate.CertificateApi
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.link.LinkDeviceApi
@@ -350,6 +351,9 @@ object AppDependencies {
val remoteConfigApi: RemoteConfigApi
get() = networkModule.remoteConfigApi
val donationsApi: DonationsApi
get() = networkModule.donationsApi
@JvmStatic
val okHttpClient: OkHttpClient
get() = networkModule.okHttpClient
@@ -405,7 +409,7 @@ object AppDependencies {
fun provideGiphyMp4Cache(): GiphyMp4Cache
fun provideExoPlayerPool(): SimpleExoPlayerPool
fun provideAndroidCallAudioManager(): AudioManagerCompat
fun provideDonationsService(pushServiceSocket: PushServiceSocket): DonationsService
fun provideDonationsService(donationsApi: DonationsApi): DonationsService
fun provideProfileService(profileOperations: ClientZkProfileOperations, authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): ProfileService
fun provideDeadlockDetector(): DeadlockDetector
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
@@ -431,5 +435,6 @@ object AppDependencies {
fun provideCertificateApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): CertificateApi
fun provideProfileApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ProfileApi
fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): RemoteConfigApi
fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi
}
}

View File

@@ -87,6 +87,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi;
import org.whispersystems.signalservice.api.calling.CallingApi;
import org.whispersystems.signalservice.api.cds.CdsApi;
import org.whispersystems.signalservice.api.certificate.CertificateApi;
import org.whispersystems.signalservice.api.donations.DonationsApi;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.keys.KeysApi;
@@ -140,7 +141,6 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
return new PushServiceSocket(signalServiceConfiguration,
new DynamicCredentialsProvider(),
BuildConfig.SIGNAL_AGENT,
groupsV2Operations.getProfileOperations(),
RemoteConfig.okHttpAutomaticRetry());
}
@@ -444,8 +444,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
}
@Override
public @NonNull DonationsService provideDonationsService(@NonNull PushServiceSocket pushServiceSocket) {
return new DonationsService(pushServiceSocket);
public @NonNull DonationsService provideDonationsService(@NonNull DonationsApi donationsApi) {
return new DonationsService(donationsApi);
}
@Override
@@ -558,6 +558,11 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
return new RemoteConfigApi(authWebSocket);
}
@Override
public @NonNull DonationsApi provideDonationsApi(@NonNull SignalWebSocket.AuthenticatedWebSocket authWebSocket, @NonNull SignalWebSocket.UnauthenticatedWebSocket unauthWebSocket) {
return new DonationsApi(authWebSocket, unauthWebSocket);
}
@VisibleForTesting
static class DynamicCredentialsProvider implements CredentialsProvider {

View File

@@ -33,6 +33,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
import org.whispersystems.signalservice.api.calling.CallingApi
import org.whispersystems.signalservice.api.cds.CdsApi
import org.whispersystems.signalservice.api.certificate.CertificateApi
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.link.LinkDeviceApi
@@ -142,7 +143,7 @@ class NetworkDependenciesModule(
}
val donationsService: DonationsService by lazy {
provider.provideDonationsService(pushServiceSocket)
provider.provideDonationsService(donationsApi)
}
val archiveApi: ArchiveApi by lazy {
@@ -213,6 +214,10 @@ class NetworkDependenciesModule(
provider.provideRemoteConfigApi(authWebSocket)
}
val donationsApi: DonationsApi by lazy {
provider.provideDonationsApi(authWebSocket, unauthWebSocket)
}
val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(StandardUserAgentInterceptor())

View File

@@ -42,6 +42,7 @@ import org.whispersystems.signalservice.api.attachment.AttachmentApi
import org.whispersystems.signalservice.api.calling.CallingApi
import org.whispersystems.signalservice.api.cds.CdsApi
import org.whispersystems.signalservice.api.certificate.CertificateApi
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.link.LinkDeviceApi
@@ -188,7 +189,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
return mockk(relaxed = true)
}
override fun provideDonationsService(pushServiceSocket: PushServiceSocket): DonationsService {
override fun provideDonationsService(donationsApi: DonationsApi): DonationsService {
return mockk(relaxed = true)
}
@@ -295,4 +296,8 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
override fun provideRemoteConfigApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket): RemoteConfigApi {
return mockk(relaxed = true)
}
override fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi {
return mockk(relaxed = true)
}
}

View File

@@ -17,6 +17,8 @@ import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse
import org.whispersystems.signalservice.internal.push.SendMessageResponse
import org.whispersystems.signalservice.internal.push.exceptions.GroupMismatchedDevicesException
import org.whispersystems.signalservice.internal.push.exceptions.GroupStaleDevicesException
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentProcessorError
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentReceiptCredentialError
import org.whispersystems.signalservice.internal.push.exceptions.InvalidUnidentifiedAccessHeaderException
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
import org.whispersystems.signalservice.internal.push.exceptions.PaymentsRegionException
@@ -30,6 +32,26 @@ import kotlin.time.Duration.Companion.seconds
*/
object NetworkResultUtil {
/**
* Unwraps [NetworkResult] to a basic [IOException] or [NonSuccessfulResponseCodeException]. Should only
* be used when you don't need a specific flavor of IOException for a specific response of any kind.
*/
@JvmStatic
@Throws(IOException::class)
fun <T> successOrThrow(result: NetworkResult<T>): T {
return when (result) {
is NetworkResult.Success -> result.result
is NetworkResult.ApplicationError -> {
throw when (val error = result.throwable) {
is IOException, is RuntimeException -> error
else -> RuntimeException(error)
}
}
is NetworkResult.NetworkError -> throw result.exception
is NetworkResult.StatusCodeError -> throw result.exception
}
}
/**
* Convert to a basic [IOException] or [NonSuccessfulResponseCodeException]. Should only be used when you don't
* need a specific flavor of IOException for a specific response code.
@@ -182,4 +204,31 @@ object NetworkResultUtil {
}
}
}
/**
* Convert a [NetworkResult] into typed exceptions expected during calls with IAP endpoints. Not all endpoints require
* specific error parsing but if those errors do happen for them they'll fail to parse and get the normal status code
* exception.
*/
@JvmStatic
@Throws(IOException::class)
fun <T> toIAPBasicLegacy(result: NetworkResult<T>): T {
return when (result) {
is NetworkResult.Success -> result.result
is NetworkResult.ApplicationError -> {
throw when (val error = result.throwable) {
is IOException, is RuntimeException -> error
else -> RuntimeException(error)
}
}
is NetworkResult.NetworkError -> throw result.exception
is NetworkResult.StatusCodeError -> {
throw when (result.code) {
402 -> result.parseJsonBody<InAppPaymentReceiptCredentialError>() ?: result.exception
440 -> result.parseJsonBody<InAppPaymentProcessorError>() ?: result.exception
else -> result.exception
}
}
}
}
}

View File

@@ -68,7 +68,7 @@ public class SignalServiceAccountManager {
return new SignalServiceAccountManager(
null,
null,
new PushServiceSocket(configuration, credentialProvider, signalAgent, gv2Operations.getProfileOperations(), automaticNetworkRetry),
new PushServiceSocket(configuration, credentialProvider, signalAgent, automaticNetworkRetry),
gv2Operations
);
}

View File

@@ -97,6 +97,7 @@ class AccountApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSock
/**
* DELETE /v1/accounts/me
* - 204: Success
* - 4401: Success
*/
fun deleteAccount(): NetworkResult<Unit> {
val request = WebSocketRequestMessage.delete("/v1/accounts/me")

View File

@@ -1,9 +1,15 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
import org.signal.core.util.Base64;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
class BoostReceiptCredentialRequestJson {
@JsonProperty("paymentIntentId")

View File

@@ -0,0 +1,320 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations
import org.signal.core.util.Base64
import org.signal.core.util.urlEncode
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.push.exceptions.MalformedResponseException
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.delete
import org.whispersystems.signalservice.internal.get
import org.whispersystems.signalservice.internal.post
import org.whispersystems.signalservice.internal.push.BankMandate
import org.whispersystems.signalservice.internal.push.DonationProcessor
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.put
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import java.util.Locale
/**
* One-stop shop for Signal service calls related to in-app payments.
*/
class DonationsApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSocket, private val unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket) {
/**
* Get configuration data associated with donations, like gift, one-time, and monthly levels, etc.
*
* GET /v1/subscription/configuration
* - 200: Success
*/
fun getDonationsConfiguration(locale: Locale): NetworkResult<SubscriptionsConfiguration> {
val request = WebSocketRequestMessage.get("/v1/subscription/configuration", locale.toAcceptLanguageHeaders())
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, SubscriptionsConfiguration::class)
}
/**
* Get localized bank mandate text for the given [bankTransferType].
*
* GET /v1/subscription/bank_mandate/[bankTransferType]
* - 200: Success
*
* @param bankTransferType Valid values for bankTransferType are 'SEPA_DEBIT'.
*/
fun getBankMandate(locale: Locale, bankTransferType: String): NetworkResult<BankMandate> {
val request = WebSocketRequestMessage.get("/v1/subscription/bank_mandate/$bankTransferType", locale.toAcceptLanguageHeaders())
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, BankMandate::class)
}
/**
* GET /v1/subscription/[subscriberId]
* - 200: Success
* - 403: Invalid or malformed [subscriberId]
* - 404: [subscriberId] not found
*/
fun getSubscription(subscriberId: SubscriberId): NetworkResult<ActiveSubscription> {
val request = WebSocketRequestMessage.get("/v1/subscription/${subscriberId.serialize()}")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, ActiveSubscription::class)
}
/**
* Creates a subscriber record on the signal server.
*
* PUT /v1/subscription/[subscriberId]
* - 200: Success
* - 403, 404: Invalid or malformed [subscriberId]
*/
fun putSubscription(subscriberId: SubscriberId): NetworkResult<Unit> {
val request = WebSocketRequestMessage.put("/v1/subscription/${subscriberId.serialize()}", body = "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* DELETE /v1/subscription/[subscriberId]
* - 204: Success
* - 403: Invalid or malformed [subscriberId]
* - 404: [subscriberId] not found
*/
fun deleteSubscription(subscriberId: SubscriberId): NetworkResult<Unit> {
val request = WebSocketRequestMessage.delete("/v1/subscription/${subscriberId.serialize()}")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* Updates the current subscription to the given level and currency.
* - 200: Success
*/
fun updateSubscriptionLevel(subscriberId: SubscriberId, level: String, currencyCode: String, idempotencyKey: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.put("/v1/subscription/${subscriberId.serialize()}/level/${level.urlEncode()}/${currencyCode.urlEncode()}/$idempotencyKey", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* Submits price information to the server to generate a payment intent via the payment gateway.
*
* POST /v1/subscription/boost/create
*
* @param amount Price, in the minimum currency unit (e.g. cents or yen)
* @param currencyCode The currency code for the amount
*/
fun createStripeOneTimePaymentIntent(currencyCode: String, paymentMethod: String, amount: Long, level: Long): NetworkResult<StripeClientSecret> {
val body = StripeOneTimePaymentIntentPayload(amount, currencyCode, level, paymentMethod)
val request = WebSocketRequestMessage.post("/v1/subscription/boost/create", body)
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, StripeClientSecret::class)
}
/**
* PUT /v1/subscription/[subscriberId]/create_payment_method?type=[type]
* - 200: Success
*
* @param type One of CARD or SEPA_DEBIT
*/
fun createStripeSubscriptionPaymentMethod(subscriberId: SubscriberId, type: String): NetworkResult<StripeClientSecret> {
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/create_payment_method?type=${type.urlEncode()}", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, StripeClientSecret::class)
}
/**
* Creates a PayPal one-time payment and returns the approval URL.
*
* POST /v1/subscription/boost/paypal/create
* - 200: Success
* - 400: Request error
* - 409: Level requires a valid currency/amount combination that does not match
*
* @param locale User locale for proper language presentation
* @param currencyCode 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param returnUrl The 'return' url after a successful login and confirmation
* @param cancelUrl The 'cancel' url for a cancelled confirmation
*/
fun createPayPalOneTimePaymentIntent(
locale: Locale,
currencyCode: String,
amount: Long,
level: Long,
returnUrl: String,
cancelUrl: String
): NetworkResult<PayPalCreatePaymentIntentResponse> {
val body = PayPalCreateOneTimePaymentIntentPayload(amount, currencyCode, level, returnUrl, cancelUrl)
val request = WebSocketRequestMessage.post("/v1/subscription/boost/paypal/create", body, locale.toAcceptLanguageHeaders())
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, PayPalCreatePaymentIntentResponse::class)
}
/**
* Confirms a PayPal one-time payment and returns the paymentId for receipt credentials
*
* POST /v1/subscription/boost/paypal/confirm
* - 200: Success
* - 400: Request error
* - 409: Level requires a valid currency/amount combination that does not match
*
* @param currency 3 letter currency code of the desired currency
* @param amount Stringified minimum precision amount
* @param level The badge level to purchase
* @param payerId Passed as a URL parameter back to returnUrl
* @param paymentId Passed as a URL parameter back to returnUrl
* @param paymentToken Passed as a URL parameter back to returnUrl
*/
fun confirmPayPalOneTimePaymentIntent(
currency: String,
amount: String,
level: Long,
payerId: String,
paymentId: String,
paymentToken: String
): NetworkResult<PayPalConfirmPaymentIntentResponse> {
val body = PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken)
val request = WebSocketRequestMessage.post("/v1/subscription/boost/paypal/confirm", body)
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, PayPalConfirmPaymentIntentResponse::class)
}
/**
* Sets up a payment method via PayPal for recurring charges.
*
* POST /v1/subscription/[subscriberId]/create_payment_method/paypal
* - 200: success
* - 403: subscriberId password mismatches OR account authentication is present
* - 404: subscriberId is not found or malformed
*
* @param locale User locale
* @param subscriberId User subscriber id
* @param returnUrl A success URL
* @param cancelUrl A cancel URL
* @return A response with an approval url and token
*/
fun createPayPalPaymentMethod(
locale: Locale,
subscriberId: SubscriberId,
returnUrl: String,
cancelUrl: String
): NetworkResult<PayPalCreatePaymentMethodResponse> {
val body = PayPalCreatePaymentMethodPayload(returnUrl, cancelUrl)
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/create_payment_method/paypal", body, locale.toAcceptLanguageHeaders())
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, PayPalCreatePaymentMethodResponse::class)
}
/**
* POST /v1/subscription/[subscriberId]/receipt_credentials
*/
fun submitReceiptCredentials(subscriberId: SubscriberId, receiptCredentialRequest: ReceiptCredentialRequest): NetworkResult<ReceiptCredentialResponse> {
val body = ReceiptCredentialRequestJson(receiptCredentialRequest)
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/receipt_credentials", body)
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, webSocketResponseConverter = NetworkResult.LongPollingWebSocketConverter(ReceiptCredentialResponseJson::class))
.map { it.receiptCredentialResponse }
.then {
if (it != null) {
NetworkResult.Success(it)
} else {
NetworkResult.NetworkError(MalformedResponseException("Unable to parse response"))
}
}
}
/**
* Given a completed payment intent and a receipt credential request produces a receipt credential response. Clients
* should always use the same ReceiptCredentialRequest with the same payment intent id. This request is repeatable so
* long as the two values are reused.
*
* POST /v1/subscription/boost/receipt_credentials
*
* @param paymentIntentId PaymentIntent ID from a boost donation intent response.
* @param receiptCredentialRequest Client-generated request token
*/
fun submitBoostReceiptCredentials(paymentIntentId: String, receiptCredentialRequest: ReceiptCredentialRequest, processor: DonationProcessor): NetworkResult<ReceiptCredentialResponse> {
val body = BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor)
val request = WebSocketRequestMessage.post("/v1/subscription/boost/receipt_credentials", body)
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request, webSocketResponseConverter = NetworkResult.LongPollingWebSocketConverter(ReceiptCredentialResponseJson::class))
.map { it.receiptCredentialResponse }
.then {
if (it != null) {
NetworkResult.Success(it)
} else {
NetworkResult.NetworkError(MalformedResponseException("Unable to parse response"))
}
}
}
/**
* POST /v1/subscription/[subscriberId]/default_payment_method/stripe/[paymentMethodId]
* - 200: Success
*/
fun setDefaultStripeSubscriptionPaymentMethod(subscriberId: SubscriberId, paymentMethodId: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/default_payment_method/stripe/${paymentMethodId.urlEncode()}", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* POST /v1/subscription/[subscriberId]/default_payment_method_for_ideal/[setupIntentId]
* - 200: Success
*/
fun setDefaultIdealSubscriptionPaymentMethod(subscriberId: SubscriberId, setupIntentId: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/default_payment_method_for_ideal/${setupIntentId.urlEncode()}", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* POST /v1/subscription/[subscriberId]/default_payment_method/braintree/[paymentMethodId]
* - 200: Success
*/
fun setDefaultPaypalSubscriptionPaymentMethod(subscriberId: SubscriberId, paymentMethodId: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/default_payment_method/braintree/${paymentMethodId.urlEncode()}", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* POST /v1/subscription/[subscriberId]/playbilling/[purchaseToken]
* - 200: Success
*/
fun linkPlayBillingPurchaseToken(subscriberId: SubscriberId, purchaseToken: String): NetworkResult<Unit> {
val request = WebSocketRequestMessage.post("/v1/subscription/${subscriberId.serialize()}/playbilling/${purchaseToken.urlEncode()}", "")
return NetworkResult.fromWebSocketRequest(unauthWebSocket, request)
}
/**
* Allows a user to redeem a given receipt they were given after submitting a donation successfully.
*
* POST /v1/donation/redeem-receipt
* - 200: Success
*
* @param receiptCredentialPresentation Receipt
* @param visible Whether the badge will be visible on the user's profile immediately after redemption
* @param primary Whether the badge will be made primary immediately after redemption
*/
fun redeemDonationReceipt(receiptCredentialPresentation: ReceiptCredentialPresentation, visible: Boolean, primary: Boolean): NetworkResult<Unit> {
val body = RedeemDonationReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize()), visible, primary)
val request = WebSocketRequestMessage.post("/v1/donation/redeem-receipt", body)
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
}
/**
* Allows a user to redeem a given receipt they were given after submitting a donation successfully.
*
* POST /v1/archives/redeem-receipt
* - 200: Success
*
* @param receiptCredentialPresentation Receipt
*/
fun redeemArchivesReceipt(receiptCredentialPresentation: ReceiptCredentialPresentation): NetworkResult<Unit> {
val body = RedeemArchivesReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize()))
val request = WebSocketRequestMessage.post("/v1/archives/redeem-receipt", body)
return NetworkResult.fromWebSocketRequest(authWebSocket, request)
}
private fun Locale.toAcceptLanguageHeaders(): Map<String, String> {
return mapOf("Accept-Language" to "${this.language}${if (this.country.isNotEmpty()) "-${this.country}" else ""}")
}
}

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -8,6 +13,8 @@ import org.signal.core.util.Base64;
import java.io.IOException;
import javax.annotation.Nullable;
class ReceiptCredentialResponseJson {
private final ReceiptCredentialResponse receiptCredentialResponse;
@@ -23,7 +30,7 @@ class ReceiptCredentialResponseJson {
this.receiptCredentialResponse = response;
}
public ReceiptCredentialResponse getReceiptCredentialResponse() {
public @Nullable ReceiptCredentialResponse getReceiptCredentialResponse() {
return receiptCredentialResponse;
}
}

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -1,4 +1,9 @@
package org.whispersystems.signalservice.internal.push;
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.signalservice.api.donations;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -14,6 +14,7 @@ import org.whispersystems.signalservice.internal.push.OutgoingPushMessageList
import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse
import org.whispersystems.signalservice.internal.push.SendMessageResponse
import org.whispersystems.signalservice.internal.put
import org.whispersystems.signalservice.internal.putCustom
import org.whispersystems.signalservice.internal.websocket.WebSocketRequestMessage
import org.whispersystems.signalservice.internal.websocket.WebsocketResponse
@@ -80,7 +81,7 @@ class MessageApi(
* - 410: Stale devices supplied for some recipients
*/
fun sendGroupMessage(body: ByteArray, sealedSenderAccess: SealedSenderAccess, timestamp: Long, online: Boolean, urgent: Boolean, story: Boolean): NetworkResult<SendGroupMessageResponse> {
val request = WebSocketRequestMessage.put(
val request = WebSocketRequestMessage.putCustom(
path = "/v1/messages/multi_recipient?ts=$timestamp&online=${online.toQueryParam()}&urgent=${urgent.toQueryParam()}&story=${story.toQueryParam()}",
body = body,
headers = mapOf("content-type" to "application/vnd.signal-messenger.mrm")

View File

@@ -5,7 +5,9 @@ import org.signal.libsignal.protocol.util.Pair;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.NetworkResult;
import org.whispersystems.signalservice.api.NetworkResultUtil;
import org.whispersystems.signalservice.api.donations.DonationsApi;
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
@@ -13,14 +15,11 @@ import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentInt
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.subscriptions.SubscriberId;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.internal.EmptyResponse;
import org.whispersystems.signalservice.internal.ServiceResponse;
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
import org.whispersystems.signalservice.internal.push.BankMandate;
import org.whispersystems.signalservice.internal.push.DonationProcessor;
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration;
import org.whispersystems.signalservice.internal.push.PushServiceSocket;
import java.io.IOException;
import java.util.Locale;
@@ -41,7 +40,7 @@ public class DonationsService {
private static final String TAG = DonationsService.class.getSimpleName();
private final PushServiceSocket pushServiceSocket;
private final DonationsApi donationsApi;
private final AtomicReference<CacheEntry<SubscriptionsConfiguration>> donationsConfigurationCache = new AtomicReference<>(null);
private final AtomicReference<CacheEntry<BankMandate>> sepaBankMandateCache = new AtomicReference<>(null);
@@ -58,8 +57,8 @@ public class DonationsService {
}
}
public DonationsService(@NonNull PushServiceSocket pushServiceSocket) {
this.pushServiceSocket = pushServiceSocket;
public DonationsService(@NonNull DonationsApi donationsApi) {
this.donationsApi = donationsApi;
}
/**
@@ -71,7 +70,7 @@ public class DonationsService {
*/
public ServiceResponse<EmptyResponse> redeemDonationReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) {
try {
pushServiceSocket.redeemDonationReceipt(receiptCredentialPresentation, visible, primary);
NetworkResultUtil.successOrThrow(donationsApi.redeemDonationReceipt(receiptCredentialPresentation, visible, primary));
return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null);
} catch (Exception e) {
return ServiceResponse.<EmptyResponse>forUnknownError(e);
@@ -85,7 +84,7 @@ public class DonationsService {
*/
public ServiceResponse<EmptyResponse> redeemArchivesReceipt(ReceiptCredentialPresentation receiptCredentialPresentation) {
try {
pushServiceSocket.redeemArchivesReceipt(receiptCredentialPresentation);
NetworkResultUtil.successOrThrow(donationsApi.redeemArchivesReceipt(receiptCredentialPresentation));
return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null);
} catch (Exception e) {
return ServiceResponse.<EmptyResponse>forUnknownError(e);
@@ -100,7 +99,7 @@ public class DonationsService {
* @return A ServiceResponse containing a DonationIntentResult with details given to us by the payment gateway.
*/
public ServiceResponse<StripeClientSecret> createDonationIntentWithAmount(String amount, String currencyCode, long level, String paymentMethod) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.createStripeOneTimePaymentIntent(currencyCode, paymentMethod, Long.parseLong(amount), level), 200));
return wrapInServiceResponse(() -> new Pair<>(NetworkResultUtil.successOrThrow(donationsApi.createStripeOneTimePaymentIntent(currencyCode, paymentMethod, Long.parseLong(amount), level)), 200));
}
/**
@@ -111,14 +110,14 @@ public class DonationsService {
* @param receiptCredentialRequest Client-generated request token
*/
public ServiceResponse<ReceiptCredentialResponse> submitBoostReceiptCredentialRequestSync(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) {
return wrapInServiceResponse(() -> new Pair<>(pushServiceSocket.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest, processor), 200));
return wrapInServiceResponse(() -> new Pair<>(NetworkResultUtil.toIAPBasicLegacy(donationsApi.submitBoostReceiptCredentials(paymentIntentId, receiptCredentialRequest, processor)), 200));
}
public ServiceResponse<SubscriptionsConfiguration> getDonationsConfiguration(Locale locale) {
return getCachedValue(
locale,
donationsConfigurationCache,
pushServiceSocket::getDonationsConfiguration,
l -> NetworkResultUtil.successOrThrow(donationsApi.getDonationsConfiguration(l)),
DONATION_CONFIGURATION_TTL
);
}
@@ -127,7 +126,7 @@ public class DonationsService {
return getCachedValue(
locale,
sepaBankMandateCache,
l -> pushServiceSocket.getBankMandate(l, bankTransferType),
l -> NetworkResultUtil.successOrThrow(donationsApi.getBankMandate(l, bankTransferType)),
SEPA_DEBIT_MANDATE_TTL
);
}
@@ -181,7 +180,7 @@ public class DonationsService {
lock.lock();
try {
pushServiceSocket.updateSubscriptionLevel(subscriberId.serialize(), level, currencyCode, idempotencyKey);
NetworkResultUtil.toIAPBasicLegacy(donationsApi.updateSubscriptionLevel(subscriberId, level, currencyCode, idempotencyKey));
} finally {
lock.unlock();
}
@@ -194,7 +193,7 @@ public class DonationsService {
return wrapInServiceResponse(() -> {
lock.lock();
try {
pushServiceSocket.linkPlayBillingPurchaseToken(subscriberId.serialize(), purchaseToken);
NetworkResultUtil.successOrThrow(donationsApi.linkPlayBillingPurchaseToken(subscriberId, purchaseToken));
} finally {
lock.unlock();
}
@@ -208,7 +207,7 @@ public class DonationsService {
*/
public ServiceResponse<ActiveSubscription> getSubscription(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> {
ActiveSubscription response = pushServiceSocket.getSubscription(subscriberId.serialize());
ActiveSubscription response = NetworkResultUtil.successOrThrow(donationsApi.getSubscription(subscriberId));
return new Pair<>(response, 200);
});
}
@@ -225,7 +224,7 @@ public class DonationsService {
*/
public ServiceResponse<EmptyResponse> putSubscription(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.putSubscription(subscriberId.serialize());
NetworkResultUtil.successOrThrow(donationsApi.putSubscription(subscriberId));
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
@@ -237,21 +236,21 @@ public class DonationsService {
*/
public ServiceResponse<EmptyResponse> cancelSubscription(SubscriberId subscriberId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.deleteSubscription(subscriberId.serialize());
NetworkResultUtil.successOrThrow(donationsApi.deleteSubscription(subscriberId));
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
public ServiceResponse<EmptyResponse> setDefaultStripePaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultStripeSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
NetworkResultUtil.successOrThrow(donationsApi.setDefaultStripeSubscriptionPaymentMethod(subscriberId, paymentMethodId));
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
public ServiceResponse<EmptyResponse> setDefaultIdealPaymentMethod(SubscriberId subscriberId, String setupIntentId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultIdealSubscriptionPaymentMethod(subscriberId.serialize(), setupIntentId);
NetworkResultUtil.successOrThrow(donationsApi.setDefaultIdealSubscriptionPaymentMethod(subscriberId, setupIntentId));
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
@@ -263,7 +262,7 @@ public class DonationsService {
*/
public ServiceResponse<StripeClientSecret> createStripeSubscriptionPaymentMethod(SubscriberId subscriberId, String type) {
return wrapInServiceResponse(() -> {
StripeClientSecret clientSecret = pushServiceSocket.createStripeSubscriptionPaymentMethod(subscriberId.serialize(), type);
StripeClientSecret clientSecret = NetworkResultUtil.successOrThrow(donationsApi.createStripeSubscriptionPaymentMethod(subscriberId, type));
return new Pair<>(clientSecret, 200);
});
}
@@ -291,7 +290,7 @@ public class DonationsService {
String cancelUrl)
{
return wrapInServiceResponse(() -> {
PayPalCreatePaymentIntentResponse response = pushServiceSocket.createPayPalOneTimePaymentIntent(
NetworkResult<PayPalCreatePaymentIntentResponse> response = donationsApi.createPayPalOneTimePaymentIntent(
locale,
currencyCode.toUpperCase(Locale.US), // Chris Eager to make this case insensitive in the next build
Long.parseLong(amount),
@@ -299,7 +298,7 @@ public class DonationsService {
returnUrl,
cancelUrl
);
return new Pair<>(response, 200);
return new Pair<>(NetworkResultUtil.successOrThrow(response), 200);
});
}
@@ -326,7 +325,7 @@ public class DonationsService {
String paymentToken)
{
return wrapInServiceResponse(() -> {
PayPalConfirmPaymentIntentResponse response = pushServiceSocket.confirmPayPalOneTimePaymentIntent(currency, amount, level, payerId, paymentId, paymentToken);
PayPalConfirmPaymentIntentResponse response = NetworkResultUtil.toIAPBasicLegacy(donationsApi.confirmPayPalOneTimePaymentIntent(currency, amount, level, payerId, paymentId, paymentToken));
return new Pair<>(response, 200);
});
}
@@ -351,7 +350,7 @@ public class DonationsService {
String cancelUrl)
{
return wrapInServiceResponse(() -> {
PayPalCreatePaymentMethodResponse response = pushServiceSocket.createPayPalPaymentMethod(locale, subscriberId.serialize(), returnUrl, cancelUrl);
PayPalCreatePaymentMethodResponse response = NetworkResultUtil.successOrThrow(donationsApi.createPayPalPaymentMethod(locale, subscriberId, returnUrl, cancelUrl));
return new Pair<>(response, 200);
});
}
@@ -370,14 +369,14 @@ public class DonationsService {
*/
public ServiceResponse<EmptyResponse> setDefaultPayPalPaymentMethod(SubscriberId subscriberId, String paymentMethodId) {
return wrapInServiceResponse(() -> {
pushServiceSocket.setDefaultPaypalSubscriptionPaymentMethod(subscriberId.serialize(), paymentMethodId);
NetworkResultUtil.successOrThrow(donationsApi.setDefaultPaypalSubscriptionPaymentMethod(subscriberId, paymentMethodId));
return new Pair<>(EmptyResponse.INSTANCE, 200);
});
}
public ServiceResponse<ReceiptCredentialResponse> submitReceiptCredentialRequestSync(SubscriberId subscriberId, ReceiptCredentialRequest receiptCredentialRequest) {
return wrapInServiceResponse(() -> {
ReceiptCredentialResponse response = pushServiceSocket.submitReceiptCredentials(subscriberId.serialize(), receiptCredentialRequest);
ReceiptCredentialResponse response = NetworkResultUtil.successOrThrow(donationsApi.submitReceiptCredentials(subscriberId, receiptCredentialRequest));
return new Pair<>(response, 200);
});
}

View File

@@ -55,7 +55,10 @@ fun WebSocketRequestMessage.Companion.put(path: String, body: Any, headers: Map<
verb = "PUT",
path = path,
headers = listOf("content-type:application/json") + headers.toHeaderList(),
body = JsonUtil.toJsonByteString(body),
body = when (body) {
is String -> body.toByteArray().toByteString()
else -> JsonUtil.toJsonByteString(body)
},
id = SecureRandom().nextLong()
)
}
@@ -63,7 +66,7 @@ fun WebSocketRequestMessage.Companion.put(path: String, body: Any, headers: Map<
/**
* Create a custom PUT web socket request, where body and content type header are provided by caller.
*/
fun WebSocketRequestMessage.Companion.put(path: String, body: ByteArray, headers: Map<String, String>): WebSocketRequestMessage {
fun WebSocketRequestMessage.Companion.putCustom(path: String, body: ByteArray, headers: Map<String, String>): WebSocketRequestMessage {
return WebSocketRequestMessage(
verb = "PUT",
path = path,

View File

@@ -14,10 +14,6 @@ import org.signal.core.util.Base64;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.protocol.util.Pair;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialRequest;
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialResponse;
import org.signal.storageservice.protos.groups.AvatarUploadAttributes;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
@@ -67,11 +63,6 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept
import org.whispersystems.signalservice.api.push.exceptions.SubmitVerificationCodeRateLimitException;
import org.whispersystems.signalservice.api.push.exceptions.TokenNotAcceptedException;
import org.whispersystems.signalservice.api.registration.RestoreMethodBody;
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription;
import org.whispersystems.signalservice.api.subscriptions.PayPalConfirmPaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentIntentResponse;
import org.whispersystems.signalservice.api.subscriptions.PayPalCreatePaymentMethodResponse;
import org.whispersystems.signalservice.api.subscriptions.StripeClientSecret;
import org.whispersystems.signalservice.api.svr.Svr3Credentials;
import org.whispersystems.signalservice.api.util.CredentialsProvider;
import org.whispersystems.signalservice.api.util.Tls12SocketFactory;
@@ -85,8 +76,6 @@ import org.whispersystems.signalservice.internal.push.exceptions.ForbiddenExcept
import org.whispersystems.signalservice.internal.push.exceptions.GroupExistsException;
import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException;
import org.whispersystems.signalservice.internal.push.exceptions.GroupPatchNotAcceptedException;
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentProcessorError;
import org.whispersystems.signalservice.internal.push.exceptions.InAppPaymentReceiptCredentialError;
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
import org.whispersystems.signalservice.internal.push.exceptions.StaleDevicesException;
@@ -179,25 +168,6 @@ public class PushServiceSocket {
private static final String GROUPSV2_TOKEN = "/v2/groups/token";
private static final String GROUPSV2_JOINED_AT = "/v2/groups/joined_at_version";
private static final String DONATION_REDEEM_RECEIPT = "/v1/donation/redeem-receipt";
private static final String ARCHIVES_REDEEM_RECEIPT = "/v1/archives/redeem-receipt";
private static final String UPDATE_SUBSCRIPTION_LEVEL = "/v1/subscription/%s/level/%s/%s/%s";
private static final String SUBSCRIPTION = "/v1/subscription/%s";
private static final String CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method?type=%s";
private static final String CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/create_payment_method/paypal";
private static final String DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/stripe/%s";
private static final String DEFAULT_IDEAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method_for_ideal/%s";
private static final String DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD = "/v1/subscription/%s/default_payment_method/braintree/%s";
private static final String SUBSCRIPTION_RECEIPT_CREDENTIALS = "/v1/subscription/%s/receipt_credentials";
private static final String CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/create";
private static final String CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/create";
private static final String CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT = "/v1/subscription/boost/paypal/confirm";
private static final String BOOST_RECEIPT_CREDENTIALS = "/v1/subscription/boost/receipt_credentials";
private static final String DONATIONS_CONFIGURATION = "/v1/subscription/configuration";
private static final String BANK_MANDATE = "/v1/subscription/bank_mandate/%s";
private static final String LINK_PLAY_BILLING_PURCHASE_TOKEN = "/v1/subscription/%s/playbilling/%s";
private static final String VERIFICATION_SESSION_PATH = "/v1/verification/session";
private static final String VERIFICATION_CODE_PATH = "/v1/verification/session/%s/code";
@@ -228,13 +198,11 @@ public class PushServiceSocket {
private final CredentialsProvider credentialsProvider;
private final String signalAgent;
private final SecureRandom random;
private final ClientZkProfileOperations clientZkProfileOperations;
private final boolean automaticNetworkRetry;
public PushServiceSocket(SignalServiceConfiguration configuration,
CredentialsProvider credentialsProvider,
String signalAgent,
ClientZkProfileOperations clientZkProfileOperations,
boolean automaticNetworkRetry)
{
this.configuration = configuration;
@@ -245,7 +213,6 @@ public class PushServiceSocket {
this.cdnClientsMap = createCdnClientsMap(configuration.getSignalCdnUrlMap(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
this.storageClients = createConnectionHolders(configuration.getSignalStorageUrls(), configuration.getNetworkInterceptors(), configuration.getDns(), configuration.getSignalProxy());
this.random = new SecureRandom();
this.clientZkProfileOperations = clientZkProfileOperations;
}
public SignalServiceConfiguration getConfiguration() {
@@ -466,153 +433,6 @@ public class PushServiceSocket {
makeServiceRequest(DELETE_ACCOUNT_PATH, "DELETE", null);
}
public void redeemDonationReceipt(ReceiptCredentialPresentation receiptCredentialPresentation, boolean visible, boolean primary) throws IOException {
String payload = JsonUtil.toJson(new RedeemDonationReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize()), visible, primary));
makeServiceRequest(DONATION_REDEEM_RECEIPT, "POST", payload);
}
public void redeemArchivesReceipt(ReceiptCredentialPresentation receiptCredentialPresentation) throws IOException {
String payload = JsonUtil.toJson(new RedeemArchivesReceiptRequest(Base64.encodeWithPadding(receiptCredentialPresentation.serialize())));
makeServiceRequest(ARCHIVES_REDEEM_RECEIPT, "POST", payload);
}
public StripeClientSecret createStripeOneTimePaymentIntent(String currencyCode, String paymentMethod, long amount, long level) throws IOException {
String payload = JsonUtil.toJson(new StripeOneTimePaymentIntentPayload(amount, currencyCode, level, paymentMethod));
String result = makeServiceRequestWithoutAuthentication(CREATE_STRIPE_ONE_TIME_PAYMENT_INTENT, "POST", payload);
return JsonUtil.fromJsonResponse(result, StripeClientSecret.class);
}
public PayPalCreatePaymentIntentResponse createPayPalOneTimePaymentIntent(Locale locale, String currencyCode, long amount, long level, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreateOneTimePaymentIntentPayload(amount, currencyCode, level, returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(CREATE_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, headers, NO_HANDLER);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentIntentResponse.class);
}
public PayPalConfirmPaymentIntentResponse confirmPayPalOneTimePaymentIntent(String currency, String amount, long level, String payerId, String paymentId, String paymentToken) throws IOException {
String payload = JsonUtil.toJson(new PayPalConfirmOneTimePaymentIntentPayload(amount, currency, level, payerId, paymentId, paymentToken));
String result = makeServiceRequestWithoutAuthentication(CONFIRM_PAYPAL_ONE_TIME_PAYMENT_INTENT, "POST", payload, NO_HEADERS, new InAppPaymentResponseCodeHandler());
return JsonUtil.fromJsonResponse(result, PayPalConfirmPaymentIntentResponse.class);
}
public PayPalCreatePaymentMethodResponse createPayPalPaymentMethod(Locale locale, String subscriberId, String returnUrl, String cancelUrl) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String payload = JsonUtil.toJson(new PayPalCreatePaymentMethodPayload(returnUrl, cancelUrl));
String result = makeServiceRequestWithoutAuthentication(String.format(CREATE_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId), "POST", payload, headers, NO_HANDLER);
return JsonUtil.fromJsonResponse(result, PayPalCreatePaymentMethodResponse.class);
}
public ReceiptCredentialResponse submitBoostReceiptCredentials(String paymentIntentId, ReceiptCredentialRequest receiptCredentialRequest, DonationProcessor processor) throws IOException {
String payload = JsonUtil.toJson(new BoostReceiptCredentialRequestJson(paymentIntentId, receiptCredentialRequest, processor));
String response = makeServiceRequestWithoutAuthentication(
BOOST_RECEIPT_CREDENTIALS,
"POST",
payload,
NO_HEADERS,
(code, body, getHeader) -> {
if (code == 204) throw new NonSuccessfulResponseCodeException(204);
if (code == 402) {
InAppPaymentReceiptCredentialError inAppPaymentReceiptCredentialError;
try {
inAppPaymentReceiptCredentialError = JsonUtil.fromJson(body.string(), InAppPaymentReceiptCredentialError.class);
} catch (IOException e) {
throw new NonSuccessfulResponseCodeException(402);
}
throw inAppPaymentReceiptCredentialError;
}
});
ReceiptCredentialResponseJson responseJson = JsonUtil.fromJson(response, ReceiptCredentialResponseJson.class);
if (responseJson.getReceiptCredentialResponse() != null) {
return responseJson.getReceiptCredentialResponse();
} else {
throw new MalformedResponseException("Unable to parse response");
}
}
/**
* Get the DonationsConfiguration pointed at by /v1/subscriptions/configuration
*/
public SubscriptionsConfiguration getDonationsConfiguration(Locale locale) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String result = makeServiceRequestWithoutAuthentication(DONATIONS_CONFIGURATION, "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(result, SubscriptionsConfiguration.class);
}
/**
* @param bankTransferType Valid values for bankTransferType are {SEPA_DEBIT}.
* @return localized bank mandate text for the given bankTransferType.
*/
public BankMandate getBankMandate(Locale locale, String bankTransferType) throws IOException {
Map<String, String> headers = Collections.singletonMap("Accept-Language", locale.getLanguage() + "-" + locale.getCountry());
String result = makeServiceRequestWithoutAuthentication(String.format(BANK_MANDATE, urlEncode(bankTransferType)), "GET", null, headers, NO_HANDLER);
return JsonUtil.fromJson(result, BankMandate.class);
}
public void linkPlayBillingPurchaseToken(String subscriberId, String purchaseToken) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(LINK_PLAY_BILLING_PURCHASE_TOKEN, subscriberId, urlEncode(purchaseToken)), "POST", "", NO_HEADERS, new LinkGooglePlayBillingPurchaseTokenResponseCodeHandler());
}
public void updateSubscriptionLevel(String subscriberId, String level, String currencyCode, String idempotencyKey) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(UPDATE_SUBSCRIPTION_LEVEL, subscriberId, urlEncode(level), urlEncode(currencyCode), idempotencyKey), "PUT", "", NO_HEADERS, new InAppPaymentResponseCodeHandler());
}
public ActiveSubscription getSubscription(String subscriberId) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "GET", null);
return JsonUtil.fromJson(response, ActiveSubscription.class);
}
public void putSubscription(String subscriberId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "PUT", "");
}
public void deleteSubscription(String subscriberId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(SUBSCRIPTION, subscriberId), "DELETE", null);
}
/**
* @param type One of CARD or SEPA_DEBIT
*/
public StripeClientSecret createStripeSubscriptionPaymentMethod(String subscriberId, String type) throws IOException {
String response = makeServiceRequestWithoutAuthentication(String.format(CREATE_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, urlEncode(type)), "POST", "");
return JsonUtil.fromJson(response, StripeClientSecret.class);
}
public void setDefaultStripeSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_STRIPE_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, urlEncode(paymentMethodId)), "POST", "");
}
public void setDefaultIdealSubscriptionPaymentMethod(String subscriberId, String setupIntentId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_IDEAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, urlEncode(setupIntentId)), "POST", "");
}
public void setDefaultPaypalSubscriptionPaymentMethod(String subscriberId, String paymentMethodId) throws IOException {
makeServiceRequestWithoutAuthentication(String.format(DEFAULT_PAYPAL_SUBSCRIPTION_PAYMENT_METHOD, subscriberId, urlEncode(paymentMethodId)), "POST", "");
}
public ReceiptCredentialResponse submitReceiptCredentials(String subscriptionId, ReceiptCredentialRequest receiptCredentialRequest) throws IOException {
String payload = JsonUtil.toJson(new ReceiptCredentialRequestJson(receiptCredentialRequest));
String response = makeServiceRequestWithoutAuthentication(
String.format(SUBSCRIPTION_RECEIPT_CREDENTIALS, subscriptionId),
"POST",
payload,
NO_HEADERS,
(code, body, getHeader) -> {
if (code == 204) throw new NonSuccessfulResponseCodeException(204);
});
ReceiptCredentialResponseJson responseJson = JsonUtil.fromJson(response, ReceiptCredentialResponseJson.class);
if (responseJson.getReceiptCredentialResponse() != null) {
return responseJson.getReceiptCredentialResponse();
} else {
throw new MalformedResponseException("Unable to parse response");
}
}
public StorageManifest getStorageManifest(String authToken) throws IOException {
try (Response response = makeStorageRequest(authToken, "/v1/storage/manifest", "GET", null, NO_HANDLER)) {
return StorageManifest.ADAPTER.decode(readBodyBytes(response));
@@ -1203,20 +1023,6 @@ public class PushServiceSocket {
.build();
}
private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
return makeServiceRequestWithoutAuthentication(urlFragment, method, jsonBody, NO_HEADERS, NO_HANDLER);
}
private String makeServiceRequestWithoutAuthentication(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
try (Response response = makeServiceRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, SealedSenderAccess.NONE, true)) {
return readBodyString(response);
}
}
private String makeServiceRequest(String urlFragment, String method, String jsonBody)
throws NonSuccessfulResponseCodeException, PushNetworkException, MalformedResponseException
{
@@ -1793,28 +1599,6 @@ public class PushServiceSocket {
}
}
/**
* Like {@link UnopinionatedResponseCodeHandler} but also treats a 204 as a failure, since that means that the server intentionally
* timed out before a valid result for the long poll was returned. Easier that way.
*/
private static class LongPollingResponseCodeHandler implements ResponseCodeHandler {
@Override
public void handle(int responseCode, ResponseBody body, Function<String, String> getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (responseCode == 204 || responseCode < 200 || responseCode > 299) {
String bodyString = null;
if (body != null) {
try {
bodyString = readBodyString(body);
} catch (MalformedResponseException e) {
Log.w(TAG, "Failed to read body string", e);
}
}
throw new NonSuccessfulResponseCodeException(responseCode, "Response: " + responseCode, bodyString);
}
}
}
/**
* A {@link ResponseCodeHandler} that only throws {@link NonSuccessfulResponseCodeException} with the response body.
* Any further processing is left to the caller.
@@ -2007,45 +1791,6 @@ public class PushServiceSocket {
}
}
/**
* Handler for Google Play Billing purchase token linking
*/
private static class LinkGooglePlayBillingPurchaseTokenResponseCodeHandler implements ResponseCodeHandler {
@Override
public void handle(int responseCode, ResponseBody body, Function<String, String> getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (responseCode < 400) {
return;
}
throw new NonSuccessfulResponseCodeException(responseCode);
}
}
/**
* Handler for a couple in app payment endpoints.
*/
private static class InAppPaymentResponseCodeHandler implements ResponseCodeHandler {
@Override
public void handle(int responseCode, ResponseBody body, Function<String, String> getHeader) throws NonSuccessfulResponseCodeException, PushNetworkException {
if (responseCode < 400) {
return;
}
if (responseCode == 440) {
InAppPaymentProcessorError exception;
try {
exception = JsonUtil.fromJson(body.string(), InAppPaymentProcessorError.class);
} catch (IOException e) {
throw new NonSuccessfulResponseCodeException(440);
}
throw exception;
} else {
throw new NonSuccessfulResponseCodeException(responseCode);
}
}
}
private static class RegistrationSessionResponseHandler implements ResponseCodeHandler {
@Override

View File

@@ -7,27 +7,28 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.push.PushServiceSocket
class DonationsServiceTest {
private val pushServiceSocket: PushServiceSocket = mockk<PushServiceSocket>()
private val testSubject = DonationsService(pushServiceSocket)
private val donationsApi: DonationsApi = mockk<DonationsApi>()
private val testSubject = DonationsService(donationsApi)
private val activeSubscription = ActiveSubscription.EMPTY
@Test
fun givenASubscriberId_whenIGetASuccessfulResponse_thenItIsMappedWithTheCorrectStatusCodeAndNonEmptyObject() {
// GIVEN
val subscriberId = SubscriberId.generate()
every { pushServiceSocket.getSubscription(subscriberId.serialize()) } returns activeSubscription
every { donationsApi.getSubscription(subscriberId) } returns NetworkResult.Success(activeSubscription)
// WHEN
val response = testSubject.getSubscription(subscriberId)
// THEN
verify { pushServiceSocket.getSubscription(subscriberId.serialize()) }
verify { donationsApi.getSubscription(subscriberId) }
assertEquals(200, response.status)
assertTrue(response.result.isPresent)
}
@@ -36,13 +37,13 @@ class DonationsServiceTest {
fun givenASubscriberId_whenIGetAnUnsuccessfulResponse_thenItIsMappedWithTheCorrectStatusCodeAndEmptyObject() {
// GIVEN
val subscriberId = SubscriberId.generate()
every { pushServiceSocket.getSubscription(subscriberId.serialize()) } throws NonSuccessfulResponseCodeException(403)
every { donationsApi.getSubscription(subscriberId) } returns NetworkResult.StatusCodeError(NonSuccessfulResponseCodeException(403))
// WHEN
val response = testSubject.getSubscription(subscriberId)
// THEN
verify { pushServiceSocket.getSubscription(subscriberId.serialize()) }
verify { donationsApi.getSubscription(subscriberId) }
assertEquals(403, response.status)
assertFalse(response.result.isPresent)
}