Add donation checkout entry-point tests.

This commit is contained in:
Alex Hart
2026-07-15 16:43:49 -04:00
committed by Greyson Parrelli
parent 4ee2b71b13
commit 88366cd416
8 changed files with 649 additions and 2 deletions
+1
View File
@@ -842,6 +842,7 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4)
androidTestImplementation(libs.androidx.compose.ui.test.manifest)
androidTestImplementation(testLibs.androidx.test.ext.junit)
androidTestImplementation(testLibs.espresso.core)
androidTestImplementation(testLibs.espresso.contrib) {
@@ -0,0 +1,191 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.scrollTo
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.getParcelableExtraCompat
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.self.none.BecomeASustainerFragment
import org.thoughtcrime.securesms.badges.self.overview.BadgesOverviewFragment
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import java.util.concurrent.TimeUnit
/**
* Entry-path coverage for the "become a subscriber" upsell.
*
* The upsell surface is [BecomeASustainerFragment], reached from the profile screen's badges row. The
* handoff test asserts the sheet's button launches the recurring-donation checkout route; the gating
* tests assert the "user might be a sustainer" rule in [org.thoughtcrime.securesms.profiles.manage.EditProfileFragment]:
* a non-donor sees the upsell, an existing donor is routed to badge management instead.
*/
@RunWith(AndroidJUnit4::class)
class BecomeASustainerUpsellTest {
@get:Rule
val harness = SignalActivityRule()
@get:Rule
val iapRule = InAppPaymentsRule()
private val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext
@Before
fun setUp() {
setSelfBadges(emptyList())
}
@Test
fun nonDonor_badgeTap_showsUpsellSheet() {
ActivityScenario.launch(EditProfileActivity::class.java).use { scenario ->
onView(withId(R.id.manage_profile_badges_container)).perform(scrollTo(), click())
val sheet = awaitNavHostFragment(scenario) { it.filterIsInstance<BecomeASustainerFragment>().firstOrNull() }
assertThat(sheet).isNotNull()
}
}
@Test
fun becomeASustainer_launchesRecurringCheckoutRoute() {
LaunchedActivityRecorder(AppSettingsActivity::class.java).use { recorder ->
ActivityScenario.launch(EditProfileActivity::class.java).use { scenario ->
onView(withId(R.id.manage_profile_badges_container)).perform(scrollTo(), click())
awaitNavHostFragment(scenario) { it.filterIsInstance<BecomeASustainerFragment>().firstOrNull() }
val becomeASustainer = withText(R.string.BecomeASustainerMegaphone__become_a_sustainer)
onView(withId(R.id.recycler)).perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(becomeASustainer)))
onView(becomeASustainer).perform(click())
val intent = recorder.awaitLaunch()
val route = intent.extras!!.keySet().firstNotNullOfOrNull { intent.getParcelableExtraCompat(it, AppSettingsRoute::class.java) }
assertThat(route).isEqualTo(AppSettingsRoute.DonationsRoute.Donations(directToCheckoutType = InAppPaymentType.RECURRING_DONATION))
}
}
}
@Test
fun donor_badgeTap_opensBadgeManagement() {
setSelfBadges(listOf(donorBadge()))
ActivityScenario.launch(EditProfileActivity::class.java).use { scenario ->
onView(withId(R.id.manage_profile_badges_container)).perform(scrollTo(), click())
val badgeManagement = awaitNavHostFragment(scenario) { it.filterIsInstance<BadgesOverviewFragment>().firstOrNull() }
assertThat(badgeManagement).isNotNull()
val upsell = navHostFragments(scenario).filterIsInstance<BecomeASustainerFragment>().firstOrNull()
assertThat(upsell).isNull()
}
}
private fun awaitNavHostFragment(scenario: ActivityScenario<EditProfileActivity>, selector: (List<Fragment>) -> Fragment?): Fragment {
return await(description = "nav host fragment") { selector(navHostFragments(scenario)) }
}
private fun navHostFragments(scenario: ActivityScenario<EditProfileActivity>): List<Fragment> {
var fragments: List<Fragment> = emptyList()
scenario.onActivity { activity ->
val navHost = activity.supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as? NavHostFragment
fragments = navHost?.childFragmentManager?.fragments ?: emptyList()
}
return fragments
}
private fun setSelfBadges(badges: List<Badge>) {
SignalDatabase.recipients.setBadges(Recipient.self().id, badges)
Recipient.self().fresh()
}
private fun donorBadge(): Badge {
return Badge(
id = "test-donor-badge",
category = Badge.Category.Donor,
name = "Signal Sustainer",
description = "",
imageUrl = Uri.EMPTY,
imageDensity = "xxhdpi",
expirationTimestamp = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30),
visible = true,
duration = TimeUnit.DAYS.toMillis(30)
)
}
private fun <T> await(timeoutMs: Long = 10_000, pollMs: Long = 50, description: String, supplier: () -> T?): T {
val deadline = System.currentTimeMillis() + timeoutMs
while (System.currentTimeMillis() < deadline) {
supplier()?.let { return it }
Thread.sleep(pollMs)
}
error("Timed out after ${timeoutMs}ms waiting for $description")
}
/**
* Captures the first launched activity of [activityClass] and finishes it immediately so its own
* downstream launches (e.g. the auto-launched checkout) don't cascade during the test.
*/
private inner class LaunchedActivityRecorder(private val activityClass: Class<out Activity>) : AutoCloseable {
@Volatile
private var launched: Intent? = null
private val app = context.applicationContext as Application
private val callbacks = object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
if (activityClass.isInstance(activity) && launched == null) {
launched = activity.intent
activity.finish()
}
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityResumed(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
}
init {
app.registerActivityLifecycleCallbacks(callbacks)
}
fun awaitLaunch(): Intent = await(description = "${activityClass.simpleName} launch") { launched }
override fun close() = app.unregisterActivityLifecycleCallbacks(callbacks)
}
}
@@ -0,0 +1,200 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.v2
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import androidx.fragment.app.FragmentActivity
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isInstanceOf
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.getSerializableCompat
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.DonateToSignalFragment
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
/**
* Entry-path coverage for the inline chat donation prompt: the Release-Channel "donation request"
* conversation item that renders a "Donate" action button and opens the one-time checkout.
*
* The conversation is hosted by [MainActivity], so this drives it the same way
* [org.thoughtcrime.securesms.main.MainNavigationLaunchTest] does — a manual lifecycle-callback launch
* plus direct view interaction, since ActivityScenario/Espresso misbehave for the conversation
* custom-action intent.
*
* Unlike the megaphone and subscriber-upsell paths, this prompt is intentionally NOT gated by sustainer
* status; [sustainer_donatePromptStillShown] documents that.
*/
@RunWith(AndroidJUnit4::class)
class InlineDonationPromptTest {
@get:Rule
val harness = SignalActivityRule()
@get:Rule
val iapRule = InAppPaymentsRule()
private val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext
@Before
fun setUp() {
SignalStore.inAppPayments.setLastEndOfPeriod(0L)
SignalDatabase.recipients.setBadges(Recipient.self().id, emptyList())
Recipient.self().fresh()
}
@Test
fun nonSustainer_donatePromptOpensOneTimeCheckout() {
seedDonationRequest()
assertDonatePromptOpensCheckout()
}
@Test
fun sustainer_donatePromptStillShown() {
SignalStore.inAppPayments.setLastEndOfPeriod(TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()) + TimeUnit.DAYS.toSeconds(30))
SignalDatabase.recipients.setBadges(Recipient.self().id, listOf(donorBadge()))
Recipient.self().fresh()
seedDonationRequest()
assertDonatePromptOpensCheckout()
}
private fun assertDonatePromptOpensCheckout() {
val activity = launchConversation()
val donateButton = awaitView(activity, R.id.conversation_update_action)
runOnMainSync { donateButton.performClick() }
val dialog = await(description = "one-time checkout dialog") {
activity.supportFragmentManager.findFragmentByTag(ONE_TIME_NAV_TAG)
}
assertThat(dialog).isInstanceOf(DonateToSignalFragment.Dialog::class)
val type = dialog.requireArguments().getSerializableCompat(DonateToSignalFragment.Dialog.ARG, InAppPaymentType::class.java)
assertThat(type).isEqualTo(InAppPaymentType.ONE_TIME_DONATION)
}
private fun seedDonationRequest() {
val recipientId: RecipientId = SignalDatabase.recipients.insertReleaseChannelRecipient()
SignalStore.releaseChannel.setReleaseChannelRecipientId(recipientId)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
SignalDatabase.messages.insertBoostRequestMessage(recipientId, threadId)
}
private fun launchConversation(): MainActivity {
val recipientId = SignalStore.releaseChannel.releaseChannelRecipientId!!
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
val conversationIntent = ConversationIntents.createBuilder(context, recipientId, threadId).blockingGet().build()
val launchIntent = Intent(context, MainActivity::class.java).apply {
action = ConversationIntents.ACTION
putExtras(conversationIntent)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val app = context.applicationContext as Application
val resumed = CountDownLatch(1)
val holder = arrayOfNulls<MainActivity>(1)
val callbacks = object : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) = Unit
// Capture on resume rather than create: a CLEAR_TOP|SINGLE_TOP conversation intent may be
// delivered to an existing MainActivity via onNewIntent, which never fires onActivityCreated.
override fun onActivityResumed(activity: Activity) {
if (activity is MainActivity) {
holder[0] = activity
resumed.countDown()
}
}
override fun onActivityStarted(activity: Activity) = Unit
override fun onActivityPaused(activity: Activity) = Unit
override fun onActivityStopped(activity: Activity) = Unit
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
override fun onActivityDestroyed(activity: Activity) = Unit
}
app.registerActivityLifecycleCallbacks(callbacks)
app.startActivity(launchIntent)
if (!resumed.await(15, TimeUnit.SECONDS)) {
app.unregisterActivityLifecycleCallbacks(callbacks)
error("MainActivity did not reach RESUMED within 15s")
}
app.unregisterActivityLifecycleCallbacks(callbacks)
return holder[0] ?: error("MainActivity was not captured")
}
private fun awaitView(activity: FragmentActivity, viewId: Int): View {
return await(description = "view $viewId") {
activity.findViewById<View>(viewId)?.takeIf { it.isShown }
}
}
private fun <T> await(
timeoutMs: Long = 15_000,
pollMs: Long = 50,
description: String,
supplier: () -> T?
): T {
val deadline = System.currentTimeMillis() + timeoutMs
while (System.currentTimeMillis() < deadline) {
val result = runOnMainSync(supplier)
if (result != null) return result
Thread.sleep(pollMs)
}
error("Timed out after ${timeoutMs}ms waiting for $description")
}
private fun <T> runOnMainSync(block: () -> T): T {
var result: Result<T> = Result.failure(IllegalStateException("runOnMainSync produced no result"))
InstrumentationRegistry.getInstrumentation().runOnMainSync { result = runCatching(block) }
return result.getOrThrow()
}
private fun donorBadge(): Badge {
return Badge(
id = "test-donor-badge",
category = Badge.Category.Donor,
name = "Signal Sustainer",
description = "",
imageUrl = Uri.EMPTY,
imageDensity = "xxhdpi",
expirationTimestamp = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30),
visible = true,
duration = TimeUnit.DAYS.toMillis(30)
)
}
companion object {
private const val ONE_TIME_NAV_TAG = "one_time_nav"
}
}
@@ -0,0 +1,84 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.megaphone
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isNull
import assertk.assertions.isTrue
import io.mockk.every
import io.mockk.mockkObject
import io.mockk.mockkStatic
import io.mockk.unmockkObject
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppDonations
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.VersionTracker
/**
* The "user might be a sustainer" rule for the donations remote megaphone: the `standard_donate`
* conditional (via [RemoteMegaphoneRepository.getRemoteMegaphoneToShow]) suppresses the megaphone for
* existing donors and shows it otherwise.
*/
@RunWith(AndroidJUnit4::class)
class DonationMegaphoneGatingTest {
@get:Rule
val harness = SignalActivityRule()
@get:Rule
val iapRule = InAppPaymentsRule()
@Before
fun setUp() {
SignalDatabase.inAppPayments.writableDatabase.deleteAll(InAppPaymentTable.TABLE_NAME)
SignalDatabase.remoteMegaphones.debugRemoveAll()
setSelfBadges(emptyList())
// Freshly-installed test APKs report 0 days installed and no configured payment methods, both of
// which independently fail shouldShowDonateMegaphone. Fix them so the donor badge is the only
// variable across the gating tests.
mockkStatic(VersionTracker::class)
mockkObject(InAppDonations)
every { VersionTracker.getDaysSinceFirstInstalled(any()) } returns 30L
every { InAppDonations.hasAtLeastOnePaymentMethodAvailable() } returns true
}
@After
fun tearDown() {
unmockkStatic(VersionTracker::class)
unmockkObject(InAppDonations)
}
@Test
fun nonDonor_showsStandardDonateMegaphone() {
val record = donateMegaphoneRecord(conditionalId = "standard_donate")
SignalDatabase.remoteMegaphones.insert(record)
assertThat(RemoteMegaphoneRepository.getRemoteMegaphoneToShow()?.uuid).isEqualTo(record.uuid)
assertThat(RemoteMegaphoneRepository.hasRemoteMegaphoneToShow(canShowLocalDonate = true)).isTrue()
}
@Test
fun sustainer_donorBadge_suppressesStandardDonateMegaphone() {
SignalDatabase.remoteMegaphones.insert(donateMegaphoneRecord(conditionalId = "standard_donate"))
setSelfBadges(listOf(donorBadge()))
assertThat(RemoteMegaphoneRepository.getRemoteMegaphoneToShow()).isNull()
assertThat(RemoteMegaphoneRepository.hasRemoteMegaphoneToShow(canShowLocalDonate = true)).isFalse()
}
}
@@ -0,0 +1,113 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.megaphone
import android.app.Activity
import android.content.Context
import android.content.Intent
import androidx.compose.ui.test.junit4.v2.createComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.fragment.app.DialogFragment
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isNotNull
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.getSerializableCompat
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
* Entry-path coverage for the donations remote megaphone: the megaphone shown on the conversation list
* that carries a "Donate" action into the one-time checkout flow.
*
* The megaphone renders through [MegaphoneComponent], so this renders that composable directly (rather
* than launching the full [org.thoughtcrime.securesms.MainActivity]) and asserts that tapping Donate
* runs the production [RemoteMegaphoneRepository] action, which navigates to [CheckoutFlowActivity]
* with [InAppPaymentType.ONE_TIME_DONATION]. The sustainer gating is covered by [DonationMegaphoneGatingTest].
*/
@RunWith(AndroidJUnit4::class)
class DonationMegaphoneTest {
@get:Rule
val harness = SignalActivityRule()
@get:Rule
val iapRule = InAppPaymentsRule()
@get:Rule
val composeRule = createComposeRule()
private val context: Context get() = InstrumentationRegistry.getInstrumentation().targetContext
@Before
fun setUp() {
SignalDatabase.remoteMegaphones.debugRemoveAll()
}
@Test
fun donateMegaphone_donateClick_opensOneTimeCheckout() {
val record = donateMegaphoneRecord(conditionalId = null)
SignalDatabase.remoteMegaphones.insert(record)
val controller = RecordingMegaphoneActionController()
composeRule.setContent {
SignalTheme {
MegaphoneComponent(buildDonateMegaphone(record), controller)
}
}
composeRule.onNodeWithText(record.primaryActionText!!).performClick()
val intent = controller.navigationIntent
assertThat(intent).isNotNull()
assertThat(intent!!.component?.className).isEqualTo(CheckoutFlowActivity::class.java.name)
val type = intent.extras!!.getSerializableCompat(CheckoutFlowActivity.ARG_IN_APP_PAYMENT_TYPE, InAppPaymentType::class.java)
assertThat(type).isEqualTo(InAppPaymentType.ONE_TIME_DONATION)
}
/** Mirrors [Megaphones.buildRemoteMegaphone]: wires the primary button to the real repository action. */
private fun buildDonateMegaphone(record: RemoteMegaphoneRecord): Megaphone {
return Megaphone.Builder(Megaphones.Event.REMOTE_MEGAPHONE, Megaphone.Style.BASIC)
.setTitle(record.title)
.setBody(record.body)
.setActionButton(record.primaryActionText!!) { _, controller ->
RemoteMegaphoneRepository.getAction(record.primaryActionId!!).run(context, controller, record)
}
.build()
}
private class RecordingMegaphoneActionController : MegaphoneActionController {
var navigationIntent: Intent? = null
private set
override fun onMegaphoneNavigationRequested(intent: Intent) {
navigationIntent = intent
}
override fun onMegaphoneNavigationRequested(intent: Intent, requestCode: Int) {
navigationIntent = intent
}
override fun onMegaphoneToastRequested(string: String) = Unit
override fun getMegaphoneActivity(): Activity = throw UnsupportedOperationException("not used")
override fun onMegaphoneSnooze(event: Megaphones.Event) = Unit
override fun onMegaphoneCompleted(event: Megaphones.Event) = Unit
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) = Unit
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.megaphone
import android.net.Uri
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.UUID
import java.util.concurrent.TimeUnit
fun donateMegaphoneRecord(conditionalId: String?): RemoteMegaphoneRecord {
val now = System.currentTimeMillis()
return RemoteMegaphoneRecord(
priority = 100,
uuid = UUID.randomUUID().toString(),
countries = null,
minimumVersion = 1,
doNotShowBefore = now - TimeUnit.DAYS.toMillis(2),
doNotShowAfter = now + TimeUnit.DAYS.toMillis(28),
showForNumberOfDays = 30,
conditionalId = conditionalId,
primaryActionId = RemoteMegaphoneRecord.ActionId.DONATE,
secondaryActionId = RemoteMegaphoneRecord.ActionId.SNOOZE,
imageUrl = null,
title = "Donate Test",
body = "Donate body test.",
primaryActionText = "Donate",
secondaryActionText = "Snooze"
)
}
fun donorBadge(): Badge {
return Badge(
id = "test-donor-badge",
category = Badge.Category.Donor,
name = "Signal Sustainer",
description = "",
imageUrl = Uri.EMPTY,
imageDensity = "xxhdpi",
expirationTimestamp = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(30),
visible = true,
duration = TimeUnit.DAYS.toMillis(30)
)
}
fun setSelfBadges(badges: List<Badge>) {
SignalDatabase.recipients.setBadges(Recipient.self().id, badges)
Recipient.self().fresh()
}
@@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.os.Parcelable
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.VisibleForTesting
import androidx.fragment.app.Fragment
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
@@ -26,7 +27,8 @@ import org.thoughtcrime.securesms.components.settings.app.subscription.GooglePay
class CheckoutFlowActivity : FragmentWrapperActivity(), GooglePayComponent {
companion object {
private const val ARG_IN_APP_PAYMENT_TYPE = "in_app_payment_type"
@VisibleForTesting
const val ARG_IN_APP_PAYMENT_TYPE = "in_app_payment_type"
const val RESULT_DATA = "result_data"
fun createIntent(context: Context, inAppPaymentType: InAppPaymentType): Intent {
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
@@ -78,7 +79,8 @@ class DonateToSignalFragment :
companion object {
private const val ARG = "in_app_payment_type"
@VisibleForTesting
const val ARG = "in_app_payment_type"
@JvmStatic
fun create(inAppPaymentType: InAppPaymentType): DialogFragment {