mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 04:58:45 +00:00
Convert donations apis to WebSocket.
This commit is contained in:
committed by
Michelle Tang
parent
2f9692a1a0
commit
ecb040ce98
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
@@ -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 ""}")
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user