mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-25 12:17:22 +00:00
Implement initial support for IAP data.
This commit is contained in:
committed by
Greyson Parrelli
parent
f537fa6436
commit
f2b4bd0585
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user