Implement initial support for IAP data.

This commit is contained in:
Alex Hart
2024-12-19 16:34:29 -04:00
committed by Greyson Parrelli
parent f537fa6436
commit f2b4bd0585
35 changed files with 957 additions and 241 deletions

View File

@@ -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)
}

View File

@@ -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 = {},

View File

@@ -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(),

View File

@@ -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

View File

@@ -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)
}
/**

View File

@@ -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
)

View File

@@ -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
)
)
}

View File

@@ -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,

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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> {

View File

@@ -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) {

View File

@@ -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")
}
}

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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,

View File

@@ -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.")

View File

@@ -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());

View File

@@ -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? {

View File

@@ -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;
}

View File

@@ -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)
}
}
}

View File

@@ -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
)
)
}

View File

@@ -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)) {

View File

@@ -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)
}

View File

@@ -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
}