Path: blob/main/Signal/src/ViewControllers/AppSettings/Donations/DonationSettingsViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SafariServices
import SignalServiceKit
import SignalUI
import UIKit
class DonationSettingsViewController: OWSTableViewController2 {
enum State {
enum SubscriptionStatus {
case loadFailed
case noSubscription
case pendingSubscription(PendingMonthlyIDEALDonation)
/// The user has a subscription, which may be active or inactive.
///
/// Both active and inactive subscriptions may be in a "processing"
/// state. Inactive subscriptions may also have a charge failure
/// if payment failed.
///
/// The receipt credential request error may be present for either
/// active or inactive subscriptions. In most cases, it will reflect
/// either a processing or failed payment – state that is available
/// from the subscription itself – but if something rare went wrong
/// it may also reflect an error external to the subscription.
case hasSubscription(
subscription: Subscription,
subscriptionLevel: DonationSubscriptionLevel?,
previouslyHadActiveSubscription: Bool,
receiptCredentialRequestError: DonationReceiptCredentialRequestError?,
)
}
case initializing
case loading
case loadFinished(
subscriptionStatus: SubscriptionStatus,
oneTimeBoostReceiptCredentialRequestError: DonationReceiptCredentialRequestError?,
profileBadgeLookup: ProfileBadgeLookup,
pendingOneTimeDonation: PendingOneTimeIDEALDonation?,
hasAnyBadges: Bool,
hasAnyDonationReceipts: Bool,
)
var debugDescription: String {
switch self {
case .initializing:
return "initializing"
case .loading:
return "loading"
case .loadFinished:
return "loadFinished"
}
}
}
private var state: State = .initializing {
didSet {
Logger.info("[Donations] DonationSettingsViewController state changed to \(state.debugDescription)")
updateTableContents()
}
}
private var avatarView: ConversationAvatarView = DonationViewsUtil.avatarView()
private static var canDonateInAnyWay: Bool {
DonationUtilities.canDonateInAnyWay(
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
)
}
private static var canSendGiftBadges: Bool {
DonationUtilities.canDonate(
inMode: .gift,
tsAccountManager: DependenciesBridge.shared.tsAccountManager,
)
}
var showExpirationSheet: Bool
/// This view can display sheets to the user on first appearance. However there are scenarios
/// (eg. deep links) where suppressing any dialogs may be wanted. This boolean allows for that.
init(showExpirationSheet: Bool = true) {
self.showExpirationSheet = showExpirationSheet
super.init()
}
override func viewDidLoad() {
super.viewDidLoad()
setUpAvatarView()
title = OWSLocalizedString("DONATION_VIEW_TITLE", comment: "Title on the 'Donate to Signal' screen")
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Task {
await self.loadAndUpdateState()
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if showExpirationSheet {
if !showPendingIDEALAuthorizationSheetIfNeeded() {
showGiftBadgeExpirationSheetIfNeeded()
}
// viewDidAppear can be called multiple times, and we don't want
// Record that an intial attempt was made to show a sheet and
// don't try again after that.
showExpirationSheet = false
}
}
@objc
private func didLongPressAvatar(sender: UIGestureRecognizer) {
let subscriberID = SSKEnvironment.shared.databaseStorageRef.read { DonationSubscriptionManager.getSubscriberID(transaction: $0) }
guard let subscriberID else { return }
UIPasteboard.general.string = subscriberID.asBase64Url
presentToast(
text: OWSLocalizedString(
"SUBSCRIPTION_SUBSCRIBER_ID_COPIED_TO_CLIPBOARD",
comment: "Toast indicating that the user has copied their subscriber ID. (Externally referred to as donor ID)",
),
image: .copy,
)
}
// MARK: - Data loading
func loadAndUpdateState() async {
switch state {
case .loading:
owsFailDebug("Already loading!")
return
case .initializing, .loadFinished:
self.state = .loading
self.state = await self.loadState()
}
}
private func loadState() async -> State {
let idealStore = DependenciesBridge.shared.externalPendingIDEALDonationStore
let profileManager = SSKEnvironment.shared.profileManagerRef
let (
subscriberID,
hasEverRedeemedRecurringSubscriptionBadge,
recurringSubscriptionReceiptCredentialRequestError,
oneTimeBoostReceiptCredentialRequestError,
hasAnyDonationReceipts,
pendingIDEALOneTimeDonation,
pendingIDEALSubscription,
hasAnyBadges,
) = SSKEnvironment.shared.databaseStorageRef.read { tx in
let resultStore = DependenciesBridge.shared.donationReceiptCredentialResultStore
return (
subscriberID: DonationSubscriptionManager.getSubscriberID(transaction: tx),
hasEverRedeemedRecurringSubscriptionBadge: resultStore.getRedemptionSuccessForAnyRecurringSubscription(tx: tx) != nil,
recurringSubscriptionReceiptCredentialRequestError: resultStore.getRequestErrorForAnyRecurringSubscription(tx: tx),
oneTimeBoostReceiptCredentialRequestError: resultStore.getRequestError(errorMode: .oneTimeBoost, tx: tx),
hasAnyDonationReceipts: DonationReceiptFinder.hasAny(transaction: tx),
idealStore.getPendingOneTimeDonation(tx: tx),
idealStore.getPendingSubscription(tx: tx),
profileManager.localUserProfile(tx: tx)?.hasBadge == true,
)
}
async let currentSubscription = DonationViewsUtil.loadCurrentSubscription(subscriberID: subscriberID)
async let donationConfiguration = DonationSubscriptionManager.fetchDonationConfiguration()
do {
let subscriptionStatus: State.SubscriptionStatus
if let currentSubscription = try await currentSubscription {
subscriptionStatus = .hasSubscription(
subscription: currentSubscription,
subscriptionLevel: DonationViewsUtil.subscriptionLevelForSubscription(
subscriptionLevels: try await DonationViewsUtil.loadSubscriptionLevels(
donationConfiguration: try await donationConfiguration,
badgeStore: SSKEnvironment.shared.profileManagerRef.badgeStore,
),
subscription: currentSubscription,
),
previouslyHadActiveSubscription: hasEverRedeemedRecurringSubscriptionBadge,
receiptCredentialRequestError: recurringSubscriptionReceiptCredentialRequestError,
)
} else if let pendingIDEALSubscription {
subscriptionStatus = .pendingSubscription(pendingIDEALSubscription)
} else {
subscriptionStatus = .noSubscription
}
let result: State = .loadFinished(
subscriptionStatus: subscriptionStatus,
oneTimeBoostReceiptCredentialRequestError: oneTimeBoostReceiptCredentialRequestError,
profileBadgeLookup: await loadProfileBadgeLookup(donationConfiguration: try? await donationConfiguration),
pendingOneTimeDonation: pendingIDEALOneTimeDonation,
hasAnyBadges: hasAnyBadges,
hasAnyDonationReceipts: hasAnyDonationReceipts,
)
if let pendingIDEALSubscription {
// Serialized badges lose their assets, so ensure they've
// been populated before returning.
try? await SSKEnvironment.shared.profileManagerRef.badgeStore.populateAssetsOnBadge(pendingIDEALSubscription.newSubscriptionLevel.badge)
}
return result
} catch {
Logger.warn("[Donations] \(error)")
owsFailDebugUnlessNetworkFailure(error)
let result: State = .loadFinished(
subscriptionStatus: .loadFailed,
oneTimeBoostReceiptCredentialRequestError: oneTimeBoostReceiptCredentialRequestError,
profileBadgeLookup: await loadProfileBadgeLookup(donationConfiguration: try? await donationConfiguration),
pendingOneTimeDonation: pendingIDEALOneTimeDonation,
hasAnyBadges: hasAnyBadges,
hasAnyDonationReceipts: hasAnyDonationReceipts,
)
return result
}
}
private func loadProfileBadgeLookup(donationConfiguration: DonationSubscriptionConfiguration?) async -> ProfileBadgeLookup {
if let donationConfiguration {
let result = ProfileBadgeLookup(
boostBadge: donationConfiguration.boost.badge,
giftBadge: donationConfiguration.gift.badge,
subscriptionLevels: donationConfiguration.subscription.levels,
)
await result.attemptToPopulateBadgeAssets(populateAssetsOnBadge: SSKEnvironment.shared.profileManagerRef.badgeStore.populateAssetsOnBadge(_:))
return result
} else {
Logger.warn("[Donations] Failed to fetch donation configuration. Proceeding without it, as it is only cosmetic here.")
return ProfileBadgeLookup(
boostBadge: nil,
giftBadge: nil,
subscriptionLevels: [],
)
}
}
private func setUpAvatarView() {
SSKEnvironment.shared.databaseStorageRef.read { transaction in
self.avatarView.update(transaction) { config in
if let address = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: transaction)?.aciAddress {
config.dataSource = .address(address)
config.addBadgeIfApplicable = true
}
}
}
avatarView.isUserInteractionEnabled = true
avatarView.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongPressAvatar)))
}
// MARK: - Table contents
private func updateTableContents() {
let contents = OWSTableContents()
contents.add(heroSection())
switch state {
case .initializing, .loading:
contents.add(loadingSection())
case let .loadFinished(
subscriptionStatus,
oneTimeBoostReceiptCredentialRequestError,
profileBadgeLookup,
pendingOneTimeDonation,
hasAnyBadges,
hasAnyDonationReceipts,
):
let sections = loadFinishedSections(
subscriptionStatus: subscriptionStatus,
profileBadgeLookup: profileBadgeLookup,
oneTimeBoostReceiptCredentialRequestError: oneTimeBoostReceiptCredentialRequestError,
pendingOneTimeDonation: pendingOneTimeDonation,
hasAnyBadges: hasAnyBadges,
hasAnyDonationReceipts: hasAnyDonationReceipts,
)
contents.add(sections: sections)
}
self.contents = contents
}
private func heroSection() -> OWSTableSection {
OWSTableSection(items: [.init(customCellBlock: { [weak self] in
let cell = OWSTableItem.newCell()
guard let self else { return cell }
let heroStack = DonationHeroView(avatarView: self.avatarView)
heroStack.delegate = self
let buttonTitle = OWSLocalizedString(
"DONATION_SCREEN_DONATE_BUTTON",
comment: "On the donation settings screen, tapping this button will take the user to a screen where they can donate.",
)
let button = UIButton(
configuration: .largePrimary(title: buttonTitle),
primaryAction: UIAction { [weak self] _ in
if Self.canDonateInAnyWay {
self?.showDonateViewController(preferredDonateMode: .oneTime)
} else {
DonationViewsUtil.openDonateWebsite()
}
},
)
heroStack.addArrangedSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.widthAnchor.constraint(equalTo: heroStack.layoutMarginsGuide.widthAnchor).isActive = true
cell.contentView.addSubview(heroStack)
heroStack.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(hMargin: 24, vMargin: 6))
return cell
})])
}
private func loadingSection() -> OWSTableSection {
let section = OWSTableSection()
section.add(AppSettingsViewsUtil.loadingTableItem())
section.hasBackground = false
return section
}
private func loadFinishedSections(
subscriptionStatus: State.SubscriptionStatus,
profileBadgeLookup: ProfileBadgeLookup,
oneTimeBoostReceiptCredentialRequestError: DonationReceiptCredentialRequestError?,
pendingOneTimeDonation: PendingOneTimeIDEALDonation?,
hasAnyBadges: Bool,
hasAnyDonationReceipts: Bool,
) -> [OWSTableSection] {
[
mySupportSection(
subscriptionStatus: subscriptionStatus,
profileBadgeLookup: profileBadgeLookup,
oneTimeBoostReceiptCredentialRequestError: oneTimeBoostReceiptCredentialRequestError,
pendingOneTimeDonation: pendingOneTimeDonation,
hasAnyBadges: hasAnyBadges,
),
moreSection(
profileBadgeLookup: profileBadgeLookup,
hasAnyDonationReceipts: hasAnyDonationReceipts,
),
].compacted()
}
private func moreSection(
profileBadgeLookup: ProfileBadgeLookup,
hasAnyDonationReceipts: Bool,
) -> OWSTableSection? {
let section = OWSTableSection()
// It should be unusual to hit this case—having a subscription but no receipts—
// but it is possible. For example, it can happen if someone started a subscription
// before a receipt was saved.
if hasAnyDonationReceipts {
section.add(donationReceiptsItem(profileBadgeLookup: profileBadgeLookup))
}
if Self.canSendGiftBadges {
section.add(.disclosureItem(
icon: .donateGift,
withText: OWSLocalizedString(
"DONATION_VIEW_DONATE_ON_BEHALF_OF_A_FRIEND",
comment: "Title for the \"donate for a friend\" button on the donation view.",
),
actionBlock: { [weak self] in
guard let self else { return }
let vc = BadgeGiftingChooseBadgeViewController()
self.navigationController?.pushViewController(vc, animated: true)
},
))
}
section.add(.disclosureItem(
icon: .settingsHelp,
withText: OWSLocalizedString(
"DONATION_VIEW_DONOR_FAQ",
comment: "Title for the 'Donor FAQ' button on the donation screen",
),
actionBlock: { [weak self] in
let vc = SFSafariViewController(url: URL.Support.Donations.donorFAQ)
self?.present(vc, animated: true, completion: nil)
},
))
guard section.itemCount > 0 else {
return nil
}
return section
}
private func donationReceiptsItem(profileBadgeLookup: ProfileBadgeLookup) -> OWSTableItem {
.disclosureItem(
icon: .donateReceipts,
withText: OWSLocalizedString("DONATION_RECEIPTS", comment: "Title of view where you can see all of your donation receipts, or button to take you there"),
actionBlock: { [weak self] in
let vc = DonationReceiptsViewController(profileBadgeLookup: profileBadgeLookup)
self?.navigationController?.pushViewController(vc, animated: true)
},
)
}
// MARK: - Showing subscription view controller
func showDonateViewController(preferredDonateMode: DonateViewController.DonateMode) {
let donateVc = DonateViewController(preferredDonateMode: preferredDonateMode) { [weak self] finishResult in
guard let self else { return }
switch finishResult {
case let .completedDonation(_, receiptCredentialSuccessMode):
self.navigationController?.popToViewController(self, animated: true) { [weak self] in
guard
let self,
let badgeThanksSheetPresenter = BadgeThanksSheetPresenter.fromGlobalsWithSneakyTransaction(
successMode: receiptCredentialSuccessMode,
)
else { return }
Task {
await badgeThanksSheetPresenter.presentAndRecordBadgeThanks(
fromViewController: self,
)
}
}
case let .monthlySubscriptionCancelled(_, toastText):
self.navigationController?.popToViewController(self, animated: true) { [weak self] in
guard let self else { return }
self.view.presentToast(text: toastText, fromViewController: self)
}
}
}
self.navigationController?.pushViewController(donateVc, animated: true)
}
// MARK: - Gift Badge Expiration
static func shouldShowExpiredGiftBadgeSheetWithSneakyTransaction() -> Bool {
let expiredGiftBadgeID = SSKEnvironment.shared.databaseStorageRef.read { transaction in
DonationSubscriptionManager.mostRecentlyExpiredGiftBadgeID(transaction: transaction)
}
guard let expiredGiftBadgeID, GiftBadgeIds.contains(expiredGiftBadgeID) else {
return false
}
return true
}
private func showGiftBadgeExpirationSheetIfNeeded() {
guard Self.shouldShowExpiredGiftBadgeSheetWithSneakyTransaction() else {
return
}
Logger.info("[Gifting] Preparing to show gift badge expiration sheet...")
firstly {
DonationSubscriptionManager.getCachedBadge(level: .giftBadge(.signalGift)).fetchIfNeeded()
}.done { [weak self] cachedValue in
guard let self else { return }
guard UIApplication.shared.frontmostViewController == self else { return }
guard case .profileBadge(let profileBadge) = cachedValue else {
// The server confirmed this badge doesn't exist. This shouldn't happen,
// but clear the flag so that we don't keep trying.
Logger.warn("[Gifting] Clearing expired badge ID because the server said it didn't exist")
DonationSubscriptionManager.clearMostRecentlyExpiredBadgeIDWithSneakyTransaction()
return
}
let hasCurrentSubscription = SSKEnvironment.shared.databaseStorageRef.read { tx -> Bool in
return DonationSubscriptionManager.probablyHasCurrentSubscription(tx: tx)
}
Logger.info("[Gifting] Showing badge gift expiration sheet (hasCurrentSubscription: \(hasCurrentSubscription))")
let sheet = BadgeIssueSheet(badge: profileBadge, mode: .giftBadgeExpired(hasCurrentSubscription: hasCurrentSubscription))
sheet.delegate = self
self.present(sheet, animated: true)
// We've shown it, so don't show it again.
DonationSubscriptionManager.clearMostRecentlyExpiredGiftBadgeIDWithSneakyTransaction()
}.cauterize()
}
// MARK: - IDEAL support methods
/// Check if there is a pending iDEAL payment awaiting authorization. If so, check how old the
/// payment is and display a message that either it still needs external authorization or the payment
/// failed and can be tried again.
private func showPendingIDEALAuthorizationSheetIfNeeded() -> Bool {
let idealStore = DependenciesBridge.shared.externalPendingIDEALDonationStore
let expiration: TimeInterval = 15 * .minute
func showError(title: String, message: String, donationMode: DonateViewController.DonateMode) {
let actionSheet = ActionSheetController(
title: title,
message: message,
)
actionSheet.addAction(ActionSheetAction(
title: OWSLocalizedString(
"DONATION_BADGE_ISSUE_SHEET_TRY_AGAIN_BUTTON_TITLE",
comment: "Title for a button asking the user to try their donation again, because something went wrong.",
),
handler: { [weak self] _ in
guard let self else { return }
self.presentAwaitingIDEALAuthorizationActionSheet(donateMode: donationMode)
},
))
actionSheet.addAction(.init(
title: CommonStrings.okayButton,
style: .cancel,
handler: nil,
))
presentActionSheet(actionSheet)
}
let (pendingOneTime, pendingSubscription) = SSKEnvironment.shared.databaseStorageRef.read { tx in
let oneTimeDonation = idealStore.getPendingOneTimeDonation(tx: tx)
let subscription = idealStore.getPendingSubscription(tx: tx)
return (oneTimeDonation, subscription)
}
if let pendingOneTime {
if abs(pendingOneTime.createDate.timeIntervalSinceNow) > expiration {
let title = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_DONATION_FAILED_ALERT_TITLE",
comment: "Title for a sheet explaining that a payment failed.",
)
let message = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_IDEAL_ONE_TIME_DONATION_FAILED_MESSAGE",
comment: "Message shown in a sheet explaining that the user's iDEAL one-time donation coultn't be processed.",
)
showError(title: title, message: message, donationMode: .oneTime)
// cleanup
SSKEnvironment.shared.databaseStorageRef.write { tx in
idealStore.clearPendingOneTimeDonation(tx: tx)
}
} else {
let title = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_DONATION_UNCONFIMRED_ALERT_TITLE",
comment: "Title for a sheet explaining that a payment needs confirmation.",
)
let messageFormat = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_IDEAL_ONE_TIME_DONATION_NOT_CONFIRMED_MESSAGE_FORMAT",
comment: "Title for a sheet explaining that a payment needs confirmation.",
)
let message = String.nonPluralLocalizedStringWithFormat(messageFormat, CurrencyFormatter.format(money: pendingOneTime.amount))
showError(title: title, message: message, donationMode: .oneTime)
}
return true
} else if let pendingSubscription {
if abs(pendingSubscription.createDate.timeIntervalSinceNow) > expiration {
let title = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_DONATION_FAILED_ALERT_TITLE",
comment: "Title for a sheet explaining that a payment failed.",
)
let message = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_IDEAL_RECURRING_SUBSCRIPTION_FAILED_MESSAGE",
comment: "Message shown in a sheet explaining that the user's iDEAL recurring monthly donation coultn't be processed.",
)
showError(title: title, message: message, donationMode: .monthly)
SSKEnvironment.shared.databaseStorageRef.write { tx in
idealStore.clearPendingSubscription(tx: tx)
}
} else {
let title = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_DONATION_UNCONFIMRED_ALERT_TITLE",
comment: "Title for a sheet explaining that a payment needs confirmation.",
)
let messageFormat = OWSLocalizedString(
"DONATION_SETTINGS_MY_SUPPORT_IDEAL_RECURRING_SUBSCRIPTION_NOT_CONFIRMED_MESSAGE_FORMAT",
comment: "Message shown in a sheet explaining that the user's iDEAL recurring monthly donation hasn't been confirmed. Embeds {{ formatted current amount }}.",
)
let message = String.nonPluralLocalizedStringWithFormat(messageFormat, CurrencyFormatter.format(money: pendingSubscription.amount))
showError(title: title, message: message, donationMode: .monthly)
}
return true
}
return false
}
func presentAwaitingIDEALAuthorizationActionSheet(donateMode: DonateViewController.DonateMode) {
let actionSheet = ActionSheetController(
title: nil,
message: OWSLocalizedString(
"DONATION_SETTINGS_CANCEL_DONATION_AWAITING_AUTHORIZATION_MESSAGE",
comment: "Prompt confirming the user wants to abandon the current donation flow and start a new donation.",
),
)
actionSheet.addAction(showDonateAndClearPendingIDEALDonation(
title: OWSLocalizedString(
"DONATION_SETTINGS_CANCEL_DONATION_AWAITING_AUTHORIZATION_DONATE_ACTION",
comment: "Button title confirming the user wants to begin a new donation.",
),
preferredDonateMode: donateMode,
))
actionSheet.addAction(OWSActionSheets.cancelAction)
self.presentActionSheet(actionSheet, animated: true)
}
private func showDonateAndClearPendingIDEALDonation(
title: String,
preferredDonateMode: DonateViewController.DonateMode,
) -> ActionSheetAction {
return clearErrorAndShowDonateAction(title: title, donateMode: preferredDonateMode) { tx in
switch preferredDonateMode {
case .oneTime:
DependenciesBridge.shared.externalPendingIDEALDonationStore
.clearPendingOneTimeDonation(tx: tx)
case .monthly:
DependenciesBridge.shared.externalPendingIDEALDonationStore
.clearPendingSubscription(tx: tx)
}
}
}
func clearErrorAndShowDonateAction(
title: String,
donateMode: DonateViewController.DonateMode,
clearErrorBlock: @escaping (DBWriteTransaction) -> Void,
) -> ActionSheetAction {
return ActionSheetAction(title: title) { _ in
SSKEnvironment.shared.databaseStorageRef.write { tx in
clearErrorBlock(tx)
}
// Not ideal, because this makes network requests. However, this
// should be rare, and doing it this way avoids us needing to add
// methods for updating the state outside the normal loading flow.
Task { [weak self] in
await self?.loadAndUpdateState()
self?.showDonateViewController(preferredDonateMode: donateMode)
}
}
}
}
// MARK: - Badge Issue Delegate
extension DonationSettingsViewController: BadgeIssueSheetDelegate {
func badgeIssueSheetActionTapped(_ action: BadgeIssueSheetAction) {
switch action {
case .dismiss:
break
case .openDonationView:
self.showDonateViewController(preferredDonateMode: .oneTime)
}
}
}
// MARK: - Badge management delegate
extension DonationSettingsViewController: BadgeConfigurationDelegate {
func badgeConfiguration(_ vc: BadgeConfigurationViewController, didCompleteWithBadgeSetting setting: BadgeConfiguration) {
if !SSKEnvironment.shared.reachabilityManagerRef.isReachable {
OWSActionSheets.showErrorAlert(
message: OWSLocalizedString(
"PROFILE_VIEW_NO_CONNECTION",
comment: "Error shown when the user tries to update their profile when the app is not connected to the internet.",
),
)
return
}
Task {
await self.didCompleteBadgeConfiguration(setting, viewController: vc)
}
}
private func didCompleteBadgeConfiguration(_ badgeConfiguration: BadgeConfiguration, viewController: BadgeConfigurationViewController) async {
let profileManager = SSKEnvironment.shared.profileManagerRef
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
do {
let localProfile = databaseStorage.read { tx in profileManager.localUserProfile(tx: tx) }
let allBadgeIds = localProfile?.badges.map { $0.badgeId } ?? []
let oldVisibleBadgeIds = localProfile?.visibleBadges.map { $0.badgeId } ?? []
let newVisibleBadgeIds: [String]
switch badgeConfiguration {
case .doNotDisplayPublicly:
newVisibleBadgeIds = []
case .display(featuredBadge: let newFeaturedBadge):
guard allBadgeIds.contains(newFeaturedBadge.badgeId) else {
throw OWSAssertionError("Invalid badge")
}
newVisibleBadgeIds = [newFeaturedBadge.badgeId] + allBadgeIds.filter { $0 != newFeaturedBadge.badgeId }
}
if oldVisibleBadgeIds != newVisibleBadgeIds {
Logger.info("[Donations] Updating visible badges from \(oldVisibleBadgeIds) to \(newVisibleBadgeIds)")
viewController.showDismissalActivity = true
let updatePromise = await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
SSKEnvironment.shared.profileManagerRef.updateLocalProfile(
profileGivenName: .noChange,
profileFamilyName: .noChange,
profileBio: .noChange,
profileBioEmoji: .noChange,
profileAvatarData: .noChange,
visibleBadgeIds: .setTo(newVisibleBadgeIds),
unsavedRotatedProfileKey: nil,
userProfileWriter: .localUser,
authedAccount: .implicit(),
tx: tx,
)
}
try await updatePromise.awaitable()
}
let displayBadgesOnProfile: Bool
switch badgeConfiguration {
case .doNotDisplayPublicly:
displayBadgesOnProfile = false
case .display:
displayBadgesOnProfile = true
}
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in
DonationSubscriptionManager.setDisplayBadgesOnProfile(
displayBadgesOnProfile,
updateStorageService: true,
transaction: tx,
)
}
} catch {
owsFailDebug("Failed to update profile: \(error)")
}
self.navigationController?.popViewController(animated: true)
}
func badgeConfirmationDidCancel(_: BadgeConfigurationViewController) {
self.navigationController?.popViewController(animated: true)
}
}
// MARK: - Donation hero delegate
extension DonationSettingsViewController: DonationHeroViewDelegate {
func present(readMoreSheet: DonationReadMoreSheetViewController) {
present(readMoreSheet, animated: true)
}
}