Implement initial support for IAP data.
This commit is contained in:
parent
f537fa6436
commit
f2b4bd0585
35 changed files with 957 additions and 241 deletions
|
@ -108,11 +108,12 @@ class CheckoutFlowActivityTest__RecurringDonations {
|
|||
currency = currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
|
@ -149,11 +150,12 @@ class CheckoutFlowActivityTest__RecurringDonations {
|
|||
currency = currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
|
||||
|
||||
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
|
||||
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.signal.core.util.count
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.signal.core.util.readToSingleInt
|
||||
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.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
|
||||
class InAppPaymentSubscriberTableTest {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.inAppPaymentSubscribers.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenASubscriberWithCurrencyAndIAPData_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = Currency.getInstance("USD"),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
|
||||
fail("Expected a thrown exception.")
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenADonorSubscriberWithGoogleIAPData_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
|
||||
fail("Expected a thrown exception.")
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenADonorSubscriberWithAppleIAPData_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
|
||||
fail("Expected a thrown exception.")
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenADonorSubscriberWithoutCurrency_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
|
||||
fail("Expected a thrown exception.")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenADonorSubscriberWithCurrency_whenITryToInsert_thenIExpectSuccess() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = Currency.getInstance("USD"),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenABackupSubscriberWithCurrency_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = Currency.getInstance("USD"),
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
|
||||
fail("Expected a thrown exception.")
|
||||
}
|
||||
|
||||
@Test(expected = SQLiteConstraintException::class)
|
||||
fun givenABackupSubscriberWithoutIAPData_whenITryToInsert_thenIExpectException() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABackupSubscriberWithGoogleIAPData_whenITryToInsert_thenIExpectSuccess() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABackupSubscriberWithAppleIAPData_whenITryToInsert_thenIExpectSuccess() {
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(subscriber)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenABackupSubscriberWithAppleIAPData_whenITryToInsertAGoogleSubscriber_thenIExpectSuccess() {
|
||||
val appleSubscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(appleSubscriber)
|
||||
|
||||
val googleSubscriber = InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(googleSubscriber)
|
||||
|
||||
val subscriberCount = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
|
||||
.from(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
|
||||
subscriberCount assertIs 1
|
||||
|
||||
val subscriber = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
subscriber.iapSubscriptionId?.originalTransactionId assertIs null
|
||||
subscriber.iapSubscriptionId?.purchaseToken assertIs "testToken"
|
||||
subscriber.subscriberId assertIs googleSubscriber.subscriberId
|
||||
}
|
||||
}
|
|
@ -104,7 +104,8 @@ class FixInAppCurrencyIfAbleTest {
|
|||
currency = Currency.getInstance(currencyCode),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
|
||||
|
|
|
@ -14,6 +14,7 @@ import okhttp3.mockwebserver.MockWebServer
|
|||
import okhttp3.mockwebserver.RecordedRequest
|
||||
import okio.ByteString
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.BuildConfig
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
|
||||
|
@ -49,6 +50,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
|||
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
|
||||
private val recipientCache: LiveRecipientCache
|
||||
private var signalServiceMessageSender: SignalServiceMessageSender? = null
|
||||
private var billingApi: BillingApi = mockk()
|
||||
|
||||
init {
|
||||
runSync {
|
||||
|
@ -108,6 +110,8 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
|
|||
recipientCache = LiveRecipientCache(application) { r -> r.run() }
|
||||
}
|
||||
|
||||
override fun provideBillingApi(): BillingApi = billingApi
|
||||
|
||||
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
|
||||
return serviceNetworkAccessMock
|
||||
}
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
package org.thoughtcrime.securesms.migrations
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.mockk.Called
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.verify
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.billing.BillingPurchaseState
|
||||
import org.signal.core.util.deleteAll
|
||||
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testing.SignalActivityRule
|
||||
import org.thoughtcrime.securesms.testing.assertIs
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class GooglePlayBillingPurchaseTokenMigrationJobTest {
|
||||
@get:Rule
|
||||
val harness = SignalActivityRule()
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
SignalDatabase.inAppPaymentSubscribers.writableDatabase.deleteAll(InAppPaymentSubscriberTable.TABLE_NAME)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenNoSubscribers_whenIRunJob_thenIExpectNoBillingAccess() {
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
verify { AppDependencies.billingApi wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSubscriberWithAppleData_whenIRunJob_thenIExpectNoBillingAccess() {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.AppleIAPOriginalTransactionId(1000L)
|
||||
)
|
||||
)
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
verify { AppDependencies.billingApi wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSubscriberWithGoogleToken_whenIRunJob_thenIExpectNoBillingAccess() {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("testToken")
|
||||
)
|
||||
)
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
verify { AppDependencies.billingApi wasNot Called }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSubscriberWithPlaceholderAndNoBillingAccess_whenIRunJob_thenIExpectNoUpdate() {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
|
||||
|
||||
sub?.iapSubscriptionId?.purchaseToken assertIs "-"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSubscriberWithPlaceholderAndNoPurchase_whenIRunJob_thenIExpectNoUpdate() {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
|
||||
|
||||
sub?.iapSubscriptionId?.purchaseToken assertIs "-"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun givenSubscriberWithPurchase_whenIRunJob_thenIExpectUpdate() {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING,
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken("-")
|
||||
)
|
||||
)
|
||||
|
||||
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
|
||||
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
|
||||
purchaseState = BillingPurchaseState.PURCHASED,
|
||||
purchaseToken = "purchaseToken",
|
||||
isAcknowledged = true,
|
||||
purchaseTime = System.currentTimeMillis(),
|
||||
isAutoRenewing = true
|
||||
)
|
||||
|
||||
val job = GooglePlayBillingPurchaseTokenMigrationJob()
|
||||
|
||||
job.run()
|
||||
|
||||
val sub = SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
|
||||
|
||||
sub?.iapSubscriptionId?.purchaseToken assertIs "purchaseToken"
|
||||
}
|
||||
}
|
|
@ -36,14 +36,14 @@ class SubscriberIdMigrationJobTest {
|
|||
@Test
|
||||
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
|
||||
val subscriberId = SubscriberId.generate()
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(Currency.getInstance("USD"))
|
||||
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
|
||||
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
|
||||
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
|
||||
|
||||
testSubject.run()
|
||||
|
||||
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD")
|
||||
|
||||
actual.assertIsNotNull()
|
||||
actual!!.subscriberId.bytes assertIs subscriberId.bytes
|
||||
|
|
|
@ -54,8 +54,8 @@ object AccountDataArchiveProcessor {
|
|||
val selfId = db.recipientTable.getByAci(signalStore.accountValues.aci!!).get()
|
||||
val selfRecord = db.recipientTable.getRecordForSync(selfId)!!
|
||||
|
||||
val donationCurrency = signalStore.inAppPaymentValues.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val donationCurrency = signalStore.inAppPaymentValues.getRecurringDonationCurrency()
|
||||
val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode)
|
||||
|
||||
val chatColors = SignalStore.chatColors.chatColors
|
||||
val chatWallpaper = SignalStore.wallpaper.currentRawWallpaper
|
||||
|
@ -127,11 +127,12 @@ object AccountDataArchiveProcessor {
|
|||
val localSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
|
||||
val subscriber = InAppPaymentSubscriberRecord(
|
||||
remoteSubscriberId,
|
||||
Currency.getInstance(accountData.donationSubscriberData.currencyCode),
|
||||
InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
localSubscriber?.requiresCancel ?: accountData.donationSubscriberData.manuallyCancelled,
|
||||
InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
subscriberId = remoteSubscriberId,
|
||||
currency = Currency.getInstance(accountData.donationSubscriberData.currencyCode),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = localSubscriber?.requiresCancel ?: accountData.donationSubscriberData.manuallyCancelled,
|
||||
paymentMethodType = InAppPaymentsRepository.getLatestPaymentMethodType(InAppPaymentSubscriberRecord.Type.DONATION),
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
|
@ -273,9 +274,12 @@ object AccountDataArchiveProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method only supports donations subscriber data, and assumes there is a currency code available.
|
||||
*/
|
||||
private fun InAppPaymentSubscriberRecord.toSubscriberData(manuallyCancelled: Boolean): AccountData.SubscriberData {
|
||||
val subscriberId = subscriberId.bytes.toByteString()
|
||||
val currencyCode = currency.currencyCode
|
||||
val currencyCode = currency!!.currencyCode
|
||||
return AccountData.SubscriberData(subscriberId = subscriberId, currencyCode = currencyCode, manuallyCancelled = manuallyCancelled)
|
||||
}
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
|
|||
stage = state.stage,
|
||||
currentBackupTier = state.currentMessageBackupTier,
|
||||
selectedBackupTier = state.selectedMessageBackupTier,
|
||||
availableBackupTypes = state.availableBackupTypes.filter { it.tier == MessageBackupTier.FREE || state.hasBackupSubscriberAvailable },
|
||||
availableBackupTypes = state.availableBackupTypes,
|
||||
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
|
||||
onNavigationClick = viewModel::goToPreviousStage,
|
||||
onReadMoreClicked = {},
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|||
import org.whispersystems.signalservice.api.AccountEntropyPool
|
||||
|
||||
data class MessageBackupsFlowState(
|
||||
val hasBackupSubscriberAvailable: Boolean = false,
|
||||
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val currentMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
|
||||
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.backup.v2.ui.subscription
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -42,6 +43,7 @@ import org.thoughtcrime.securesms.jobs.InAppPaymentPurchaseTokenJob
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
|
@ -68,17 +70,6 @@ class MessageBackupsFlowViewModel(
|
|||
check(SignalStore.backup.backupTier != MessageBackupTier.PAID) { "This screen does not support cancellation or downgrades." }
|
||||
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
ensureSubscriberIdForBackups()
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
hasBackupSubscriberAvailable = true
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to ensure a subscriber id exists.", e)
|
||||
}
|
||||
|
||||
internalStateFlow.update {
|
||||
it.copy(
|
||||
availableBackupTypes = BackupRepository.getAvailableBackupsTypes(
|
||||
|
@ -210,7 +201,6 @@ class MessageBackupsFlowViewModel(
|
|||
MessageBackupTier.PAID -> {
|
||||
check(state.selectedMessageBackupTier == MessageBackupTier.PAID)
|
||||
check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier })
|
||||
check(state.hasBackupSubscriberAvailable)
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
internalStateFlow.update { it.copy(inAppPayment = null) }
|
||||
|
@ -221,7 +211,7 @@ class MessageBackupsFlowViewModel(
|
|||
val id = SignalDatabase.inAppPayments.insert(
|
||||
type = InAppPaymentType.RECURRING_BACKUP,
|
||||
state = InAppPaymentTable.State.CREATED,
|
||||
subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId,
|
||||
subscriberId = null,
|
||||
endOfPeriod = null,
|
||||
inAppPaymentData = InAppPaymentData(
|
||||
badge = null,
|
||||
|
@ -251,10 +241,9 @@ class MessageBackupsFlowViewModel(
|
|||
* the screen this is called in is assumed to only be accessible if the user does not currently have
|
||||
* a subscription.
|
||||
*/
|
||||
private suspend fun ensureSubscriberIdForBackups() {
|
||||
val product = AppDependencies.billingApi.queryProduct() ?: error("No product available.")
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(product.price.currency, InAppPaymentSubscriberRecord.Type.BACKUP)
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP).blockingAwait()
|
||||
@WorkerThread
|
||||
private fun ensureSubscriberIdForBackups(purchaseToken: IAPSubscriptionId.GooglePlayBillingPurchaseToken) {
|
||||
RecurringInAppPaymentRepository.ensureSubscriberId(InAppPaymentSubscriberRecord.Type.BACKUP, iapSubscriptionId = purchaseToken).blockingAwait()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -264,11 +253,13 @@ class MessageBackupsFlowViewModel(
|
|||
@OptIn(FlowPreview::class)
|
||||
private suspend fun handleSuccess(result: BillingPurchaseResult.Success, inAppPaymentId: InAppPaymentTable.InAppPaymentId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Log.d(TAG, "Setting purchase token data on InAppPayment.")
|
||||
Log.d(TAG, "Setting purchase token data on InAppPayment and InAppPaymentSubscriber.")
|
||||
ensureSubscriberIdForBackups(IAPSubscriptionId.GooglePlayBillingPurchaseToken(result.purchaseToken))
|
||||
|
||||
val inAppPayment = SignalDatabase.inAppPayments.getById(inAppPaymentId)!!
|
||||
SignalDatabase.inAppPayments.update(
|
||||
inAppPayment.copy(
|
||||
subscriberId = InAppPaymentsRepository.requireSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP).subscriberId,
|
||||
data = inAppPayment.data.copy(
|
||||
redemption = inAppPayment.data.redemption!!.copy(
|
||||
googlePlayBillingPurchaseToken = result.purchaseToken
|
||||
|
|
|
@ -450,14 +450,10 @@ object InAppPaymentsRepository {
|
|||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("DiscouragedApi")
|
||||
@WorkerThread
|
||||
fun getSubscriber(currency: Currency, type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
val subscriber = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode(currency.currencyCode, type)
|
||||
fun getRecurringDonationSubscriber(currency: Currency): InAppPaymentSubscriberRecord? {
|
||||
val subscriber = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode(currency.currencyCode)
|
||||
|
||||
return if (subscriber == null && type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.inAppPayments.getSubscriber(currency)
|
||||
} else {
|
||||
subscriber
|
||||
}
|
||||
return subscriber ?: SignalStore.inAppPayments.getSubscriber(currency)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -466,10 +462,14 @@ object InAppPaymentsRepository {
|
|||
@JvmStatic
|
||||
@WorkerThread
|
||||
fun getSubscriber(type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
val currency = SignalStore.inAppPayments.getSubscriptionCurrency(type)
|
||||
if (type == InAppPaymentSubscriberRecord.Type.BACKUP) {
|
||||
return SignalDatabase.inAppPaymentSubscribers.getBackupsSubscriber()
|
||||
}
|
||||
|
||||
val currency = SignalStore.inAppPayments.getRecurringDonationCurrency()
|
||||
Log.d(TAG, "Attempting to retrieve subscriber of type $type for ${currency.currencyCode}")
|
||||
|
||||
return getSubscriber(currency, type)
|
||||
return getRecurringDonationSubscriber(currency)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
|||
import org.thoughtcrime.securesms.subscription.LevelUpdate
|
||||
import org.thoughtcrime.securesms.subscription.LevelUpdateOperation
|
||||
import org.thoughtcrime.securesms.subscription.Subscription
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
|
||||
import org.whispersystems.signalservice.api.subscriptions.IdempotencyKey
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
|
@ -109,7 +110,7 @@ object RecurringInAppPaymentRepository {
|
|||
return cancelCompletable.andThen(ensureSubscriberId(subscriberType, isRotation = true))
|
||||
}
|
||||
|
||||
fun ensureSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type, isRotation: Boolean = false): Completable {
|
||||
fun ensureSubscriberId(subscriberType: InAppPaymentSubscriberRecord.Type, isRotation: Boolean = false, iapSubscriptionId: IAPSubscriptionId? = null): Completable {
|
||||
return Single.fromCallable {
|
||||
Log.d(TAG, "Ensuring SubscriberId for type $subscriberType exists on Signal service {isRotation?$isRotation}...", true)
|
||||
|
||||
|
@ -131,10 +132,19 @@ object RecurringInAppPaymentRepository {
|
|||
InAppPaymentsRepository.setSubscriber(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = subscriberId,
|
||||
currency = SignalStore.inAppPayments.getSubscriptionCurrency(subscriberType),
|
||||
currency = if (subscriberType == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.inAppPayments.getRecurringDonationCurrency()
|
||||
} else {
|
||||
null
|
||||
},
|
||||
type = subscriberType,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
paymentMethodType = if (subscriberType == InAppPaymentSubscriberRecord.Type.BACKUP) {
|
||||
InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING
|
||||
} else {
|
||||
InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
},
|
||||
iapSubscriptionId = iapSubscriptionId
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -214,7 +224,7 @@ object RecurringInAppPaymentRepository {
|
|||
AppDependencies.donationsService.updateSubscriptionLevel(
|
||||
subscriber.subscriberId,
|
||||
subscriptionLevel,
|
||||
subscriber.currency.currencyCode,
|
||||
subscriber.currency!!.currencyCode,
|
||||
levelUpdateOperation.idempotencyKey.serialize(),
|
||||
subscriberType.lock
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@ class SetCurrencyViewModel(
|
|||
private val store = Store(
|
||||
SetCurrencyState(
|
||||
selectedCurrencyCode = if (inAppPaymentType.recurring) {
|
||||
SignalStore.inAppPayments.getSubscriptionCurrency(inAppPaymentType.requireSubscriberType()).currencyCode
|
||||
SignalStore.inAppPayments.getRecurringDonationCurrency().currencyCode
|
||||
} else {
|
||||
SignalStore.inAppPayments.getOneTimeCurrency().currencyCode
|
||||
},
|
||||
|
@ -34,6 +34,10 @@ class SetCurrencyViewModel(
|
|||
)
|
||||
)
|
||||
|
||||
init {
|
||||
check(inAppPaymentType != InAppPaymentType.RECURRING_BACKUP) { "Setting currency is unsupported for backups." }
|
||||
}
|
||||
|
||||
val state: LiveData<SetCurrencyState> = store.stateLiveData
|
||||
|
||||
fun setSelectedCurrency(selectedCurrencyCode: String) {
|
||||
|
@ -43,7 +47,7 @@ class SetCurrencyViewModel(
|
|||
SignalStore.inAppPayments.setOneTimeCurrency(Currency.getInstance(selectedCurrencyCode))
|
||||
} else {
|
||||
val currency = Currency.getInstance(selectedCurrencyCode)
|
||||
val subscriber = InAppPaymentsRepository.getSubscriber(currency, inAppPaymentType.requireSubscriberType())
|
||||
val subscriber = InAppPaymentsRepository.getRecurringDonationSubscriber(currency)
|
||||
|
||||
if (subscriber != null) {
|
||||
InAppPaymentsRepository.setSubscriber(subscriber)
|
||||
|
@ -54,7 +58,8 @@ class SetCurrencyViewModel(
|
|||
currency = currency,
|
||||
type = inAppPaymentType.requireSubscriberType(),
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
|||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.boost.Boost
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.manage.NonVerifiedMonthlyDonation
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingOneTimeDonation
|
||||
import org.thoughtcrime.securesms.database.model.isLongRunning
|
||||
import org.thoughtcrime.securesms.database.model.isPending
|
||||
|
@ -114,7 +113,7 @@ data class DonateToSignalState(
|
|||
}
|
||||
|
||||
data class MonthlyDonationState(
|
||||
val selectedCurrency: Currency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION),
|
||||
val selectedCurrency: Currency = SignalStore.inAppPayments.getRecurringDonationCurrency(),
|
||||
val subscriptions: List<Subscription> = emptyList(),
|
||||
private val _activeSubscription: ActiveSubscription? = null,
|
||||
val selectedSubscription: Subscription? = null,
|
||||
|
|
|
@ -388,17 +388,18 @@ class DonateToSignalViewModel(
|
|||
onSuccess = { subscriptions ->
|
||||
if (subscriptions.isNotEmpty()) {
|
||||
val priceCurrencies = subscriptions[0].prices.map { it.currency }
|
||||
val selectedCurrency = SignalStore.inAppPayments.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val selectedCurrency = SignalStore.inAppPayments.getRecurringDonationCurrency()
|
||||
|
||||
if (selectedCurrency !in priceCurrencies) {
|
||||
Log.w(TAG, "Unsupported currency selection. Defaulting to USD. $selectedCurrency isn't supported.")
|
||||
val usd = PlatformCurrencyUtil.USD
|
||||
val newSubscriber = InAppPaymentsRepository.getSubscriber(usd, InAppPaymentSubscriberRecord.Type.DONATION) ?: InAppPaymentSubscriberRecord(
|
||||
val newSubscriber = InAppPaymentsRepository.getRecurringDonationSubscriber(usd) ?: InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.generate(),
|
||||
currency = usd,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.UNKNOWN,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
InAppPaymentsRepository.setSubscriber(newSubscriber)
|
||||
RecurringInAppPaymentRepository.syncAccountRecord().subscribe()
|
||||
|
|
|
@ -240,7 +240,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
|
|||
// TODO [alex] - DB on main thread!
|
||||
val subscriber: InAppPaymentSubscriberRecord? = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val summary = if (subscriber != null) {
|
||||
"""currency code: ${subscriber.currency.currencyCode}
|
||||
"""currency code: ${subscriber.currency!!.currencyCode}
|
||||
|subscriber id: ${subscriber.subscriberId.serialize()}
|
||||
""".trimMargin()
|
||||
} else {
|
||||
|
|
|
@ -13,17 +13,21 @@ import androidx.core.content.contentValuesOf
|
|||
import org.signal.core.util.DatabaseSerializer
|
||||
import org.signal.core.util.Serializer
|
||||
import org.signal.core.util.insertInto
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToSingleObject
|
||||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.signal.core.util.select
|
||||
import org.signal.core.util.update
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
|
||||
|
@ -61,7 +65,13 @@ class InAppPaymentSubscriberTable(
|
|||
/** Specifies which payment method was utilized for the latest transaction with this id */
|
||||
private const val PAYMENT_METHOD_TYPE = "payment_method_type"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
/** Google Play Billing purchase token, only valid for backup payments */
|
||||
private const val PURCHASE_TOKEN = "purchase_token"
|
||||
|
||||
/** iOS original transaction id token, only valid for synced backups that originated on iOS */
|
||||
private const val ORIGINAL_TRANSACTION_ID = "original_transaction_id"
|
||||
|
||||
val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
$ID INTEGER PRIMARY KEY,
|
||||
$SUBSCRIBER_ID TEXT NOT NULL UNIQUE,
|
||||
|
@ -69,7 +79,14 @@ class InAppPaymentSubscriberTable(
|
|||
$TYPE INTEGER NOT NULL,
|
||||
$REQUIRES_CANCEL INTEGER DEFAULT 0,
|
||||
$PAYMENT_METHOD_TYPE INTEGER DEFAULT 0,
|
||||
UNIQUE($CURRENCY_CODE, $TYPE)
|
||||
$PURCHASE_TOKEN TEXT,
|
||||
$ORIGINAL_TRANSACTION_ID INTEGER,
|
||||
UNIQUE($CURRENCY_CODE, $TYPE),
|
||||
CHECK (
|
||||
($CURRENCY_CODE != '' AND $PURCHASE_TOKEN IS NULL AND $ORIGINAL_TRANSACTION_ID IS NULL AND $TYPE = ${TypeSerializer.serialize(InAppPaymentSubscriberRecord.Type.DONATION)})
|
||||
OR ($CURRENCY_CODE = '' AND $PURCHASE_TOKEN IS NOT NULL AND $ORIGINAL_TRANSACTION_ID IS NULL AND $TYPE = ${TypeSerializer.serialize(InAppPaymentSubscriberRecord.Type.BACKUP)})
|
||||
OR ($CURRENCY_CODE = '' AND $PURCHASE_TOKEN IS NULL AND $ORIGINAL_TRANSACTION_ID IS NOT NULL AND $TYPE = ${TypeSerializer.serialize(InAppPaymentSubscriberRecord.Type.BACKUP)})
|
||||
)
|
||||
)
|
||||
"""
|
||||
}
|
||||
|
@ -80,20 +97,31 @@ class InAppPaymentSubscriberTable(
|
|||
* This is a destructive, mutating operation. For setting specific values, prefer the alternative setters available on this table class.
|
||||
*/
|
||||
fun insertOrReplace(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
|
||||
Log.i(TAG, "Setting subscriber for currency ${inAppPaymentSubscriberRecord.currency.currencyCode}", Exception(), true)
|
||||
if (inAppPaymentSubscriberRecord.type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
Log.i(TAG, "Setting subscriber for currency ${inAppPaymentSubscriberRecord.currency?.currencyCode}", Exception(), true)
|
||||
}
|
||||
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.insertInto(TABLE_NAME)
|
||||
.values(InAppPaymentSubscriberSerializer.serialize(inAppPaymentSubscriberRecord))
|
||||
.run(conflictStrategy = SQLiteDatabase.CONFLICT_REPLACE)
|
||||
|
||||
SignalStore.inAppPayments.setSubscriberCurrency(
|
||||
inAppPaymentSubscriberRecord.currency,
|
||||
inAppPaymentSubscriberRecord.type
|
||||
)
|
||||
if (inAppPaymentSubscriberRecord.type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.inAppPayments.setRecurringDonationCurrency(
|
||||
inAppPaymentSubscriberRecord.currency!!
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getBackupsSubscriber(): InAppPaymentSubscriberRecord? {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$TYPE = ?", TypeSerializer.serialize(InAppPaymentSubscriberRecord.Type.BACKUP))
|
||||
.run()
|
||||
.readToSingleObject(InAppPaymentSubscriberSerializer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the subscriber in question requires a cancellation before a new subscription can be created.
|
||||
*/
|
||||
|
@ -117,10 +145,10 @@ class InAppPaymentSubscriberTable(
|
|||
/**
|
||||
* Retrieves a subscriber for the given type by the currency code.
|
||||
*/
|
||||
fun getByCurrencyCode(currencyCode: String, type: InAppPaymentSubscriberRecord.Type): InAppPaymentSubscriberRecord? {
|
||||
fun getByCurrencyCode(currencyCode: String): InAppPaymentSubscriberRecord? {
|
||||
return readableDatabase.select()
|
||||
.from(TABLE_NAME)
|
||||
.where("$TYPE = ? AND $CURRENCY_CODE = ?", TypeSerializer.serialize(type), currencyCode.uppercase())
|
||||
.where("$CURRENCY_CODE = ?", currencyCode.uppercase())
|
||||
.run()
|
||||
.readToSingleObject(InAppPaymentSubscriberSerializer)
|
||||
}
|
||||
|
@ -140,10 +168,12 @@ class InAppPaymentSubscriberTable(
|
|||
override fun serialize(data: InAppPaymentSubscriberRecord): ContentValues {
|
||||
return contentValuesOf(
|
||||
SUBSCRIBER_ID to data.subscriberId.serialize(),
|
||||
CURRENCY_CODE to data.currency.currencyCode.uppercase(),
|
||||
CURRENCY_CODE to (data.currency?.currencyCode?.uppercase() ?: ""),
|
||||
TYPE to TypeSerializer.serialize(data.type),
|
||||
REQUIRES_CANCEL to data.requiresCancel,
|
||||
PAYMENT_METHOD_TYPE to data.paymentMethodType.value
|
||||
PAYMENT_METHOD_TYPE to data.paymentMethodType.value,
|
||||
PURCHASE_TOKEN to data.iapSubscriptionId?.purchaseToken,
|
||||
ORIGINAL_TRANSACTION_ID to data.iapSubscriptionId?.originalTransactionId
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -152,12 +182,36 @@ class InAppPaymentSubscriberTable(
|
|||
val currencyCode = input.requireNonNullString(CURRENCY_CODE).takeIf { it.isNotEmpty() }
|
||||
return InAppPaymentSubscriberRecord(
|
||||
subscriberId = SubscriberId.deserialize(input.requireNonNullString(SUBSCRIBER_ID)),
|
||||
currency = currencyCode?.let { Currency.getInstance(it) } ?: SignalStore.inAppPayments.getSubscriptionCurrency(type),
|
||||
currency = resolveCurrency(currencyCode, type),
|
||||
type = type,
|
||||
requiresCancel = input.requireBoolean(REQUIRES_CANCEL) || currencyCode.isNullOrBlank(),
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.fromValue(input.requireInt(PAYMENT_METHOD_TYPE)) ?: InAppPaymentData.PaymentMethodType.UNKNOWN
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.fromValue(input.requireInt(PAYMENT_METHOD_TYPE)) ?: InAppPaymentData.PaymentMethodType.UNKNOWN,
|
||||
iapSubscriptionId = readIAPSubscriptionIdFromCursor(input)
|
||||
)
|
||||
}
|
||||
|
||||
private fun resolveCurrency(currencyCode: String?, type: InAppPaymentSubscriberRecord.Type): Currency? {
|
||||
return currencyCode?.let {
|
||||
Currency.getInstance(currencyCode)
|
||||
} ?: if (type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
SignalStore.inAppPayments.getRecurringDonationCurrency()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readIAPSubscriptionIdFromCursor(cursor: Cursor): IAPSubscriptionId? {
|
||||
val purchaseToken = cursor.requireString(PURCHASE_TOKEN)
|
||||
val originalTransactionId = cursor.requireLongOrNull(ORIGINAL_TRANSACTION_ID)
|
||||
|
||||
return if (purchaseToken.isNotNullOrBlank()) {
|
||||
IAPSubscriptionId.GooglePlayBillingPurchaseToken(purchaseToken)
|
||||
} else if (originalTransactionId != null) {
|
||||
IAPSubscriptionId.AppleIAPOriginalTransactionId(originalTransactionId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TypeSerializer : Serializer<InAppPaymentSubscriberRecord.Type, Int> {
|
||||
|
|
|
@ -118,6 +118,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V258_FixGroupRevoke
|
|||
import org.thoughtcrime.securesms.database.helpers.migration.V259_AdjustNotificationProfileMidnightEndTimes
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V260_RemapQuoteAuthors
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V261_RemapCallRingers
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V262_InAppPaymentsSubscriberTableRebuild
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
|
@ -238,10 +239,11 @@ object SignalDatabaseMigrations {
|
|||
258 to V258_FixGroupRevokedInviteeUpdate,
|
||||
259 to V259_AdjustNotificationProfileMidnightEndTimes,
|
||||
260 to V260_RemapQuoteAuthors,
|
||||
261 to V261_RemapCallRingers
|
||||
261 to V261_RemapCallRingers,
|
||||
261 to V262_InAppPaymentsSubscriberTableRebuild
|
||||
)
|
||||
|
||||
const val DATABASE_VERSION = 261
|
||||
const val DATABASE_VERSION = 262
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
|
||||
/**
|
||||
* Adds IAP fields and updates constraints.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V262_InAppPaymentsSubscriberTableRebuild : SignalDatabaseMigration {
|
||||
|
||||
private const val DONOR_TYPE = 0
|
||||
private const val BACKUP_TYPE = 1
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE in_app_payment_subscriber_tmp (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
subscriber_id TEXT NOT NULL UNIQUE,
|
||||
currency_code TEXT NOT NULL,
|
||||
type INTEGER NOT NULL,
|
||||
requires_cancel INTEGER DEFAULT 0,
|
||||
payment_method_type INTEGER DEFAULT 0,
|
||||
purchase_token TEXT,
|
||||
original_transaction_id INTEGER,
|
||||
UNIQUE(currency_code, type),
|
||||
CHECK (
|
||||
(currency_code != '' AND purchase_token IS NULL AND original_transaction_id IS NULL AND type = $DONOR_TYPE)
|
||||
OR (currency_code = '' AND purchase_token IS NOT NULL AND original_transaction_id IS NULL AND type = $BACKUP_TYPE)
|
||||
OR (currency_code = '' AND purchase_token IS NULL AND original_transaction_id IS NOT NULL AND type = $BACKUP_TYPE)
|
||||
)
|
||||
)
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO in_app_payment_subscriber_tmp (_id, subscriber_id, currency_code, type, requires_cancel, payment_method_type, purchase_token)
|
||||
SELECT
|
||||
_id,
|
||||
subscriber_id,
|
||||
CASE
|
||||
WHEN type = $DONOR_TYPE THEN currency_code
|
||||
ELSE ''
|
||||
END,
|
||||
type,
|
||||
requires_cancel,
|
||||
payment_method_type,
|
||||
CASE
|
||||
WHEN type = $BACKUP_TYPE THEN "-"
|
||||
ELSE NULL
|
||||
END
|
||||
FROM in_app_payment_subscriber
|
||||
""".trimIndent()
|
||||
)
|
||||
|
||||
db.execSQL("DROP TABLE in_app_payment_subscriber")
|
||||
|
||||
db.execSQL("ALTER TABLE in_app_payment_subscriber_tmp RENAME TO in_app_payment_subscriber")
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.database.model
|
|||
|
||||
import org.signal.donations.InAppPaymentType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
|
||||
import java.util.Currency
|
||||
import java.util.concurrent.locks.Lock
|
||||
|
@ -18,10 +19,11 @@ import java.util.concurrent.locks.ReentrantLock
|
|||
*/
|
||||
data class InAppPaymentSubscriberRecord(
|
||||
val subscriberId: SubscriberId,
|
||||
val currency: Currency,
|
||||
val type: Type,
|
||||
val requiresCancel: Boolean,
|
||||
val paymentMethodType: InAppPaymentData.PaymentMethodType
|
||||
val paymentMethodType: InAppPaymentData.PaymentMethodType,
|
||||
val currency: Currency?,
|
||||
val iapSubscriptionId: IAPSubscriptionId?
|
||||
) {
|
||||
/**
|
||||
* Serves as the mutex by which to perform mutations to subscriptions.
|
||||
|
|
|
@ -248,7 +248,7 @@ class InAppPaymentAuthCheckJob private constructor(parameters: Parameters) : Bas
|
|||
val updateLevelResponse = AppDependencies.donationsService.updateSubscriptionLevel(
|
||||
subscriber.subscriberId,
|
||||
level,
|
||||
subscriber.currency.currencyCode,
|
||||
subscriber.currency!!.currencyCode,
|
||||
updateOperation.idempotencyKey.serialize(),
|
||||
subscriber.type.lock
|
||||
)
|
||||
|
|
|
@ -244,7 +244,7 @@ class InAppPaymentKeepAliveJob private constructor(
|
|||
paymentMethodType = subscriber.paymentMethodType,
|
||||
badge = badge,
|
||||
amount = FiatValue(
|
||||
currencyCode = subscriber.currency.currencyCode,
|
||||
currencyCode = subscription.currency,
|
||||
amount = subscription.amount.toDecimalValue()
|
||||
),
|
||||
error = null,
|
||||
|
|
|
@ -309,15 +309,25 @@ class InAppPaymentRecurringContextJob private constructor(
|
|||
}
|
||||
|
||||
private fun handlePaymentFailure(inAppPayment: InAppPaymentTable.InAppPayment, subscription: Subscription, chargeFailure: ChargeFailure?) {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = inAppPayment.subscriberId!!,
|
||||
currency = Currency.getInstance(inAppPayment.data.amount!!.currencyCode),
|
||||
type = inAppPayment.type.requireSubscriberType(),
|
||||
requiresCancel = true,
|
||||
paymentMethodType = inAppPayment.data.paymentMethodType
|
||||
val record = SignalDatabase.inAppPaymentSubscribers.getBySubscriberId(subscriberId = inAppPayment.subscriberId!!)
|
||||
if (record == null) {
|
||||
warning("Could not find subscriber record in local database. Building from payment data.")
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriberId = inAppPayment.subscriberId,
|
||||
currency = if (inAppPayment.type == InAppPaymentType.RECURRING_BACKUP) null else Currency.getInstance(inAppPayment.data.amount!!.currencyCode),
|
||||
type = inAppPayment.type.requireSubscriberType(),
|
||||
requiresCancel = true,
|
||||
paymentMethodType = inAppPayment.data.paymentMethodType,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
info("Marking requiresCancel as true in subscriber record due to payment failure.")
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
record.copy(requiresCancel = true)
|
||||
)
|
||||
}
|
||||
|
||||
if (inAppPayment.data.redemption?.keepAlive == true) {
|
||||
info("Cancellation occurred during keep-alive. Setting cancellation state.")
|
||||
|
|
|
@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.migrations.DeleteDeprecatedLogsMigrationJob;
|
|||
import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.EmojiDownloadMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.EmojiSearchIndexCheckMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.GooglePlayBillingPurchaseTokenMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.IdentityTableCleanupMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.migrations.MigrationCompleteJob;
|
||||
|
@ -263,68 +264,69 @@ public final class JobManagerFactories {
|
|||
put(UploadAttachmentToArchiveJob.KEY, new UploadAttachmentToArchiveJob.Factory());
|
||||
|
||||
// Migrations
|
||||
put(AccountConsistencyMigrationJob.KEY, new AccountConsistencyMigrationJob.Factory());
|
||||
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
|
||||
put(AepMigrationJob.KEY, new AepMigrationJob.Factory());
|
||||
put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory());
|
||||
put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory());
|
||||
put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory());
|
||||
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
||||
put(BackfillDigestsMigrationJob.KEY, new BackfillDigestsMigrationJob.Factory());
|
||||
put(BackfillDigestsForDuplicatesMigrationJob.KEY, new BackfillDigestsForDuplicatesMigrationJob.Factory());
|
||||
put(BackupJitterMigrationJob.KEY, new BackupJitterMigrationJob.Factory());
|
||||
put(BackupMediaSnapshotSyncJob.KEY, new BackupMediaSnapshotSyncJob.Factory());
|
||||
put(BackupNotificationMigrationJob.KEY, new BackupNotificationMigrationJob.Factory());
|
||||
put(BackupRefreshJob.KEY, new BackupRefreshJob.Factory());
|
||||
put(BadE164MigrationJob.KEY, new BadE164MigrationJob.Factory());
|
||||
put(BlobStorageLocationMigrationJob.KEY, new BlobStorageLocationMigrationJob.Factory());
|
||||
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
|
||||
put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory());
|
||||
put(ContactLinkRebuildMigrationJob.KEY, new ContactLinkRebuildMigrationJob.Factory());
|
||||
put(CopyUsernameToSignalStoreMigrationJob.KEY, new CopyUsernameToSignalStoreMigrationJob.Factory());
|
||||
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
|
||||
put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory());
|
||||
put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory());
|
||||
put(EmojiDownloadMigrationJob.KEY, new EmojiDownloadMigrationJob.Factory());
|
||||
put(EmojiSearchIndexCheckMigrationJob.KEY, new EmojiSearchIndexCheckMigrationJob.Factory());
|
||||
put(IdentityTableCleanupMigrationJob.KEY, new IdentityTableCleanupMigrationJob.Factory());
|
||||
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
|
||||
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
|
||||
put(OptimizeMessageSearchIndexMigrationJob.KEY, new OptimizeMessageSearchIndexMigrationJob.Factory());
|
||||
put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory());
|
||||
put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory());
|
||||
put(PniAccountInitializationMigrationJob.KEY, new PniAccountInitializationMigrationJob.Factory());
|
||||
put(PniMigrationJob.KEY, new PniMigrationJob.Factory());
|
||||
put(PnpLaunchMigrationJob.KEY, new PnpLaunchMigrationJob.Factory());
|
||||
put(PreKeysSyncMigrationJob.KEY, new PreKeysSyncMigrationJob.Factory());
|
||||
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
|
||||
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());
|
||||
put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory());
|
||||
put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.Factory());
|
||||
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
|
||||
put(SelfRegisteredStateMigrationJob.KEY, new SelfRegisteredStateMigrationJob.Factory());
|
||||
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
|
||||
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
|
||||
put(StickerDayByDayMigrationJob.KEY, new StickerDayByDayMigrationJob.Factory());
|
||||
put(StickerMyDailyLifeMigrationJob.KEY, new StickerMyDailyLifeMigrationJob.Factory());
|
||||
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
|
||||
put(StorageFixLocalUnknownMigrationJob.KEY, new StorageFixLocalUnknownMigrationJob.Factory());
|
||||
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
|
||||
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
|
||||
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
|
||||
put(SubscriberIdMigrationJob.KEY, new SubscriberIdMigrationJob.Factory());
|
||||
put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory());
|
||||
put(SyncCallLinksMigrationJob.KEY, new SyncCallLinksMigrationJob.Factory());
|
||||
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
||||
put(SyncKeysMigrationJob.KEY, new SyncKeysMigrationJob.Factory());
|
||||
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());
|
||||
put(UpdateSmsJobsMigrationJob.KEY, new UpdateSmsJobsMigrationJob.Factory());
|
||||
put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory());
|
||||
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
|
||||
put(WallpaperCleanupMigrationJob.KEY, new WallpaperCleanupMigrationJob.Factory());
|
||||
put(WallpaperStorageMigrationJob.KEY, new WallpaperStorageMigrationJob.Factory());
|
||||
put(AccountConsistencyMigrationJob.KEY, new AccountConsistencyMigrationJob.Factory());
|
||||
put(AccountRecordMigrationJob.KEY, new AccountRecordMigrationJob.Factory());
|
||||
put(AepMigrationJob.KEY, new AepMigrationJob.Factory());
|
||||
put(ApplyUnknownFieldsToSelfMigrationJob.KEY, new ApplyUnknownFieldsToSelfMigrationJob.Factory());
|
||||
put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory());
|
||||
put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory());
|
||||
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
|
||||
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
|
||||
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
|
||||
put(BackfillDigestsMigrationJob.KEY, new BackfillDigestsMigrationJob.Factory());
|
||||
put(BackfillDigestsForDuplicatesMigrationJob.KEY, new BackfillDigestsForDuplicatesMigrationJob.Factory());
|
||||
put(BackupJitterMigrationJob.KEY, new BackupJitterMigrationJob.Factory());
|
||||
put(BackupMediaSnapshotSyncJob.KEY, new BackupMediaSnapshotSyncJob.Factory());
|
||||
put(BackupNotificationMigrationJob.KEY, new BackupNotificationMigrationJob.Factory());
|
||||
put(BackupRefreshJob.KEY, new BackupRefreshJob.Factory());
|
||||
put(BadE164MigrationJob.KEY, new BadE164MigrationJob.Factory());
|
||||
put(BlobStorageLocationMigrationJob.KEY, new BlobStorageLocationMigrationJob.Factory());
|
||||
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
|
||||
put(ClearGlideCacheMigrationJob.KEY, new ClearGlideCacheMigrationJob.Factory());
|
||||
put(ContactLinkRebuildMigrationJob.KEY, new ContactLinkRebuildMigrationJob.Factory());
|
||||
put(CopyUsernameToSignalStoreMigrationJob.KEY, new CopyUsernameToSignalStoreMigrationJob.Factory());
|
||||
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
|
||||
put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory());
|
||||
put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory());
|
||||
put(EmojiDownloadMigrationJob.KEY, new EmojiDownloadMigrationJob.Factory());
|
||||
put(EmojiSearchIndexCheckMigrationJob.KEY, new EmojiSearchIndexCheckMigrationJob.Factory());
|
||||
put(GooglePlayBillingPurchaseTokenMigrationJob.KEY, new GooglePlayBillingPurchaseTokenMigrationJob.Factory());
|
||||
put(IdentityTableCleanupMigrationJob.KEY, new IdentityTableCleanupMigrationJob.Factory());
|
||||
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());
|
||||
put(MigrationCompleteJob.KEY, new MigrationCompleteJob.Factory());
|
||||
put(OptimizeMessageSearchIndexMigrationJob.KEY, new OptimizeMessageSearchIndexMigrationJob.Factory());
|
||||
put(PinOptOutMigration.KEY, new PinOptOutMigration.Factory());
|
||||
put(PinReminderMigrationJob.KEY, new PinReminderMigrationJob.Factory());
|
||||
put(PniAccountInitializationMigrationJob.KEY, new PniAccountInitializationMigrationJob.Factory());
|
||||
put(PniMigrationJob.KEY, new PniMigrationJob.Factory());
|
||||
put(PnpLaunchMigrationJob.KEY, new PnpLaunchMigrationJob.Factory());
|
||||
put(PreKeysSyncMigrationJob.KEY, new PreKeysSyncMigrationJob.Factory());
|
||||
put(ProfileMigrationJob.KEY, new ProfileMigrationJob.Factory());
|
||||
put(ProfileSharingUpdateMigrationJob.KEY, new ProfileSharingUpdateMigrationJob.Factory());
|
||||
put(RebuildMessageSearchIndexMigrationJob.KEY, new RebuildMessageSearchIndexMigrationJob.Factory());
|
||||
put(RecheckPaymentsMigrationJob.KEY, new RecheckPaymentsMigrationJob.Factory());
|
||||
put(RecipientSearchMigrationJob.KEY, new RecipientSearchMigrationJob.Factory());
|
||||
put(SelfRegisteredStateMigrationJob.KEY, new SelfRegisteredStateMigrationJob.Factory());
|
||||
put(StickerLaunchMigrationJob.KEY, new StickerLaunchMigrationJob.Factory());
|
||||
put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory());
|
||||
put(StickerDayByDayMigrationJob.KEY, new StickerDayByDayMigrationJob.Factory());
|
||||
put(StickerMyDailyLifeMigrationJob.KEY, new StickerMyDailyLifeMigrationJob.Factory());
|
||||
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
|
||||
put(StorageFixLocalUnknownMigrationJob.KEY, new StorageFixLocalUnknownMigrationJob.Factory());
|
||||
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
|
||||
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
|
||||
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
|
||||
put(SubscriberIdMigrationJob.KEY, new SubscriberIdMigrationJob.Factory());
|
||||
put(Svr2MirrorMigrationJob.KEY, new Svr2MirrorMigrationJob.Factory());
|
||||
put(SyncCallLinksMigrationJob.KEY, new SyncCallLinksMigrationJob.Factory());
|
||||
put(SyncDistributionListsMigrationJob.KEY, new SyncDistributionListsMigrationJob.Factory());
|
||||
put(SyncKeysMigrationJob.KEY, new SyncKeysMigrationJob.Factory());
|
||||
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());
|
||||
put(UpdateSmsJobsMigrationJob.KEY, new UpdateSmsJobsMigrationJob.Factory());
|
||||
put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory());
|
||||
put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory());
|
||||
put(WallpaperCleanupMigrationJob.KEY, new WallpaperCleanupMigrationJob.Factory());
|
||||
put(WallpaperStorageMigrationJob.KEY, new WallpaperStorageMigrationJob.Factory());
|
||||
|
||||
// Dead jobs
|
||||
put(FailingJob.KEY, new FailingJob.Factory());
|
||||
|
|
|
@ -45,7 +45,6 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
private val TAG = Log.tag(InAppPaymentValues::class.java)
|
||||
|
||||
private const val KEY_DONATION_SUBSCRIPTION_CURRENCY_CODE = "donation.currency.code"
|
||||
private const val KEY_BACKUPS_SUBSCRIPTION_CURRENCY_CODE = "donation.backups.currency.code"
|
||||
private const val KEY_CURRENCY_CODE_ONE_TIME = "donation.currency.code.boost"
|
||||
private const val KEY_SUBSCRIBER_ID_PREFIX = "donation.subscriber.id."
|
||||
private const val KEY_LAST_KEEP_ALIVE_LAUNCH = "donation.last.successful.ping"
|
||||
|
@ -164,12 +163,9 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
SUBSCRIPTION_PAYMENT_SOURCE_TYPE
|
||||
)
|
||||
|
||||
private val recurringDonationCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)) }
|
||||
private val recurringDonationCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getRecurringDonationCurrency()) }
|
||||
val observableRecurringDonationCurrency: Observable<Currency> by lazy { recurringDonationCurrencyPublisher }
|
||||
|
||||
private val recurringBackupCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.BACKUP)) }
|
||||
val observableRecurringBackupsCurrency: Observable<Currency> by lazy { recurringBackupCurrencyPublisher }
|
||||
|
||||
private val oneTimeCurrencyPublisher: Subject<Currency> by lazy { BehaviorSubject.createDefault(getOneTimeCurrency()) }
|
||||
val observableOneTimeCurrency: Observable<Currency> by lazy { oneTimeCurrencyPublisher }
|
||||
|
||||
|
@ -185,12 +181,8 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
}
|
||||
}
|
||||
|
||||
fun getSubscriptionCurrency(subscriberType: InAppPaymentSubscriberRecord.Type): Currency {
|
||||
val currencyCode = if (subscriberType == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
getString(KEY_DONATION_SUBSCRIPTION_CURRENCY_CODE, null)
|
||||
} else {
|
||||
getString(KEY_BACKUPS_SUBSCRIPTION_CURRENCY_CODE, null)
|
||||
}
|
||||
fun getRecurringDonationCurrency(): Currency {
|
||||
val currencyCode = getString(KEY_DONATION_SUBSCRIPTION_CURRENCY_CODE, null)
|
||||
|
||||
val currency: Currency? = if (currencyCode == null) {
|
||||
val localeCurrency = CurrencyUtil.getCurrencyByLocale(Locale.getDefault())
|
||||
|
@ -218,7 +210,7 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
fun getOneTimeCurrency(): Currency {
|
||||
val oneTimeCurrency = getString(KEY_CURRENCY_CODE_ONE_TIME, null)
|
||||
return if (oneTimeCurrency == null) {
|
||||
val currency = getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val currency = getRecurringDonationCurrency()
|
||||
setOneTimeCurrency(currency)
|
||||
currency
|
||||
} else {
|
||||
|
@ -247,29 +239,22 @@ class InAppPaymentValues internal constructor(store: KeyValueStore) : SignalStor
|
|||
null
|
||||
} else {
|
||||
InAppPaymentSubscriberRecord(
|
||||
SubscriberId.fromBytes(subscriberIdBytes),
|
||||
currency,
|
||||
InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
shouldCancelSubscriptionBeforeNextSubscribeAttempt,
|
||||
getSubscriptionPaymentSourceType().toPaymentMethodType()
|
||||
subscriberId = SubscriberId.fromBytes(subscriberIdBytes),
|
||||
currency = currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = shouldCancelSubscriptionBeforeNextSubscribeAttempt,
|
||||
paymentMethodType = getSubscriptionPaymentSourceType().toPaymentMethodType(),
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSubscriberCurrency(currency: Currency, type: InAppPaymentSubscriberRecord.Type) {
|
||||
if (type == InAppPaymentSubscriberRecord.Type.DONATION) {
|
||||
store.beginWrite()
|
||||
.putString(KEY_DONATION_SUBSCRIPTION_CURRENCY_CODE, currency.currencyCode)
|
||||
.apply()
|
||||
fun setRecurringDonationCurrency(currency: Currency) {
|
||||
store.beginWrite()
|
||||
.putString(KEY_DONATION_SUBSCRIPTION_CURRENCY_CODE, currency.currencyCode)
|
||||
.apply()
|
||||
|
||||
recurringDonationCurrencyPublisher.onNext(currency)
|
||||
} else {
|
||||
store.beginWrite()
|
||||
.putString(KEY_BACKUPS_SUBSCRIPTION_CURRENCY_CODE, currency.currencyCode)
|
||||
.apply()
|
||||
|
||||
recurringBackupCurrencyPublisher.onNext(currency)
|
||||
}
|
||||
recurringDonationCurrencyPublisher.onNext(currency)
|
||||
}
|
||||
|
||||
fun getLevelOperation(level: String): LevelUpdateOperation? {
|
||||
|
|
|
@ -165,9 +165,10 @@ public class ApplicationMigrations {
|
|||
static final int EMOJI_SEARCH_INDEX_CHECK_2 = 121;
|
||||
static final int QUOTE_AUTHOR_FIX = 122;
|
||||
static final int BAD_E164_FIX = 123;
|
||||
static final int GPB_TOKEN_MIGRATION = 124;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 123;
|
||||
public static final int CURRENT_VERSION = 124;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
|
@ -758,6 +759,10 @@ public class ApplicationMigrations {
|
|||
jobs.put(Version.BAD_E164_FIX, new BadE164MigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.GPB_TOKEN_MIGRATION) {
|
||||
jobs.put(Version.GPB_TOKEN_MIGRATION, new GooglePlayBillingPurchaseTokenMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.migrations
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okio.IOException
|
||||
import org.signal.core.util.billing.BillingPurchaseResult
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
|
||||
/**
|
||||
* When we migrate subscriptions, purchase tokens are stored as '-' string. This migration
|
||||
* goes in and updates that purchase token with the real value from the latest subscription, if
|
||||
* available.
|
||||
*/
|
||||
internal class GooglePlayBillingPurchaseTokenMigrationJob private constructor(
|
||||
parameters: Parameters
|
||||
) : MigrationJob(parameters) {
|
||||
|
||||
constructor() : this(
|
||||
Parameters.Builder()
|
||||
.addConstraint(NetworkConstraint.KEY)
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(GooglePlayBillingPurchaseTokenMigrationJob::class)
|
||||
|
||||
const val KEY = "GooglePlayBillingPurchaseTokenMigrationJob"
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String = KEY
|
||||
|
||||
override fun isUiBlocking(): Boolean = false
|
||||
|
||||
override fun performMigration() {
|
||||
if (!SignalStore.account.isRegistered) {
|
||||
return
|
||||
}
|
||||
|
||||
val backupSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP) ?: return
|
||||
|
||||
if (backupSubscriber.iapSubscriptionId?.purchaseToken == "-") {
|
||||
val purchaseResult: BillingPurchaseResult.Success? = runBlocking {
|
||||
if (AppDependencies.billingApi.isApiAvailable()) {
|
||||
val purchase = AppDependencies.billingApi.queryPurchases()
|
||||
|
||||
if (purchase is BillingPurchaseResult.Success) {
|
||||
Log.d(TAG, "Successfully found purchase result.")
|
||||
purchase
|
||||
} else {
|
||||
Log.d(TAG, "No purchase was available.")
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Billing API is not available.")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (purchaseResult == null) {
|
||||
return
|
||||
}
|
||||
|
||||
InAppPaymentsRepository.setSubscriber(
|
||||
backupSubscriber.copy(
|
||||
iapSubscriptionId = IAPSubscriptionId.GooglePlayBillingPurchaseToken(purchaseToken = purchaseResult.purchaseToken)
|
||||
)
|
||||
)
|
||||
|
||||
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
|
||||
StorageSyncHelper.scheduleSyncForDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldRetry(e: Exception): Boolean {
|
||||
Log.w(TAG, "Checking retry state for exception.", e)
|
||||
return e is IOException
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<GooglePlayBillingPurchaseTokenMigrationJob> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): GooglePlayBillingPurchaseTokenMigrationJob {
|
||||
return GooglePlayBillingPurchaseTokenMigrationJob(parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,11 +36,12 @@ internal class SubscriberIdMigrationJob(
|
|||
if (subscriber != null) {
|
||||
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(
|
||||
InAppPaymentSubscriberRecord(
|
||||
subscriber.subscriberId,
|
||||
subscriber.currency,
|
||||
InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt,
|
||||
SignalStore.inAppPayments.getSubscriptionPaymentSourceType().toPaymentMethodType()
|
||||
subscriberId = subscriber.subscriberId,
|
||||
currency = subscriber.currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt,
|
||||
paymentMethodType = SignalStore.inAppPayments.getSubscriptionPaymentSourceType().toPaymentMethodType(),
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.signal.core.util.nullIfEmpty
|
|||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.storage.StorageSyncHelper.applyAccountStorageSyncUpdates
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
|
||||
import org.whispersystems.signalservice.api.storage.StorageId
|
||||
import org.whispersystems.signalservice.api.storage.safeSetBackupsSubscriber
|
||||
|
@ -88,14 +89,15 @@ class AccountRecordProcessor(
|
|||
}
|
||||
|
||||
val backupsSubscriberId: ByteString
|
||||
val backupsSubscriberCurrencyCode: String
|
||||
val backupsPurchaseToken: IAPSubscriptionId?
|
||||
|
||||
if (remote.proto.backupsSubscriberId.isNotEmpty()) {
|
||||
backupsSubscriberId = remote.proto.backupsSubscriberId
|
||||
backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode
|
||||
val remoteBackupSubscriberData = remote.proto.backupSubscriberData
|
||||
if (remoteBackupSubscriberData != null && remoteBackupSubscriberData.subscriberId.isNotEmpty()) {
|
||||
backupsSubscriberId = remoteBackupSubscriberData.subscriberId
|
||||
backupsPurchaseToken = IAPSubscriptionId.from(remoteBackupSubscriberData)
|
||||
} else {
|
||||
backupsSubscriberId = local.proto.backupsSubscriberId
|
||||
backupsSubscriberCurrencyCode = remote.proto.backupsSubscriberCurrencyCode
|
||||
backupsSubscriberId = local.proto.backupSubscriberData?.subscriberId ?: ByteString.EMPTY
|
||||
backupsPurchaseToken = IAPSubscriptionId.from(local.proto.backupSubscriberData)
|
||||
}
|
||||
|
||||
val storyViewReceiptsState = if (remote.proto.storyViewReceiptsEnabled == OptionalBool.UNSET) {
|
||||
|
@ -139,7 +141,7 @@ class AccountRecordProcessor(
|
|||
|
||||
safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray())
|
||||
safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode)
|
||||
safeSetBackupsSubscriber(backupsSubscriberId, backupsSubscriberCurrencyCode)
|
||||
safeSetBackupsSubscriber(backupsSubscriberId, backupsPurchaseToken)
|
||||
}.toSignalAccountRecord(StorageId.forAccount(keyGenerator.generate()))
|
||||
|
||||
return if (doParamsMatch(remote, merged)) {
|
||||
|
|
|
@ -172,11 +172,11 @@ object StorageSyncHelper {
|
|||
}
|
||||
|
||||
getSubscriber(InAppPaymentSubscriberRecord.Type.DONATION)?.let {
|
||||
safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
|
||||
safeSetSubscriber(it.subscriberId.bytes.toByteString(), it.currency?.currencyCode ?: "")
|
||||
}
|
||||
|
||||
getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)?.let {
|
||||
safeSetBackupsSubscriber(it.subscriberId.bytes.toByteString(), it.currency.currencyCode)
|
||||
safeSetBackupsSubscriber(it.subscriberId.bytes.toByteString(), it.iapSubscriptionId)
|
||||
}
|
||||
|
||||
safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null))
|
||||
|
@ -219,11 +219,16 @@ object StorageSyncHelper {
|
|||
SignalStore.story.viewedReceiptsEnabled = update.new.proto.storyViewReceiptsEnabled == OptionalBool.ENABLED
|
||||
}
|
||||
|
||||
val remoteSubscriber = StorageSyncModels.remoteToLocalSubscriber(update.new.proto.subscriberId, update.new.proto.subscriberCurrencyCode, InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
val remoteSubscriber = StorageSyncModels.remoteToLocalDonorSubscriber(update.new.proto.subscriberId, update.new.proto.subscriberCurrencyCode)
|
||||
if (remoteSubscriber != null) {
|
||||
setSubscriber(remoteSubscriber)
|
||||
}
|
||||
|
||||
val remoteBackupsSubscriber = StorageSyncModels.remoteToLocalBackupSubscriber(update.new.proto.backupSubscriberData)
|
||||
if (remoteBackupsSubscriber != null) {
|
||||
setSubscriber(remoteBackupsSubscriber)
|
||||
}
|
||||
|
||||
if (update.new.proto.subscriptionManuallyCancelled && !update.old.proto.subscriptionManuallyCancelled) {
|
||||
SignalStore.inAppPayments.updateLocalStateForManualCancellation(InAppPaymentSubscriberRecord.Type.DONATION)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.storage
|
|||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.isNotEmpty
|
||||
import org.signal.core.util.isNullOrEmpty
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
|
||||
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
|
||||
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState
|
||||
|
@ -18,6 +19,7 @@ import org.thoughtcrime.securesms.database.model.RecipientRecord
|
|||
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
|
||||
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
|
||||
import org.whispersystems.signalservice.api.storage.SignalCallLinkRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalContactRecord
|
||||
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
|
||||
|
@ -283,10 +285,32 @@ object StorageSyncModels {
|
|||
}
|
||||
}
|
||||
|
||||
fun remoteToLocalSubscriber(
|
||||
fun remoteToLocalBackupSubscriber(
|
||||
iapData: AccountRecord.IAPSubscriberData?
|
||||
): InAppPaymentSubscriberRecord? {
|
||||
if (iapData == null || iapData.subscriberId.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val subscriberId = SubscriberId.fromBytes(iapData.subscriberId.toByteArray())
|
||||
val localSubscriberRecord = inAppPaymentSubscribers.getBySubscriberId(subscriberId)
|
||||
val requiresCancel = localSubscriberRecord != null && localSubscriberRecord.requiresCancel
|
||||
val paymentMethodType = localSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.GOOGLE_PLAY_BILLING
|
||||
val iapSubscriptionId = IAPSubscriptionId.from(iapData) ?: return null
|
||||
|
||||
return InAppPaymentSubscriberRecord(
|
||||
subscriberId = subscriberId,
|
||||
currency = null,
|
||||
type = InAppPaymentSubscriberRecord.Type.BACKUP,
|
||||
requiresCancel = requiresCancel,
|
||||
paymentMethodType = paymentMethodType,
|
||||
iapSubscriptionId = iapSubscriptionId
|
||||
)
|
||||
}
|
||||
|
||||
fun remoteToLocalDonorSubscriber(
|
||||
subscriberId: ByteString,
|
||||
subscriberCurrencyCode: String,
|
||||
type: InAppPaymentSubscriberRecord.Type
|
||||
subscriberCurrencyCode: String
|
||||
): InAppPaymentSubscriberRecord? {
|
||||
if (subscriberId.isNotEmpty()) {
|
||||
val subscriberId = SubscriberId.fromBytes(subscriberId.toByteArray())
|
||||
|
@ -305,7 +329,14 @@ object StorageSyncModels {
|
|||
}
|
||||
}
|
||||
|
||||
return InAppPaymentSubscriberRecord(subscriberId, currency, type, requiresCancel, paymentMethodType)
|
||||
return InAppPaymentSubscriberRecord(
|
||||
subscriberId = subscriberId,
|
||||
currency = currency,
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = requiresCancel,
|
||||
paymentMethodType = paymentMethodType,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -81,7 +81,7 @@ class RecurringInAppPaymentRepositoryTest {
|
|||
|
||||
mockkObject(SignalStore.Companion)
|
||||
every { SignalStore.Companion.inAppPayments } returns mockk {
|
||||
every { SignalStore.Companion.inAppPayments.getSubscriptionCurrency(any()) } returns Currency.getInstance("USD")
|
||||
every { SignalStore.Companion.inAppPayments.getRecurringDonationCurrency() } returns Currency.getInstance("USD")
|
||||
every { SignalStore.Companion.inAppPayments.updateLocalStateForManualCancellation(any()) } returns Unit
|
||||
every { SignalStore.Companion.inAppPayments.updateLocalStateForLocalSubscribe(any()) } returns Unit
|
||||
}
|
||||
|
@ -285,7 +285,8 @@ class RecurringInAppPaymentRepositoryTest {
|
|||
currency = Currency.getInstance("USD"),
|
||||
type = InAppPaymentSubscriberRecord.Type.DONATION,
|
||||
requiresCancel = false,
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
|
||||
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD,
|
||||
iapSubscriptionId = null
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,11 @@ package org.whispersystems.signalservice.api.storage
|
|||
import okio.ByteString
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.signal.core.util.isNotEmpty
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.whispersystems.signalservice.api.payments.PaymentsConstants
|
||||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress
|
||||
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId.Companion.isNotNullOrBlank
|
||||
import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
import org.whispersystems.signalservice.internal.storage.protos.Payments
|
||||
|
@ -41,13 +43,15 @@ fun AccountRecord.Builder.safeSetSubscriber(subscriberId: ByteString, subscriber
|
|||
return this
|
||||
}
|
||||
|
||||
fun AccountRecord.Builder.safeSetBackupsSubscriber(subscriberId: ByteString, subscriberCurrencyCode: String): AccountRecord.Builder {
|
||||
if (subscriberId.isNotEmpty() && subscriberId.size == 32 && subscriberCurrencyCode.isNotBlank()) {
|
||||
this.backupsSubscriberId = subscriberId
|
||||
this.backupsSubscriberCurrencyCode = subscriberCurrencyCode
|
||||
fun AccountRecord.Builder.safeSetBackupsSubscriber(subscriberId: ByteString, iapSubscriptionId: IAPSubscriptionId?): AccountRecord.Builder {
|
||||
if (subscriberId.isNotEmpty() && subscriberId.size == 32 && iapSubscriptionId.isNotNullOrBlank()) {
|
||||
this.backupSubscriberData = AccountRecord.IAPSubscriberData(
|
||||
subscriberId = subscriberId,
|
||||
purchaseToken = iapSubscriptionId.purchaseToken,
|
||||
originalTransactionId = iapSubscriptionId.originalTransactionId
|
||||
)
|
||||
} else {
|
||||
this.backupsSubscriberId = defaultAccountRecord.backupsSubscriberId
|
||||
this.backupsSubscriberCurrencyCode = defaultAccountRecord.backupsSubscriberCurrencyCode
|
||||
this.backupSubscriberData = defaultAccountRecord.backupSubscriberData
|
||||
}
|
||||
|
||||
return this
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.signalservice.api.storage
|
||||
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
import kotlin.contracts.contract
|
||||
|
||||
/**
|
||||
* Represents an In-app purchase subscription id, whose form depends on what platform the subscription originated on.
|
||||
*/
|
||||
sealed class IAPSubscriptionId(open val purchaseToken: String?, open val originalTransactionId: Long?) {
|
||||
/**
|
||||
* Represents a Google Play Billing subscription, identified by the purchase token.
|
||||
*/
|
||||
data class GooglePlayBillingPurchaseToken(override val purchaseToken: String) : IAPSubscriptionId(purchaseToken, null)
|
||||
|
||||
/**
|
||||
* Represents an Apple IAP subscription, identified by the original transaction id.
|
||||
*/
|
||||
data class AppleIAPOriginalTransactionId(override val originalTransactionId: Long) : IAPSubscriptionId(null, originalTransactionId)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Checks the given proto for valid IAP subscription data and creates an ID for it.
|
||||
* If there is no valid data, we return null.
|
||||
*/
|
||||
fun from(proto: AccountRecord.IAPSubscriberData?): IAPSubscriptionId? {
|
||||
return if (proto == null) {
|
||||
null
|
||||
} else if (proto.purchaseToken.isNotNullOrBlank()) {
|
||||
GooglePlayBillingPurchaseToken(proto.purchaseToken)
|
||||
} else if (proto.originalTransactionId != null) {
|
||||
AppleIAPOriginalTransactionId(proto.originalTransactionId)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalContracts::class)
|
||||
fun IAPSubscriptionId?.isNotNullOrBlank(): Boolean {
|
||||
contract {
|
||||
returns(true) implies (this@isNotNullOrBlank != null)
|
||||
}
|
||||
|
||||
return this != null && (purchaseToken.isNotNullOrBlank() || originalTransactionId != null)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -185,43 +185,58 @@ message AccountRecord {
|
|||
Color color = 3;
|
||||
}
|
||||
|
||||
bytes profileKey = 1;
|
||||
string givenName = 2;
|
||||
string familyName = 3;
|
||||
string avatarUrlPath = 4;
|
||||
bool noteToSelfArchived = 5;
|
||||
bool readReceipts = 6;
|
||||
bool sealedSenderIndicators = 7;
|
||||
bool typingIndicators = 8;
|
||||
reserved /* proxiedLinkPreviews */ 9;
|
||||
bool noteToSelfMarkedUnread = 10;
|
||||
bool linkPreviews = 11;
|
||||
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||
bool unlistedPhoneNumber = 13;
|
||||
repeated PinnedConversation pinnedConversations = 14;
|
||||
bool preferContactAvatars = 15;
|
||||
Payments payments = 16;
|
||||
uint32 universalExpireTimer = 17;
|
||||
bool primarySendsSms = 18;
|
||||
string e164 = 19;
|
||||
repeated string preferredReactionEmoji = 20;
|
||||
bytes subscriberId = 21;
|
||||
string subscriberCurrencyCode = 22;
|
||||
bool displayBadgesOnProfile = 23;
|
||||
bool subscriptionManuallyCancelled = 24;
|
||||
bool keepMutedChatsArchived = 25;
|
||||
bool hasSetMyStoriesPrivacy = 26;
|
||||
bool hasViewedOnboardingStory = 27;
|
||||
reserved /* storiesDisabled */ 28;
|
||||
bool storiesDisabled = 29;
|
||||
OptionalBool storyViewReceiptsEnabled = 30;
|
||||
reserved /* hasReadOnboardingStory */ 31;
|
||||
bool hasSeenGroupStoryEducationSheet = 32;
|
||||
string username = 33;
|
||||
bool hasCompletedUsernameOnboarding = 34;
|
||||
UsernameLink usernameLink = 35;
|
||||
bytes backupsSubscriberId = 36;
|
||||
string backupsSubscriberCurrencyCode = 37;
|
||||
message IAPSubscriberData {
|
||||
bytes subscriberId = 1;
|
||||
|
||||
oneof iapSubscriptionId {
|
||||
// Identifies an Android Play Store IAP subscription.
|
||||
string purchaseToken = 2;
|
||||
// Identifies an iOS App Store IAP subscription.
|
||||
uint64 originalTransactionId = 3;
|
||||
}
|
||||
}
|
||||
|
||||
bytes profileKey = 1;
|
||||
string givenName = 2;
|
||||
string familyName = 3;
|
||||
string avatarUrlPath = 4;
|
||||
bool noteToSelfArchived = 5;
|
||||
bool readReceipts = 6;
|
||||
bool sealedSenderIndicators = 7;
|
||||
bool typingIndicators = 8;
|
||||
reserved /* proxiedLinkPreviews */ 9;
|
||||
bool noteToSelfMarkedUnread = 10;
|
||||
bool linkPreviews = 11;
|
||||
PhoneNumberSharingMode phoneNumberSharingMode = 12;
|
||||
bool unlistedPhoneNumber = 13;
|
||||
repeated PinnedConversation pinnedConversations = 14;
|
||||
bool preferContactAvatars = 15;
|
||||
Payments payments = 16;
|
||||
uint32 universalExpireTimer = 17;
|
||||
bool primarySendsSms = 18;
|
||||
string e164 = 19;
|
||||
repeated string preferredReactionEmoji = 20;
|
||||
bytes subscriberId = 21;
|
||||
string subscriberCurrencyCode = 22;
|
||||
bool displayBadgesOnProfile = 23;
|
||||
bool subscriptionManuallyCancelled = 24;
|
||||
bool keepMutedChatsArchived = 25;
|
||||
bool hasSetMyStoriesPrivacy = 26;
|
||||
bool hasViewedOnboardingStory = 27;
|
||||
reserved /* storiesDisabled */ 28;
|
||||
bool storiesDisabled = 29;
|
||||
OptionalBool storyViewReceiptsEnabled = 30;
|
||||
reserved /* hasReadOnboardingStory */ 31;
|
||||
bool hasSeenGroupStoryEducationSheet = 32;
|
||||
string username = 33;
|
||||
bool hasCompletedUsernameOnboarding = 34;
|
||||
UsernameLink usernameLink = 35;
|
||||
reserved /* backupsSubscriberId */ 36;
|
||||
reserved /* backupsSubscriberCurrencyCode */ 37;
|
||||
reserved /* backupsSubscriptionManuallyCancelled */ 38;
|
||||
optional bool hasBackup = 39; // Set to true after backups are enabled and one is uploaded.
|
||||
optional uint64 backupTier = 40; // See zkgroup for integer particular values
|
||||
IAPSubscriberData backupSubscriberData = 41;
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
|
|
Loading…
Add table
Reference in a new issue