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

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