Implement several donor badge fixes and rotate flags.

* Add white Google Pay buttons for use in dark mode.
* Always display badges for self.
* Disallow toggling / feature selection if no network is present.
* Only display bottom sheet overscroll if content scrolls.
* Flatten settings xml for better animations.
* Add a bit of space to the bottom of subscribe fragment.
* Treat GooglePay errors as setup failures.
* Add quieter log for 404.
* Ensure we check case before initial currency code comparison.
* Fix timeout dialog copy.
* Fix double settings activity on top issue.
* Rotate FF.
This commit is contained in:
Alex Hart
2021-11-18 13:25:37 -04:00
committed by GitHub
parent 473747ee03
commit e17c49505c
95 changed files with 838 additions and 228 deletions

View File

@@ -20,15 +20,13 @@ class BadgeRepository(context: Context) {
selfBadges: List<Badge> = Recipient.self().badges
): Completable = Completable.fromAction {
val recipientDatabase: RecipientDatabase = DatabaseFactory.getRecipientDatabase(context)
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
ProfileUtil.uploadProfileWithBadges(context, badges)
recipientDatabase.setBadges(Recipient.self().id, badges)
}.subscribeOn(Schedulers.io())

View File

@@ -80,7 +80,7 @@ class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
),
onClick = {
dismiss()
findNavController().navigate(R.id.action_directly_to_subscribe)
findNavController().navigate(R.id.action_direct_to_subscriptions)
}
)

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.badges.self.none
import android.content.Intent
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import org.signal.core.util.DimensionUnit
@@ -60,7 +61,7 @@ class BecomeASustainerFragment : DSLSettingsBottomSheetFragment() {
),
onClick = {
requireActivity().finish()
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP))
}
)

View File

@@ -71,7 +71,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
asyncSwitchPref(
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__display_badges_on_profile),
isChecked = state.displayBadgesOnProfile,
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges,
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
isProcessing = state.stage == BadgesOverviewState.Stage.UPDATING_BADGE_DISPLAY_STATE,
onClick = {
viewModel.setDisplayBadgesOnProfile(!state.displayBadgesOnProfile)
@@ -81,7 +81,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
clickPref(
title = DSLSettingsText.from(R.string.BadgesOverviewFragment__featured_badge),
summary = state.featuredBadge?.name?.let { DSLSettingsText.from(it) },
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges,
isEnabled = state.stage == BadgesOverviewState.Stage.READY && state.hasUnexpiredBadges && state.hasInternet,
onClick = {
findNavController().navigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToFeaturedBadgeFragment())
}

View File

@@ -7,7 +7,8 @@ data class BadgesOverviewState(
val allUnlockedBadges: List<Badge> = listOf(),
val featuredBadge: Badge? = null,
val displayBadgesOnProfile: Boolean = false,
val fadedBadgeId: String? = null
val fadedBadgeId: String? = null,
val hasInternet: Boolean = false
) {
val hasUnexpiredBadges = allUnlockedBadges.any { it.expirationTimestamp > System.currentTimeMillis() }

View File

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.libsignal.util.guava.Optional
@@ -42,6 +43,12 @@ class BadgesOverviewViewModel(
)
}
disposables += InternetConnectionObserver.observe()
.distinctUntilChanged()
.subscribeBy { isConnected ->
store.update { it.copy(hasInternet = isConnected) }
}
disposables += Single.zip(
subscriptionsRepository.getActiveSubscription(),
subscriptionsRepository.getSubscriptions()

View File

@@ -120,7 +120,7 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
recipientId: RecipientId,
startBadge: Badge? = null
) {
if (!FeatureFlags.displayDonorBadges()) {
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
return
}

View File

@@ -34,6 +34,7 @@ abstract class DSLSettingsBottomSheetFragment(
recyclerView.layoutManager = layoutManagerProducer(requireContext())
recyclerView.adapter = adapter
recyclerView.overScrollMode = RecyclerView.OVER_SCROLL_IF_CONTENT_SCROLLS
bindAdapter(adapter)
}

View File

@@ -47,8 +47,8 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
StartLocation.PROXY -> AppSettingsFragmentDirections.actionDirectToEditProxyFragment()
StartLocation.NOTIFICATIONS -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment()
StartLocation.CHANGE_NUMBER -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment()
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions().setSkipToSubscribe(true)
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
StartLocation.SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToSubscriptions()
StartLocation.MANAGE_SUBSCRIPTIONS -> AppSettingsFragmentDirections.actionDirectToManageDonations()
}
}
@@ -75,6 +75,12 @@ class AppSettingsActivity : DSLSettingsActivity(), DonationPaymentComponent {
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
finish()
startActivity(intent)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(STATE_WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated)

View File

@@ -157,11 +157,11 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),
isActive = state.hasActiveSubscription,
onClick = { isActive ->
findNavController()
.navigate(
AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscriptions()
.setSkipToSubscribe(!isActive)
)
if (isActive) {
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToManageDonationsFragment())
} else {
findNavController().navigate(AppSettingsFragmentDirections.actionAppSettingsFragmentToSubscribeFragment())
}
}
)
)

View File

@@ -11,6 +11,8 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
import java.util.concurrent.TimeUnit
class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRepository) : ViewModel() {
@@ -39,6 +41,10 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
subscriptionsRepository.getActiveSubscription().subscribeBy(
onSuccess = { subscription -> store.update { it.copy(hasActiveSubscription = subscription.isActive) } },
onError = { throwable ->
if (throwable.isNotFoundException()) {
Log.w(TAG, "Could not load active subscription due to unset SubscriberId (404).")
}
Log.w(TAG, "Could not load active subscription", throwable)
}
)
@@ -53,4 +59,8 @@ class AppSettingsViewModel(private val subscriptionsRepository: SubscriptionsRep
companion object {
private val TAG = Log.tag(AppSettingsViewModel::class.java)
}
private fun Throwable.isNotFoundException(): Boolean {
return this is PushNetworkException && this.cause is NotFoundException || this is NotFoundException
}
}

View File

@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
sealed class DonationEvent {
class GooglePayUnavailableError(val throwable: Throwable) : DonationEvent()
object RequestTokenSuccess : DonationEvent()
object RequestTokenError : DonationEvent()
class RequestTokenError(val throwable: Throwable) : DonationEvent()
class PaymentConfirmationError(val throwable: Throwable) : DonationEvent()
class PaymentConfirmationSuccess(val badge: Badge) : DonationEvent()
class SubscriptionCancellationFailed(val throwable: Throwable) : DonationEvent()

View File

@@ -1,14 +1,9 @@
package org.thoughtcrime.securesms.components.settings.app.subscription
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import com.google.android.gms.wallet.PaymentData
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
@@ -21,7 +16,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.DatabaseFactory
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.jobs.BoostReceiptRequestResponseJob
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -75,19 +69,6 @@ class DonationPaymentRepository(activity: Activity) : StripeApi.PaymentIntentFet
StorageSyncHelper.scheduleSyncForDataChange()
}
fun internetConnectionObserver(): Observable<Boolean> = Observable.create {
val observer = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (!it.isDisposed) {
it.onNext(NetworkConstraint.isMet(application))
}
}
}
it.setCancellable { application.unregisterReceiver(observer) }
application.registerReceiver(observer, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
fun requestTokenFromGooglePay(price: FiatMoney, label: String, requestCode: Int) {
googlePayApi.requestPayment(price, label, requestCode)
}

View File

@@ -121,7 +121,7 @@ class BoostFragment : DSLSettingsBottomSheetFragment(
is DonationEvent.GooglePayUnavailableError -> Unit
is DonationEvent.PaymentConfirmationError -> onPaymentError(event.throwable)
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(event.badge)
DonationEvent.RequestTokenError -> onPaymentError(null)
is DonationEvent.RequestTokenError -> onPaymentError(event.throwable)
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> Unit
is DonationEvent.SubscriptionCancellationFailed -> Unit

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
import org.thoughtcrime.securesms.util.livedata.Store
import java.math.BigDecimal
@@ -42,8 +43,8 @@ class BoostViewModel(
private var boostToPurchase: Boost? = null
init {
networkDisposable = donationPaymentRepository
.internetConnectionObserver()
networkDisposable = InternetConnectionObserver
.observe()
.distinctUntilChanged()
.subscribe { isConnected ->
if (isConnected) {
@@ -152,9 +153,9 @@ class BoostViewModel(
}
}
override fun onError() {
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = BoostState.Stage.READY) }
eventPublisher.onNext(DonationEvent.RequestTokenError)
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
}
override fun onCancelled() {

View File

@@ -1,9 +1,7 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.manage
import android.os.Bundle
import android.widget.Toast
import androidx.fragment.app.viewModels
import androidx.navigation.NavOptions
import androidx.navigation.fragment.findNavController
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
@@ -37,29 +35,12 @@ class ManageDonationsFragment : DSLSettingsFragment() {
private val lifecycleDisposable = LifecycleDisposable()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = ManageDonationsFragmentArgs.fromBundle(requireArguments())
if (args.skipToSubscribe) {
findNavController().navigate(
ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment(),
NavOptions.Builder().setPopUpTo(R.id.manageDonationsFragment, true).build()
)
}
}
override fun onResume() {
super.onResume()
viewModel.refresh()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
val args = ManageDonationsFragmentArgs.fromBundle(requireArguments())
if (args.skipToSubscribe) {
return
}
ActiveSubscriptionPreference.register(adapter)
IndeterminateLoadingCircle.register(adapter)
BadgePreview.register(adapter)
@@ -149,7 +130,7 @@ class ManageDonationsFragment : DSLSettingsFragment() {
title = DSLSettingsText.from(R.string.ManageDonationsFragment__badges),
icon = DSLSettingsIcon.from(R.drawable.ic_badge_24),
onClick = {
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscriptionBadgeManageFragment())
findNavController().navigate(ManageDonationsFragmentDirections.actionManageDonationsFragmentToSubscribeFragment())
}
)

View File

@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
@@ -33,7 +32,6 @@ import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyboard.findListener
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.SpanUtil
import java.util.Calendar
@@ -52,7 +50,7 @@ class SubscribeFragment : DSLSettingsFragment(
SpannableStringBuilder(requireContext().getString(R.string.SubscribeFragment__support_technology_that_is_built_for_you_not))
.append(" ")
.append(
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_accent_primary)) {
SpanUtil.readMore(requireContext(), ContextCompat.getColor(requireContext(), R.color.signal_button_secondary_text)) {
findNavController().navigate(SubscribeFragmentDirections.actionSubscribeFragmentToSubscribeLearnMoreBottomSheetDialog())
}
)
@@ -98,7 +96,7 @@ class SubscribeFragment : DSLSettingsFragment(
is DonationEvent.GooglePayUnavailableError -> Unit
is DonationEvent.PaymentConfirmationError -> onPaymentError(it.throwable)
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed(it.badge)
DonationEvent.RequestTokenError -> onPaymentError(null)
is DonationEvent.RequestTokenError -> onPaymentError(DonationExceptions.SetupFailed(it.throwable))
DonationEvent.RequestTokenSuccess -> Log.w(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> onSubscriptionCancelled()
is DonationEvent.SubscriptionCancellationFailed -> onSubscriptionFailedToCancel(it.throwable)
@@ -249,13 +247,7 @@ class SubscribeFragment : DSLSettingsFragment(
)
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.SubscribeFragment__more_payment_options),
icon = DSLSettingsIcon.from(R.drawable.ic_open_20, R.color.signal_accent_primary),
onClick = {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
}
)
space(DimensionUnit.DP.toPixels(8f).toInt())
}
}
}
@@ -275,7 +267,7 @@ class SubscribeFragment : DSLSettingsFragment(
Log.w(TAG, "Timeout occurred while redeeming token", throwable, true)
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.DonationsErrors__still_processing)
.setMessage(R.string.DonationsErrors__your_payment)
.setMessage(R.string.DonationsErrors__your_payment_is_still)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
dialog.dismiss()
requireActivity().finish()

View File

@@ -25,6 +25,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.subscription.LevelUpdate
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
@@ -49,8 +50,8 @@ class SubscribeViewModel(
private val activeSubscriptionSubject = PublishSubject.create<ActiveSubscription>()
init {
networkDisposable = donationPaymentRepository
.internetConnectionObserver()
networkDisposable = InternetConnectionObserver
.observe()
.distinctUntilChanged()
.subscribe { isConnected ->
if (isConnected) {
@@ -218,9 +219,9 @@ class SubscribeViewModel(
}
}
override fun onError() {
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = SubscribeState.Stage.READY) }
eventPublisher.onNext(DonationEvent.RequestTokenError)
eventPublisher.onNext(DonationEvent.RequestTokenError(googlePayException))
}
override fun onCancelled() {

View File

@@ -39,7 +39,7 @@ object AsyncSwitch {
super.bind(model)
switchWidget.isEnabled = model.isEnabled
switchWidget.isChecked = model.isChecked
itemView.isEnabled = !model.isProcessing
itemView.isEnabled = !model.isProcessing && model.isEnabled
switcher.displayedChild = if (model.isProcessing) 1 else 0
itemView.setOnClickListener {

View File

@@ -63,7 +63,7 @@ internal class DonationsValues internal constructor(store: KeyValueStore) : Sign
CurrencyUtil.getCurrencyByCurrencyCode(currencyCode)
}
return if (currency != null && StripeApi.Validation.supportedCurrencyCodes.contains(currency.currencyCode)) {
return if (currency != null && StripeApi.Validation.supportedCurrencyCodes.contains(currency.currencyCode.toUpperCase(Locale.ROOT))) {
currency
} else {
Currency.getInstance("USD")

View File

@@ -23,6 +23,8 @@ import org.thoughtcrime.securesms.util.DynamicTheme;
*/
public class ManageProfileActivity extends PassphraseRequiredActivity implements ReactWithAnyEmojiBottomSheetDialogFragment.Callback {
public static final int RESULT_BECOME_A_SUSTAINER = 12382;
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static final String START_AT_USERNAME = "start_at_username";

View File

@@ -1028,7 +1028,7 @@ public class Recipient {
}
public @NonNull List<Badge> getBadges() {
return FeatureFlags.displayDonorBadges() ? badges : Collections.emptyList();
return FeatureFlags.displayDonorBadges() || isSelf() ? badges : Collections.emptyList();
}
public @Nullable Badge getFeaturedBadge() {

View File

@@ -84,9 +84,9 @@ public final class FeatureFlags {
private static final String MAX_GROUP_CALL_RING_SIZE = "global.calling.maxGroupCallRingSize";
private static final String GROUP_CALL_RINGING = "android.calling.groupCallRinging";
private static final String CHANGE_NUMBER_ENABLED = "android.changeNumber";
private static final String DONOR_BADGES = "android.donorBadges.5";
private static final String DONOR_BADGES = "android.donorBadges.6";
private static final String DONOR_BADGES_MEGAPHONE = "android.donorBadges.megaphone.3";
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.3";
private static final String DONOR_BADGES_DISPLAY = "android.donorBadges.display.4";
private static final String CDSH = "android.cdsh";
/**
@@ -427,7 +427,7 @@ public final class FeatureFlags {
* Whether or not donor badges should be displayed throughout the app.
*/
public static boolean displayDonorBadges() {
return getBoolean(DONOR_BADGES_DISPLAY, Environment.IS_STAGING);
return getBoolean(DONOR_BADGES_DISPLAY, false);
}
public static boolean cdsh() {

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.util
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import io.reactivex.rxjava3.core.Observable
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
object InternetConnectionObserver {
fun observe(): Observable<Boolean> = Observable.create {
val application = ApplicationDependencies.getApplication()
val observer = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (!it.isDisposed) {
it.onNext(NetworkConstraint.isMet(application))
}
}
}
it.setCancellable { application.unregisterReceiver(observer) }
application.registerReceiver(observer, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
}