From ecb040ce982fbfa07b20800ea589abeec5a06510 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 2 Apr 2025 10:58:43 -0400 Subject: [PATCH] Convert donations apis to WebSocket. --- .../MessageBackupsCheckoutActivityTest.kt | 9 - ...outFlowActivityTest__RecurringDonations.kt | 94 +++-- ...umentationApplicationDependencyProvider.kt | 11 + .../securesms/testing/InAppPaymentsRule.kt | 38 +-- .../delete/DeleteAccountRepository.java | 10 +- .../securesms/dependencies/AppDependencies.kt | 7 +- .../ApplicationDependencyProvider.java | 11 +- .../dependencies/NetworkDependenciesModule.kt | 7 +- .../MockApplicationDependencyProvider.kt | 7 +- .../signalservice/api/NetworkResultUtil.kt | 49 +++ .../api/SignalServiceAccountManager.java | 2 +- .../signalservice/api/account/AccountApi.kt | 1 + .../BoostReceiptCredentialRequestJson.java | 8 +- .../api/donations/DonationsApi.kt | 320 ++++++++++++++++++ ...PalConfirmOneTimePaymentIntentPayload.java | 7 +- ...yPalCreateOneTimePaymentIntentPayload.java | 7 +- .../PayPalCreatePaymentMethodPayload.java | 7 +- .../ReceiptCredentialRequestJson.java | 7 +- .../ReceiptCredentialResponseJson.java | 11 +- .../RedeemArchivesReceiptRequest.kt | 7 +- .../RedeemDonationReceiptRequest.java | 7 +- .../StripeOneTimePaymentIntentPayload.java | 7 +- .../signalservice/api/message/MessageApi.kt | 3 +- .../api/services/DonationsService.java | 53 ++- .../internal/WebSocketRequestExt.kt | 7 +- .../internal/push/PushServiceSocket.java | 255 -------------- .../api/services/DonationsServiceTest.kt | 15 +- 27 files changed, 575 insertions(+), 392 deletions(-) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/BoostReceiptCredentialRequestJson.java (78%) create mode 100644 libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/DonationsApi.kt rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/PayPalConfirmOneTimePaymentIntentPayload.java (84%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/PayPalCreateOneTimePaymentIntentPayload.java (82%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/PayPalCreatePaymentMethodPayload.java (69%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/ReceiptCredentialRequestJson.java (76%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/ReceiptCredentialResponseJson.java (75%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/RedeemArchivesReceiptRequest.kt (79%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/RedeemDonationReceiptRequest.java (90%) rename libsignal-service/src/main/java/org/whispersystems/signalservice/{internal/push => api/donations}/StripeOneTimePaymentIntentPayload.java (78%) diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt index 95af439d73..da192621ba 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ui/subscription/MessageBackupsCheckoutActivityTest.kt @@ -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 } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt index 8456ea14fa..d28df7d8fc 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/components/settings/app/subscription/donate/CheckoutFlowActivityTest__RecurringDonations.kt @@ -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 ) - } - ) + ) + } } } diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt index 4c4d089bf0..1b71cfce35 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/dependencies/InstrumentationApplicationDependencyProvider.kt @@ -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, diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt index 118eb4f726..8ae591f81c 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/testing/InAppPaymentsRule.kt @@ -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) + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java index 4fe1a63bde..bceecbd08c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/delete/DeleteAccountRepository.java @@ -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"); diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt index 22db0c8ba4..ad63f54557 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppDependencies.kt @@ -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 } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index f4c70fbf61..250de2c15c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt index 718e325f73..d7c994c0cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/NetworkDependenciesModule.kt @@ -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()) diff --git a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt index 684c608194..5e489f5f82 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/dependencies/MockApplicationDependencyProvider.kt @@ -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) + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt index 7851532f60..1ce8d60fef 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/NetworkResultUtil.kt @@ -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 successOrThrow(result: NetworkResult): 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 toIAPBasicLegacy(result: NetworkResult): 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() ?: result.exception + 440 -> result.parseJsonBody() ?: result.exception + else -> result.exception + } + } + } + } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java index 0cffceabcd..acf28d1608 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceAccountManager.java @@ -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 ); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt index af59cf38bf..8276ffb4dc 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/account/AccountApi.kt @@ -97,6 +97,7 @@ class AccountApi(private val authWebSocket: SignalWebSocket.AuthenticatedWebSock /** * DELETE /v1/accounts/me * - 204: Success + * - 4401: Success */ fun deleteAccount(): NetworkResult { val request = WebSocketRequestMessage.delete("/v1/accounts/me") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BoostReceiptCredentialRequestJson.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/BoostReceiptCredentialRequestJson.java similarity index 78% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BoostReceiptCredentialRequestJson.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/BoostReceiptCredentialRequestJson.java index 4df8eab03f..390e7a9b4e 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/BoostReceiptCredentialRequestJson.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/BoostReceiptCredentialRequestJson.java @@ -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") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/DonationsApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/DonationsApi.kt new file mode 100644 index 0000000000..295681374f --- /dev/null +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/DonationsApi.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + return mapOf("Accept-Language" to "${this.language}${if (this.country.isNotEmpty()) "-${this.country}" else ""}") + } +} diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalConfirmOneTimePaymentIntentPayload.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalConfirmOneTimePaymentIntentPayload.java similarity index 84% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalConfirmOneTimePaymentIntentPayload.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalConfirmOneTimePaymentIntentPayload.java index a9d887390a..85626ec9dc 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalConfirmOneTimePaymentIntentPayload.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalConfirmOneTimePaymentIntentPayload.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreateOneTimePaymentIntentPayload.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreateOneTimePaymentIntentPayload.java similarity index 82% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreateOneTimePaymentIntentPayload.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreateOneTimePaymentIntentPayload.java index f48be306dc..9fa3731974 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreateOneTimePaymentIntentPayload.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreateOneTimePaymentIntentPayload.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreatePaymentMethodPayload.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreatePaymentMethodPayload.java similarity index 69% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreatePaymentMethodPayload.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreatePaymentMethodPayload.java index d231959605..358b9d9a5f 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PayPalCreatePaymentMethodPayload.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/PayPalCreatePaymentMethodPayload.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialRequestJson.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialRequestJson.java similarity index 76% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialRequestJson.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialRequestJson.java index 9d33692998..a16c846a0a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialRequestJson.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialRequestJson.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialResponseJson.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialResponseJson.java similarity index 75% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialResponseJson.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialResponseJson.java index e3f839b647..ed62e4893d 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/ReceiptCredentialResponseJson.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/ReceiptCredentialResponseJson.java @@ -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; } } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemArchivesReceiptRequest.kt similarity index 79% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemArchivesReceiptRequest.kt index 21ed0312d3..c04eeb09dd 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemArchivesReceiptRequest.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemArchivesReceiptRequest.kt @@ -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 diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemDonationReceiptRequest.java similarity index 90% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemDonationReceiptRequest.java index ffa5d761fc..4fba59f176 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/RedeemDonationReceiptRequest.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/RedeemDonationReceiptRequest.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/StripeOneTimePaymentIntentPayload.java similarity index 78% rename from libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java rename to libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/StripeOneTimePaymentIntentPayload.java index f36bac40dd..298b16f63e 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/StripeOneTimePaymentIntentPayload.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/donations/StripeOneTimePaymentIntentPayload.java @@ -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; diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/message/MessageApi.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/message/MessageApi.kt index b66a50ea79..e0ec021134 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/message/MessageApi.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/message/MessageApi.kt @@ -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 { - 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") diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java index 6871cf86f4..b1e4d453ab 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/api/services/DonationsService.java @@ -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> donationsConfigurationCache = new AtomicReference<>(null); private final AtomicReference> 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 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.forUnknownError(e); @@ -85,7 +84,7 @@ public class DonationsService { */ public ServiceResponse redeemArchivesReceipt(ReceiptCredentialPresentation receiptCredentialPresentation) { try { - pushServiceSocket.redeemArchivesReceipt(receiptCredentialPresentation); + NetworkResultUtil.successOrThrow(donationsApi.redeemArchivesReceipt(receiptCredentialPresentation)); return ServiceResponse.forResult(EmptyResponse.INSTANCE, 200, null); } catch (Exception e) { return ServiceResponse.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 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 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 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 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 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 cancelSubscription(SubscriberId subscriberId) { return wrapInServiceResponse(() -> { - pushServiceSocket.deleteSubscription(subscriberId.serialize()); + NetworkResultUtil.successOrThrow(donationsApi.deleteSubscription(subscriberId)); return new Pair<>(EmptyResponse.INSTANCE, 200); }); } public ServiceResponse 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 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 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 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 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 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); }); } diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/WebSocketRequestExt.kt b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/WebSocketRequestExt.kt index 576bb1c8fb..8e14a4bb2b 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/WebSocketRequestExt.kt +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/WebSocketRequestExt.kt @@ -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): WebSocketRequestMessage { +fun WebSocketRequestMessage.Companion.putCustom(path: String, body: ByteArray, headers: Map): WebSocketRequestMessage { return WebSocketRequestMessage( verb = "PUT", path = path, diff --git a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java index 527dd10c38..f42799f78a 100644 --- a/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java +++ b/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/push/PushServiceSocket.java @@ -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 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 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 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 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 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 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 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 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 diff --git a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/services/DonationsServiceTest.kt b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/services/DonationsServiceTest.kt index fa36648a79..bc29104610 100644 --- a/libsignal-service/src/test/java/org/whispersystems/signalservice/api/services/DonationsServiceTest.kt +++ b/libsignal-service/src/test/java/org/whispersystems/signalservice/api/services/DonationsServiceTest.kt @@ -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() - private val testSubject = DonationsService(pushServiceSocket) + private val donationsApi: DonationsApi = mockk() + 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) }