Signal-iOS/Signal/src/ViewControllers/AppSettings/SubscriptionViewController.swift

1361 lines
64 KiB
Swift

//
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalUI
import SignalServiceKit
import UIKit
import BonMot
import PassKit
import SafariServices
import Lottie
import SignalCoreKit
class SubscriptionViewController: OWSTableViewController2 {
private enum SubscriptionViewState {
case loading
case subscriptionNotYetSetUp
case subscriptionExists
case subscriptionUpdating
}
private var subscriptionViewState: SubscriptionViewState {
if updatingSubscriptions == true {
return .subscriptionUpdating
} else if fetchedSubscriptionStatus == false || subscriptions == nil {
return .loading
} else {
return currentSubscription != nil ? .subscriptionExists : .subscriptionNotYetSetUp
}
}
private var subscriptions: [SubscriptionLevel]? {
didSet {
if let newSubscriptions = subscriptions, selectedSubscription == nil {
selectedSubscription = newSubscriptions.first ?? nil
}
updateTableContents()
}
}
private var selectedSubscription: SubscriptionLevel?
private var fetchedSubscriptionStatus = false
private var updatingSubscriptions = false
private var currentSubscription: Subscription?
private var persistedSubscriberID: Data?
private var persistedSubscriberCurrencyCode: String?
private var currencyCode = Stripe.defaultCurrencyCode {
didSet {
guard oldValue != currencyCode else { return }
updateTableContents()
}
}
private lazy var avatarView: ConversationAvatarView = {
let newAvatarView = ConversationAvatarView(sizeClass: .eightyEight, localUserDisplayMode: .asUser)
databaseStorage.read { readTx in
newAvatarView.update(readTx) { config in
if let address = tsAccountManager.localAddress(with: readTx) {
config.dataSource = .address(address)
}
}
}
return newAvatarView
}()
private lazy var redemptionLoadingSpinner: AnimationView = {
let loadingAnimationView = AnimationView(name: "indeterminate_spinner_blue")
loadingAnimationView.loopMode = .loop
loadingAnimationView.contentMode = .scaleAspectFit
loadingAnimationView.play()
return loadingAnimationView
}()
private lazy var statusLabel: LinkingTextView = LinkingTextView()
private lazy var descriptionTextView = LinkingTextView()
private var subscriptionRedemptionPending: Bool {
var hasPendingJobs = false
SDSDatabaseStorage.shared.read { transaction in
hasPendingJobs = SubscriptionManager.subscriptionJobQueue.hasPendingJobs(transaction: transaction)
}
hasPendingJobs = hasPendingJobs || SubscriptionManager.subscriptionJobQueue.runningOperations.get().count != 0
return hasPendingJobs
}
private var subscriptionRedemptionFailed: Bool {
var redemptionFailed = false
SDSDatabaseStorage.shared.read { transaction in
redemptionFailed = SubscriptionManager.lastReceiptRedemptionFailed(transaction: transaction)
}
return redemptionFailed
}
private let bottomFooterStackView = UIStackView()
open override var bottomFooter: UIView? {
get { bottomFooterStackView }
set {}
}
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = .current
formatter.dateStyle = .medium
return formatter
}()
static let bubbleBorderWidth: CGFloat = 1.5
static var bubbleBorderColor: UIColor { Theme.isDarkThemeEnabled ? UIColor.ows_gray65 : UIColor(rgbHex: 0xdedede) }
static var bubbleBackgroundColor: UIColor { Theme.isDarkThemeEnabled ? .ows_gray80 : .ows_white }
private static let subscriptionBannerAvatarSize: UInt = 88
public init(updatingSubscriptionsState: Bool, subscriptions: [SubscriptionLevel]?, currentSubscription: Subscription?, subscriberID: Data?, subscriberCurrencyCode: String?) {
updatingSubscriptions = updatingSubscriptionsState
self.subscriptions = subscriptions
self.currentSubscription = currentSubscription
if let currentSubscription = currentSubscription, let subscriptions = subscriptions {
fetchedSubscriptionStatus = true
for subscription in subscriptions {
if subscription.level == currentSubscription.level {
self.selectedSubscription = subscription
}
}
}
self.persistedSubscriberID = subscriberID
self.persistedSubscriberCurrencyCode = subscriberCurrencyCode
super.init()
}
public convenience override init() {
self.init(updatingSubscriptionsState: false, subscriptions: nil, currentSubscription: nil, subscriberID: nil, subscriberCurrencyCode: nil)
}
public override func viewDidLoad() {
super.viewDidLoad()
if subscriptions == nil {
// Fetch available subscriptions
firstly {
SubscriptionManager.getSubscriptions()
}.done(on: .main) { (fetchedSubscriptions: [SubscriptionLevel]) in
self.subscriptions = fetchedSubscriptions
Logger.debug("successfully fetched subscriptions")
let badgeUpdatePromises = fetchedSubscriptions.map { return self.profileManager.badgeStore.populateAssetsOnBadge($0.badge) }
firstly {
return Promise.when(fulfilled: badgeUpdatePromises)
}.done(on: .main) {
self.updateTableContents()
}.catch { error in
owsFailDebug("Failed to fetch assets for badge \(error)")
}
}.catch(on: .main) { error in
owsFailDebug("Failed to fetch subscriptions \(error)")
}
}
fetchCurrentSubscription()
updateTableContents()
NotificationCenter.default.addObserver(
self,
selector: #selector(subscriptionRedemptionJobStateDidChange),
name: SubscriptionManager.SubscriptionJobQueueDidFinishJobNotification,
object: nil)
NotificationCenter.default.addObserver(
self,
selector: #selector(subscriptionRedemptionJobStateDidChange),
name: SubscriptionManager.SubscriptionJobQueueDidFailJobNotification,
object: nil)
}
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
fetchCurrentSubscription()
// If we're the root view, add a cancel button
if navigationController?.viewControllers.first == self {
navigationItem.leftBarButtonItem = .init(
barButtonSystemItem: .done,
target: self,
action: #selector(didTapDone)
)
}
}
@objc
func didTapDone() {
self.dismiss(animated: true)
}
@objc
func subscriptionRedemptionJobStateDidChange(notification: NSNotification) {
updateTableContents()
}
func fetchCurrentSubscription() {
// Fetch current subscription state
var subscriberID: Data?
var currencyCode: String?
SDSDatabaseStorage.shared.read { transaction in
subscriberID = SubscriptionManager.getSubscriberID(transaction: transaction)
currencyCode = SubscriptionManager.getSubscriberCurrencyCode(transaction: transaction)
}
if let subscripberID = subscriberID, let currencyCode = currencyCode {
self.persistedSubscriberID = subscriberID
self.persistedSubscriberCurrencyCode = currencyCode
firstly {
SubscriptionManager.getCurrentSubscriptionStatus(for: subscripberID)
}.done(on: .main) { subscription in
self.fetchedSubscriptionStatus = true
self.currentSubscription = subscription
self.updateTableContents()
self.avatarView.reloadDataIfNecessary()
}.catch { error in
owsFailDebug("Failed to fetch subscription \(error)")
}
} else {
self.currentSubscription = nil
if self.fetchedSubscriptionStatus {
// Dispatch to next run loop to avoid reentrant db calls
DispatchQueue.main.async {
self.updateTableContents()
}
} else {
self.fetchedSubscriptionStatus = true
}
}
}
func updateTableContents() {
let contents = OWSTableContents()
defer {
self.contents = contents
}
let section = OWSTableSection()
section.hasBackground = false
contents.addSection(section)
section.customHeaderView = {
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(top: 0, left: 19, bottom: 20, right: 19)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.addArrangedSubview(avatarView)
stackView.setCustomSpacing(16, after: avatarView)
// Title text
let titleLabel = UILabel()
titleLabel.textAlignment = .center
titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
titleLabel.text = NSLocalizedString(
"SUSTAINER_VIEW_TITLE",
comment: "Title for the signal sustainer view"
)
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel)
stackView.setCustomSpacing(20, after: titleLabel)
if subscriptionViewState == .subscriptionNotYetSetUp || subscriptionViewState == .subscriptionUpdating {
descriptionTextView.attributedText = .composed(of: [NSLocalizedString("SUSTAINER_VIEW_WHY_DONATE_BODY", comment: "The body text for the signal sustainer view"), " ", NSLocalizedString("SUSTAINER_VIEW_READ_MORE", comment: "Read More tappable text in sustainer view body").styled(with: .link(SupportConstants.subscriptionFAQURL))]).styled(with: .color(Theme.primaryTextColor), .font(.ows_dynamicTypeBody))
descriptionTextView.textAlignment = .center
descriptionTextView.linkTextAttributes = [
.foregroundColor: Theme.accentBlueColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
descriptionTextView.delegate = self
stackView.addArrangedSubview(descriptionTextView)
}
return stackView
}()
// Footer setup
bottomFooterStackView.axis = .vertical
bottomFooterStackView.alignment = .center
bottomFooterStackView.layer.backgroundColor = self.tableBackgroundColor.cgColor
bottomFooterStackView.layoutMargins = UIEdgeInsets(top: 10, leading: 23, bottom: 10, trailing: 23)
bottomFooterStackView.spacing = 16
bottomFooterStackView.isLayoutMarginsRelativeArrangement = true
bottomFooterStackView.removeAllSubviews()
switch subscriptionViewState {
case .loading:
buildTableForPendingState(contents: contents, section: section)
case .subscriptionNotYetSetUp:
buildTableForPendingSubscriptionState(contents: contents, section: section)
case .subscriptionExists:
buildTableForExistingSubscriptionState(contents: contents, section: section)
case .subscriptionUpdating:
buildTableForUpdatingSubscriptionState(contents: contents, section: section)
}
UIView.performWithoutAnimation {
self.shouldHideBottomFooter = !(self.subscriptionViewState == .subscriptionNotYetSetUp || self.subscriptionViewState == .subscriptionUpdating)
}
}
private func buildTableForPendingSubscriptionState(contents: OWSTableContents, section: OWSTableSection) {
if DonationUtilities.isApplePayAvailable {
if let subscriptions = self.subscriptions {
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
let cell = self.newCell()
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 8
stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 20, trailing: 0)
stackView.isLayoutMarginsRelativeArrangement = true
cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
let label = UILabel()
label.font = .ows_dynamicTypeBodyClamped
label.textColor = Theme.primaryTextColor
label.text = NSLocalizedString(
"SUSTAINER_VIEW_CURRENCY",
comment: "Set currency label in sustainer view"
)
stackView.addArrangedSubview(label)
let picker = OWSButton { [weak self] in
guard let self = self else { return }
let vc = CurrencyPickerViewController(
dataSource: StripeCurrencyPickerDataSource(currentCurrencyCode: self.currencyCode)
) { [weak self] currencyCode in
self?.currencyCode = currencyCode
}
self.navigationController?.pushViewController(vc, animated: true)
}
picker.setAttributedTitle(NSAttributedString.composed(of: [
self.currencyCode,
Special.noBreakSpace,
NSAttributedString.with(
image: #imageLiteral(resourceName: "chevron-down-18").withRenderingMode(.alwaysTemplate),
font: .ows_regularFont(withSize: 17)
).styled(
with: .color(Self.bubbleBorderColor)
)
]).styled(
with: .font(.ows_regularFont(withSize: 17)),
.color(Theme.primaryTextColor)
), for: .normal)
picker.setBackgroundImage(UIImage(color: Self.bubbleBackgroundColor), for: .normal)
picker.setBackgroundImage(UIImage(color: Self.bubbleBackgroundColor.withAlphaComponent(0.8)), for: .highlighted)
let pillView = PillView()
pillView.layer.borderWidth = Self.bubbleBorderWidth
pillView.layer.borderColor = Self.bubbleBorderColor.cgColor
pillView.clipsToBounds = true
pillView.addSubview(picker)
picker.autoPinEdgesToSuperviewEdges()
picker.autoSetDimension(.width, toSize: 74, relation: .greaterThanOrEqual)
stackView.addArrangedSubview(pillView)
pillView.autoSetDimension(.height, toSize: 36, relation: .greaterThanOrEqual)
let leadingSpacer = UIView.hStretchingSpacer()
let trailingSpacer = UIView.hStretchingSpacer()
stackView.insertArrangedSubview(leadingSpacer, at: 0)
stackView.addArrangedSubview(trailingSpacer)
leadingSpacer.autoMatch(.width, to: .width, of: trailingSpacer)
return cell
},
actionBlock: {}
))
buildSubscriptionLevelCells(subscriptions: subscriptions, section: section)
}
let applePayContributeButton = newPaymentButton()
bottomFooterStackView.addArrangedSubview(applePayContributeButton)
applePayContributeButton.autoSetDimension(.height, toSize: 48, relation: .greaterThanOrEqual)
applePayContributeButton.autoPinWidthToSuperview(withMargin: 23)
}
}
private func buildTableForExistingSubscriptionState(contents: OWSTableContents, section: OWSTableSection) {
let subscriptionPending = subscriptionRedemptionPending
let subscriptionFailed = subscriptionRedemptionFailed
// My Support header
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
let cell = self.newCell()
let titleLabel = UILabel()
titleLabel.text = NSLocalizedString("SUSTAINER_VIEW_MY_SUPPORT", comment: "Existing subscriber support header")
titleLabel.font = .ows_dynamicTypeBody.ows_semibold
titleLabel.numberOfLines = 0
cell.contentView.addSubview(titleLabel)
titleLabel.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 24, bottom: 14, trailing: 24))
return cell
},
actionBlock: {}
))
// Current support level
if let currentSubscription = currentSubscription, let subscriptions = subscriptions {
let level = currentSubscription.level
var subscriptionLevel: SubscriptionLevel?
for subscription in subscriptions {
if subscription.level == level {
subscriptionLevel = subscription
break
}
}
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
let cell = self.newCell()
let containerStackView = UIStackView()
containerStackView.axis = .vertical
containerStackView.alignment = .center
containerStackView.layoutMargins = UIEdgeInsets(top: 16, leading: 30, bottom: 16, trailing: 30)
containerStackView.isLayoutMarginsRelativeArrangement = true
containerStackView.spacing = 16
cell.contentView.addSubview(containerStackView)
containerStackView.autoPinEdgesToSuperviewEdges()
// Background view
let background = UIView()
background.backgroundColor = self.cellBackgroundColor
background.layer.cornerRadius = 12
containerStackView.addSubview(background)
background.autoPinEdgesToSuperviewEdges(withInsets: UIEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.spacing = 10
containerStackView.addArrangedSubview(stackView)
stackView.autoPinWidthToSuperviewMargins()
guard let subscription = subscriptionLevel else {
owsFailDebug("Can't find a matching description")
return cell
}
if let subscription = subscriptionLevel {
let imageView = UIImageView()
imageView.setContentHuggingHigh()
if let badgeImage = subscription.badge.assets?.universal160 {
imageView.image = badgeImage
}
stackView.addArrangedSubview(imageView)
imageView.autoSetDimensions(to: CGSize(square: 64))
if subscriptionPending {
stackView.addSubview(self.redemptionLoadingSpinner)
self.redemptionLoadingSpinner.autoPin(toEdgesOf: imageView, withInset: UIEdgeInsets(hMargin: 14, vMargin: 14))
let progress = self.redemptionLoadingSpinner.currentProgress
self.redemptionLoadingSpinner.play(fromProgress: progress, toProgress: 1)
} else {
self.redemptionLoadingSpinner.removeFromSuperview()
}
imageView.alpha = subscriptionPending || subscriptionFailed ? 0.5 : 1
}
let textStackView = UIStackView()
textStackView.axis = .vertical
textStackView.alignment = .leading
textStackView.spacing = 4
let localizedBadgeName = subscription.name
let titleLabel = UILabel()
titleLabel.text = localizedBadgeName
titleLabel.font = .ows_dynamicTypeBody.ows_semibold
titleLabel.numberOfLines = 0
let pricingLabel = UILabel()
if let persistedCurrencyCode = self.persistedSubscriberCurrencyCode, let price = subscription.currency[persistedCurrencyCode] {
let pricingFormat = NSLocalizedString("SUSTAINER_VIEW_PRICING", comment: "Pricing text for sustainer view badges, embeds {{price}}")
let currencyString = DonationUtilities.formatCurrency(price, currencyCode: self.currencyCode)
pricingLabel.text = String(format: pricingFormat, currencyString)
pricingLabel.font = .ows_dynamicTypeBody2
pricingLabel.numberOfLines = 0
}
var statusText: NSMutableAttributedString?
if subscriptionPending {
let text = NSLocalizedString("SUSTAINER_VIEW_PROCESSING_TRANSACTION", comment: "Status text while processing a badge redemption")
statusText = NSMutableAttributedString(string: text, attributes: [.foregroundColor: UIColor.ows_gray45, .font: UIFont.ows_dynamicTypeBody2])
} else if subscriptionFailed {
let helpFormat = NSLocalizedString("SUSTAINER_VIEW_CANT_ADD_BADGE", comment: "Couldn't add badge text, embeds {{link to contact support}}")
let contactSupport = NSLocalizedString("SUSTAINER_VIEW_CONTACT_SUPPORT", comment: "Contact support link")
let text = String(format: helpFormat, contactSupport)
let attributedText = NSMutableAttributedString(string: text, attributes: [.foregroundColor: UIColor.ows_gray45, .font: UIFont.ows_dynamicTypeBody2])
attributedText.addAttributes([.link: NSURL()], range: NSRange(location: text.utf16.count - contactSupport.utf16.count, length: contactSupport.utf16.count))
statusText = attributedText
} else {
let renewalFormat = NSLocalizedString("SUSTAINER_VIEW_RENEWAL", comment: "Renewal date text for sustainer view level, embeds {{renewal date}}")
let renewalDate = Date(timeIntervalSince1970: currentSubscription.endOfCurrentPeriod)
let renewalString = self.dateFormatter.string(from: renewalDate)
let text = String(format: renewalFormat, renewalString)
statusText = NSMutableAttributedString(string: text, attributes: [.foregroundColor: UIColor.ows_gray45, .font: UIFont.ows_dynamicTypeBody2])
}
self.statusLabel.attributedText = statusText
self.statusLabel.linkTextAttributes = [
.foregroundColor: Theme.accentBlueColor,
.underlineColor: UIColor.clear,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
self.statusLabel.delegate = self
textStackView.addArrangedSubviews([titleLabel, pricingLabel, self.statusLabel])
stackView.addArrangedSubview(textStackView)
let addBoostString = NSLocalizedString("SUSTAINER_VIEW_ADD_BOOST", comment: "Sustainer view Add Boost button title")
let addBoostButton = OWSButton(title: addBoostString) { [weak self] in
guard let self = self else { return }
self.present(BoostSheetView(), animated: true)
}
addBoostButton.dimsWhenHighlighted = true
addBoostButton.layer.cornerRadius = 8
addBoostButton.backgroundColor = Theme.accentBlueColor
containerStackView.addArrangedSubview(addBoostButton)
addBoostButton.autoSetDimension(.height, toSize: 48)
addBoostButton.autoPinWidthToSuperviewMargins()
if subscriptionPending || subscriptionFailed {
addBoostButton.isHighlighted = true
addBoostButton.isUserInteractionEnabled = false
}
return cell
},
actionBlock: {}
))
}
// Management disclosure
let managementSection = OWSTableSection()
contents.addSection(managementSection)
if subscriptionPending {
managementSection.add(.item(icon: .settingsManage,
tintColor: .ows_gray40,
name: NSLocalizedString("SUBSCRIBER_MANAGE_SUBSCRIPTION", comment: "Title for the 'Manage Subscription' button in sustainer view."),
maxNameLines: 0,
textColor: .ows_gray40,
accessoryText: nil,
accessoryType: .none,
accessoryImage: nil,
accessoryView: nil,
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "manageSubscription"),
actionBlock: nil))
managementSection.add(.item(icon: .settingsBadges,
tintColor: .ows_gray40,
name: NSLocalizedString("SUBSCRIBER_BADGES", comment: "Title for the 'Badges' button in sustainer view."),
maxNameLines: 0,
textColor: .ows_gray40,
accessoryText: nil,
accessoryType: .none,
accessoryImage: nil,
accessoryView: nil,
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "badges"),
actionBlock: nil))
} else {
managementSection.add(.disclosureItem(
icon: .settingsManage,
name: NSLocalizedString("SUBSCRIBER_MANAGE_SUBSCRIPTION", comment: "Title for the 'Manage Subscription' button in sustainer view."),
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "manageSubscription"),
actionBlock: { [weak self] in
guard let self = self else { return }
if let subscriptions = self.subscriptions, let currentSubscription = self.currentSubscription, let persistedSubscriberID = self.persistedSubscriberID, let persistedSubscriberCurrencyCode = self.persistedSubscriberCurrencyCode {
let managementController = SubscriptionViewController(updatingSubscriptionsState: true, subscriptions: subscriptions, currentSubscription: currentSubscription, subscriberID: persistedSubscriberID, subscriberCurrencyCode: persistedSubscriberCurrencyCode)
self.navigationController?.pushViewController(managementController, animated: true)
}
}
))
managementSection.add(.disclosureItem(
icon: .settingsBadges,
name: NSLocalizedString("SUBSCRIBER_BADGES", comment: "Title for the 'Badges' button in sustainer view."),
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "badges"),
actionBlock: { [weak self] in
if let self = self {
let vc = BadgeConfigurationViewController(fetchingDataFromLocalProfileWithDelegate: self)
self.navigationController?.pushViewController(vc, animated: true)
}
}
))
}
managementSection.add(.disclosureItem(
icon: .settingsHelp,
name: NSLocalizedString("SUBSCRIBER_SUBSCRIPTION_FAQ", comment: "Title for the 'Subscription FAQ' button in sustainer view."),
accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "subscriptionFAQ"),
actionBlock: { [weak self] in
let vc = SFSafariViewController(url: SupportConstants.subscriptionFAQURL)
self?.present(vc, animated: true, completion: nil)
}
))
}
private func buildTableForPendingState(contents: OWSTableContents, section: OWSTableSection) {
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
let cell = self.newCell()
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(top: 16, leading: 0, bottom: 16, trailing: 0)
stackView.isLayoutMarginsRelativeArrangement = true
cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
let activitySpinner: UIActivityIndicatorView
if #available(iOS 13, *) {
activitySpinner = UIActivityIndicatorView(style: .medium)
} else {
activitySpinner = UIActivityIndicatorView(style: .gray)
}
activitySpinner.startAnimating()
stackView.addArrangedSubview(activitySpinner)
return cell
},
actionBlock: {}
))
}
private func buildTableForUpdatingSubscriptionState(contents: OWSTableContents, section: OWSTableSection) {
if let subscriptions = subscriptions {
buildSubscriptionLevelCells(subscriptions: subscriptions, section: section)
}
let applePayContributeButton = newPaymentButton()
bottomFooterStackView.addArrangedSubview(applePayContributeButton)
applePayContributeButton.autoSetDimension(.height, toSize: 48, relation: .greaterThanOrEqual)
applePayContributeButton.autoPinWidthToSuperview(withMargin: 23)
if let currentSubscription = currentSubscription, let selectedSubscription = selectedSubscription {
let enabled = currentSubscription.level != selectedSubscription.level
applePayContributeButton.isEnabled = enabled
}
let cancelButtonString = NSLocalizedString("SUSTAINER_VIEW_CANCEL_SUBSCRIPTION", comment: "Sustainer view Cancel Subscription button title")
let cancelButton = OWSButton(title: cancelButtonString) { [weak self] in
guard let self = self else { return }
let title = NSLocalizedString("SUSTAINER_VIEW_CANCEL_SUBSCRIPTION_CONFIRMATION_TITLE", comment: "Confirm Cancellation? Action sheet title")
let message = NSLocalizedString("SUSTAINER_VIEW_CANCEL_SUBSCRIPTION_CONFIRMATION_MESSAGE", comment: "Confirm Cancellation? Action sheet message")
let confirm = NSLocalizedString("SUSTAINER_VIEW_CANCEL_SUBSCRIPTION_CONFIRMATION_CONFIRM", comment: "Confirm Cancellation? Action sheet confirm button")
let notNow = NSLocalizedString("SUSTAINER_VIEW_SUBSCRIPTION_CONFIRMATION_NOT_NOW", comment: "Sustainer view Not Now Action sheet button")
let actionSheet = ActionSheetController(title: title, message: message)
actionSheet.addAction(ActionSheetAction(
title: confirm,
style: .default,
handler: { [weak self] _ in
self?.cancelSubscription()
}
))
actionSheet.addAction(ActionSheetAction(
title: notNow,
style: .cancel,
handler: nil
))
self.presentActionSheet(actionSheet)
}
cancelButton.setTitleColor(Theme.accentBlueColor, for: .normal)
cancelButton.dimsWhenHighlighted = true
bottomFooterStackView.addArrangedSubview(cancelButton)
}
private func buildSubscriptionLevelCells(subscriptions: [SubscriptionLevel], section: OWSTableSection) {
for (index, subscription) in subscriptions.enumerated() {
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
let cell = self.newSubscriptionCell()
cell.subscriptionID = subscription.level
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(top: index == 0 ? 16 : 28, leading: 34, bottom: 16, trailing: 34)
stackView.isLayoutMarginsRelativeArrangement = true
stackView.spacing = 10
cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
let isSelected = self.selectedSubscription?.level == subscription.level
// Background view
let background = UIView()
background.backgroundColor = self.cellBackgroundColor
background.layer.borderWidth = Self.bubbleBorderWidth
background.layer.borderColor = isSelected ? Theme.accentBlueColor.cgColor : Self.bubbleBorderColor.cgColor
background.layer.cornerRadius = 12
stackView.addSubview(background)
background.autoPinEdgesToSuperviewEdges(withInsets: UIEdgeInsets(top: index == 0 ? 0 : 12, leading: 24, bottom: 0, trailing: 24))
let badge = subscription.badge
let imageView = UIImageView()
imageView.setContentHuggingHigh()
if let badgeImage = badge.assets?.universal160 {
imageView.image = badgeImage
}
stackView.addArrangedSubview(imageView)
imageView.autoSetDimensions(to: CGSize(square: 64))
let textStackView = UIStackView()
textStackView.axis = .vertical
textStackView.alignment = .leading
textStackView.spacing = 4
let titleStackView = UIStackView()
titleStackView.axis = .horizontal
titleStackView.distribution = .fill
titleStackView.spacing = 4
let localizedBadgeName = subscription.name
let titleLabel = UILabel()
titleLabel.text = localizedBadgeName
titleLabel.font = .ows_dynamicTypeBody.ows_semibold
titleLabel.numberOfLines = 0
titleLabel.setContentHuggingHorizontalHigh()
titleLabel.setCompressionResistanceHorizontalHigh()
titleStackView.addArrangedSubview(titleLabel)
let isUpdating = self.subscriptionViewState == .subscriptionUpdating
let isCurrent = isUpdating && self.currentSubscription?.level == subscription.level
if isCurrent {
titleStackView.addArrangedSubview(.hStretchingSpacer())
let checkmark = UIImageView(image: UIImage(named: "check-20")?.withRenderingMode(.alwaysTemplate))
checkmark.tintColor = Theme.primaryTextColor
titleStackView.addArrangedSubview(checkmark)
checkmark.setContentHuggingHorizontalLow()
checkmark.setCompressionResistanceHorizontalHigh()
}
let descriptionLabel = UILabel()
let descriptionFormat = NSLocalizedString("SUSTAINER_VIEW_BADGE_DESCRIPTION", comment: "Description text for sustainer view badges, embeds {{localized badge name}}")
descriptionLabel.text = String(format: descriptionFormat, subscription.badge.localizedName)
descriptionLabel.font = .ows_dynamicTypeBody2
descriptionLabel.numberOfLines = 0
let pricingLabel = UILabel()
let currencyCode = isUpdating ? self.persistedSubscriberCurrencyCode ?? self.currencyCode : self.currencyCode
if let price = subscription.currency[currencyCode] {
let pricingFormat = NSLocalizedString("SUSTAINER_VIEW_PRICING", comment: "Pricing text for sustainer view badges, embeds {{price}}")
let currencyString = DonationUtilities.formatCurrency(price, currencyCode: currencyCode)
pricingLabel.numberOfLines = 0
if !isCurrent {
pricingLabel.text = String(format: pricingFormat, currencyString)
pricingLabel.font = .ows_dynamicTypeBody2
} else {
if let currentSubscription = self.currentSubscription {
let pricingString = String(format: pricingFormat, currencyString)
let renewalFormat = currentSubscription.cancelAtEndOfPeriod ? NSLocalizedString("SUSTAINER_VIEW_PRICING_EXPIRATION", comment: "Renewal text for sustainer view management badges, embeds {{Expiration}}") : NSLocalizedString("SUSTAINER_VIEW_PRICING_RENEWAL", comment: "Expiration text for sustainer view management badges, embeds {{Expiration}}")
let renewalDate = Date(timeIntervalSince1970: currentSubscription.endOfCurrentPeriod)
let renewalString = String(format: renewalFormat, self.dateFormatter.string(from: renewalDate))
let attributedString = NSMutableAttributedString(string: pricingString + renewalString)
attributedString.addAttributesToEntireString([.font: UIFont.ows_dynamicTypeBody2, .foregroundColor: Theme.primaryTextColor])
attributedString.addAttributes([.foregroundColor: UIColor.ows_gray45], range: NSRange(location: pricingString.utf16.count, length: renewalString.utf16.count))
pricingLabel.attributedText = attributedString
}
}
}
textStackView.addArrangedSubviews([titleStackView, descriptionLabel, pricingLabel])
stackView.addArrangedSubview(textStackView)
return cell
},
actionBlock: {
self.selectedSubscription = subscription
self.updateTableContents()
}
))
}
}
private func cancelSubscription() {
guard let persistedSubscriberID = persistedSubscriberID else {
owsFailDebug("Asked to cancel subscription but no persisted subscriberID")
return
}
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { modal in
firstly {
try SubscriptionManager.cancelSubscription(for: persistedSubscriberID)
}.done(on: .main) {
modal.dismiss {
if let navController = self.navigationController {
self.view.presentToast(
text: NSLocalizedString("SUSTAINER_VIEW_SUBSCRIPTION_CANCELLED", comment: "Toast indicating that the subscription has been cancelled"), fromViewController: navController)
navController.popViewController(animated: true)
}
}
}.catch { error in
modal.dismiss {}
owsFailDebug("Failed to cancel subscription \(error)")
}
}
}
private func newCell() -> UITableViewCell {
let cell = OWSTableItem.newCell()
cell.selectionStyle = .none
cell.layoutMargins = cellOuterInsets
cell.contentView.layoutMargins = .zero
return cell
}
private func newSubscriptionCell() -> SubscriptionLevelCell {
let cell = SubscriptionLevelCell()
OWSTableItem.configureCell(cell)
cell.layoutMargins = cellOuterInsets
cell.contentView.layoutMargins = .zero
cell.selectionStyle = .none
return cell
}
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
updateTableContents()
}
func presentBadgeCantBeAddedSheet() {
let title = NSLocalizedString("SUSTAINER_VIEW_CANT_ADD_BADGE_TITLE", comment: "Action sheet title for Couldn't Add Badge sheet")
let message = NSLocalizedString("SUSTAINER_VIEW_CANT_ADD_BADGE_MESSAGE", comment: "Action sheet message for Couldn't Add Badge sheet")
let actionSheet = ActionSheetController(title: title, message: message)
actionSheet.addAction(ActionSheetAction(
title: NSLocalizedString("CONTACT_SUPPORT", comment: "Button text to initiate an email to signal support staff"),
style: .default,
handler: { [weak self] _ in
let localizedSheetTitle = NSLocalizedString("EMAIL_SIGNAL_TITLE",
comment: "Title for the fallback support sheet if user cannot send email")
let localizedSheetMessage = NSLocalizedString("EMAIL_SIGNAL_MESSAGE",
comment: "Description for the fallback support sheet if user cannot send email")
guard ComposeSupportEmailOperation.canSendEmails else {
let fallbackSheet = ActionSheetController(title: localizedSheetTitle,
message: localizedSheetMessage)
let buttonTitle = NSLocalizedString("BUTTON_OKAY", comment: "Label for the 'okay' button.")
fallbackSheet.addAction(ActionSheetAction(title: buttonTitle, style: .default))
self?.presentActionSheet(fallbackSheet)
return
}
let supportVC = ContactSupportViewController()
let navVC = OWSNavigationController(rootViewController: supportVC)
self?.presentFormSheet(navVC, animated: true)
}
))
actionSheet.addAction(ActionSheetAction(
title: NSLocalizedString("SUSTAINER_VIEW_SUBSCRIPTION_CONFIRMATION_NOT_NOW", comment: "Sustainer view Not Now Action sheet button"),
style: .cancel,
handler: nil
))
self.navigationController?.topViewController?.presentActionSheet(actionSheet)
}
func presentStillProcessingSheet() {
let title = NSLocalizedString("SUSTAINER_STILL_PROCESSING_BADGE_TITLE", comment: "Action sheet title for Still Processing Badge sheet")
let message = NSLocalizedString("SUSTAINER_VIEW_STILL_PROCESSING_BADGE_MESSAGE", comment: "Action sheet message for Still Processing Badge sheet")
let actionSheet = ActionSheetController(title: title, message: message)
actionSheet.addAction(OWSActionSheets.okayAction)
self.navigationController?.topViewController?.presentActionSheet(actionSheet)
}
func presentReadMoreSheet() {
let readMoreSheet = SubscriptionReadMoreSheet()
self.present(readMoreSheet, animated: true)
}
}
extension SubscriptionViewController: PKPaymentAuthorizationControllerDelegate {
fileprivate func newPaymentButton() -> PKPaymentButton {
let applePayContributeButton = PKPaymentButton(
paymentButtonType: .donate,
paymentButtonStyle: Theme.isDarkThemeEnabled ? .white : .black
)
if #available(iOS 12, *) { applePayContributeButton.cornerRadius = 12 }
applePayContributeButton.addTarget(self, action: #selector(self.requestApplePayDonation), for: .touchUpInside)
return applePayContributeButton
}
@objc
fileprivate func requestApplePayDonation() {
guard let subscription = selectedSubscription else {
owsFailDebug("No selected subscription, can't invoke Apple Pay donation")
return
}
let currencyCode = subscriptionViewState == .subscriptionUpdating ? persistedSubscriberCurrencyCode ?? currencyCode : currencyCode
guard let subscriptionAmount = subscription.currency[currencyCode] else {
owsFailDebug("Failed to get amount for current currency code")
return
}
if subscriptionViewState == .subscriptionUpdating {
var currencyString: String = ""
if let selectedSubscription = selectedSubscription, let price = selectedSubscription.currency[currencyCode] {
currencyString = DonationUtilities.formatCurrency(price, currencyCode: currencyCode)
}
let title = NSLocalizedString("SUSTAINER_VIEW_UPDATE_SUBSCRIPTION_CONFIRMATION_TITLE", comment: "Update Subscription? Action sheet title")
let message = String(format: NSLocalizedString("SUSTAINER_VIEW_UPDATE_SUBSCRIPTION_CONFIRMATION_MESSAGE", comment: "Update Subscription? Action sheet message, embeds {{Price}}"), currencyString)
let confirm = NSLocalizedString("SUSTAINER_VIEW_UPDATE_SUBSCRIPTION_CONFIRMATION_UPDATE", comment: "Update Subscription? Action sheet confirm button")
let notNow = NSLocalizedString("SUSTAINER_VIEW_SUBSCRIPTION_CONFIRMATION_NOT_NOW", comment: "Sustainer view Not Now Action sheet button")
let actionSheet = ActionSheetController(title: title, message: message)
actionSheet.addAction(ActionSheetAction(
title: confirm,
style: .default,
handler: { [weak self] _ in
if let currencyCode = self?.persistedSubscriberCurrencyCode {
self?.presentApplePay(for: subscriptionAmount, currencyCode: currencyCode)
}
}
))
actionSheet.addAction(ActionSheetAction(
title: notNow,
style: .cancel,
handler: nil
))
self.presentActionSheet(actionSheet)
} else {
presentApplePay(for: subscriptionAmount, currencyCode: currencyCode)
}
}
private func presentApplePay(for amount: NSDecimalNumber, currencyCode: String) {
guard let subscription = selectedSubscription else {
owsFailDebug("No selected subscription, can't invoke Apple Pay donation")
return
}
guard let subscriptionAmount = subscription.currency[currencyCode] else {
owsFailDebug("Failed to get amount for current currency code")
return
}
guard !Stripe.isAmountTooSmall(subscriptionAmount, in: currencyCode) else {
owsFailDebug("Subscription amount is too small per Stripe API")
return
}
guard !Stripe.isAmountTooLarge(subscriptionAmount, in: currencyCode) else {
owsFailDebug("Subscription amount is too large per Stripe API")
return
}
let request = DonationUtilities.newPaymentRequest(for: subscriptionAmount, currencyCode: currencyCode)
let paymentController = PKPaymentAuthorizationController(paymentRequest: request)
paymentController.delegate = self
SubscriptionManager.terminateTransactionIfPossible = false
paymentController.present { presented in
if !presented { owsFailDebug("Failed to present payment controller") }
}
}
func paymentAuthorizationController(
_ controller: PKPaymentAuthorizationController,
didAuthorizePayment payment: PKPayment,
handler completion: @escaping (PKPaymentAuthorizationResult) -> Void
) {
guard let selectedSubscription = self.selectedSubscription else {
owsFailDebug("No currently selected subscription")
let authResult = PKPaymentAuthorizationResult(status: .failure, errors: nil)
completion(authResult)
return
}
if subscriptionViewState == .subscriptionUpdating, let priorSubscriptionLevel = self.subscriptionLevelForSubscription(self.currentSubscription),
let subscriberID = self.persistedSubscriberID,
let currencyCode = self.persistedSubscriberCurrencyCode {
firstly {
return try SubscriptionManager.updateSubscriptionLevel(for: subscriberID, from: priorSubscriptionLevel, to: selectedSubscription, payment: payment, currencyCode: currencyCode)
}.done(on: .main) {
let authResult = PKPaymentAuthorizationResult(status: .success, errors: nil)
completion(authResult)
SubscriptionManager.terminateTransactionIfPossible = false
self.fetchAndRedeemReceipts(newSubscriptionLevel: selectedSubscription)
}.catch { error in
let authResult = PKPaymentAuthorizationResult(status: .failure, errors: [error])
completion(authResult)
SubscriptionManager.terminateTransactionIfPossible = false
owsFailDebug("Error setting up subscription, \(error)")
}
} else {
firstly {
return try SubscriptionManager.setupNewSubscription(subscription: selectedSubscription, payment: payment, currencyCode: self.currencyCode)
}.done(on: .main) {
let authResult = PKPaymentAuthorizationResult(status: .success, errors: nil)
completion(authResult)
SubscriptionManager.terminateTransactionIfPossible = false
self.fetchAndRedeemReceipts(newSubscriptionLevel: selectedSubscription)
}.catch { error in
let authResult = PKPaymentAuthorizationResult(status: .failure, errors: [error])
completion(authResult)
SubscriptionManager.terminateTransactionIfPossible = false
owsFailDebug("Error setting up subscription, \(error)")
}
}
}
func paymentAuthorizationControllerDidFinish(_ controller: PKPaymentAuthorizationController) {
SubscriptionManager.terminateTransactionIfPossible = true
controller.dismiss()
}
func fetchAndRedeemReceipts(newSubscriptionLevel: SubscriptionLevel, priorSubscriptionLevel: SubscriptionLevel? = nil) {
guard let subscriberID = databaseStorage.read(
block: { SubscriptionManager.getSubscriberID(transaction: $0) }
) else {
return owsFailDebug("Did not fetch subscriberID")
}
do {
try SubscriptionManager.requestAndRedeemRecieptsIfNecessary(for: subscriberID,
subscriptionLevel: newSubscriptionLevel.level,
priorSubscriptionLevel: priorSubscriptionLevel?.level ?? 0)
} catch {
owsFailDebug("Failed to redeem receipts \(error)")
}
let backdropView = UIView()
backdropView.backgroundColor = Theme.backdropColor
backdropView.alpha = 0
view.addSubview(backdropView)
backdropView.autoPinEdgesToSuperviewEdges()
let progressViewContainer = UIView()
progressViewContainer.backgroundColor = Theme.backgroundColor
progressViewContainer.layer.cornerRadius = 12
backdropView.addSubview(progressViewContainer)
progressViewContainer.autoCenterInSuperview()
let progressView = AnimatedProgressView(loadingText: NSLocalizedString("SUSTAINER_VIEW_PROCESSING_PAYMENT", comment: "Loading indicator on the sustainer view"))
view.addSubview(progressView)
progressView.autoCenterInSuperview()
progressViewContainer.autoMatch(.width, to: .width, of: progressView, withOffset: 32)
progressViewContainer.autoMatch(.height, to: .height, of: progressView, withOffset: 32)
progressView.startAnimating {
backdropView.alpha = 1
}
enum SubscriptionError: Error { case timeout, assertion }
Promise.race(
NotificationCenter.default.observe(
once: SubscriptionManager.SubscriptionJobQueueDidFinishJobNotification,
object: nil
),
NotificationCenter.default.observe(
once: SubscriptionManager.SubscriptionJobQueueDidFailJobNotification,
object: nil
)
).timeout(seconds: 30) {
return SubscriptionError.timeout
}.done { notification in
if notification.name == SubscriptionManager.SubscriptionJobQueueDidFailJobNotification {
throw SubscriptionError.assertion
}
progressView.stopAnimating(success: true) {
backdropView.alpha = 0
} completion: {
backdropView.removeFromSuperview()
progressView.removeFromSuperview()
if self.subscriptionViewState == .subscriptionUpdating {
self.navigationController?.popViewController(animated: true)
} else {
self.fetchCurrentSubscription()
}
ExperienceUpgradeManager.snoozeExperienceUpgradeWithSneakyTransaction(.subscriptionMegaphone)
// Only show the badge sheet if redemption succeeds.
if let selectedSubscription = self.selectedSubscription {
self.navigationController?.topViewController?.present(BadgeThanksSheet(badge: selectedSubscription.badge), animated: true)
} else {
owsFailDebug("Missing selected subscription after redemption.")
}
}
}.catch { error in
progressView.stopAnimating(success: false) {
backdropView.alpha = 0
} completion: {
backdropView.removeFromSuperview()
progressView.removeFromSuperview()
if self.subscriptionViewState == .subscriptionUpdating {
self.navigationController?.popViewController(animated: true)
} else {
self.fetchCurrentSubscription()
}
guard let error = error as? SubscriptionError else {
return owsFailDebug("Unexpected error \(error)")
}
switch error {
case .timeout:
self.presentStillProcessingSheet()
case .assertion:
self.presentBadgeCantBeAddedSheet()
}
}
}
}
func subscriptionLevelForSubscription( _ subscription: Subscription?) -> SubscriptionLevel? {
guard let subscription = subscription else {
return nil
}
guard let subscriptionLevels = subscriptions else {
return nil
}
for subscriptionLevel in subscriptionLevels {
if subscriptionLevel.level == subscription.level {
return subscriptionLevel
}
}
return nil
}
}
extension SubscriptionViewController: BadgeConfigurationDelegate {
func badgeConfiguration(_ vc: BadgeConfigurationViewController, didCompleteWithBadgeSetting setting: BadgeConfiguration) {
if !self.reachabilityManager.isReachable {
OWSActionSheets.showErrorAlert(
message: NSLocalizedString("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
}
firstly { () -> Promise<Void> in
let snapshot = profileManagerImpl.localProfileSnapshot(shouldIncludeAvatar: true)
let allBadges = snapshot.profileBadgeInfo ?? []
let oldVisibleBadges = allBadges.filter { $0.isVisible ?? true }
let oldVisibleBadgeIds = oldVisibleBadges.map { $0.badgeId }
let newVisibleBadgeIds: [String]
switch setting {
case .doNotDisplayPublicly:
newVisibleBadgeIds = []
case .display(featuredBadge: let newFeaturedBadge):
let allBadgeIds = allBadges.map { $0.badgeId }
guard allBadgeIds.contains(newFeaturedBadge.badgeId) else {
throw OWSAssertionError("Invalid badge")
}
newVisibleBadgeIds = [newFeaturedBadge.badgeId] + allBadgeIds.filter { $0 != newFeaturedBadge.badgeId }
}
if oldVisibleBadgeIds != newVisibleBadgeIds {
Logger.info("Updating visible badges from \(oldVisibleBadgeIds) to \(newVisibleBadgeIds)")
vc.showDismissalActivity = true
return OWSProfileManager.updateLocalProfilePromise(
profileGivenName: snapshot.givenName,
profileFamilyName: snapshot.familyName,
profileBio: snapshot.bio,
profileBioEmoji: snapshot.bioEmoji,
profileAvatarData: snapshot.avatarData,
visibleBadgeIds: newVisibleBadgeIds,
userProfileWriter: .localUser)
} else {
return Promise.value(())
}
}.done {
self.navigationController?.popViewController(animated: true)
}.catch { error in
owsFailDebug("Failed to update profile: \(error)")
self.navigationController?.popViewController(animated: true)
}
}
func badgeConfirmationDidCancel(_: BadgeConfigurationViewController) {
self.navigationController?.popViewController(animated: true)
}
}
extension SubscriptionViewController: UITextViewDelegate {
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
if textView == statusLabel {
presentBadgeCantBeAddedSheet()
} else if textView == descriptionTextView {
presentReadMoreSheet()
}
return false
}
}
private class SubscriptionLevelCell: UITableViewCell {
public var subscriptionID: UInt = 0
}
private class SubscriptionReadMoreSheet: InteractiveSheetViewController {
let contentScrollView = UIScrollView()
let stackView = UIStackView()
let handleContainer = UIView()
override var interactiveScrollViews: [UIScrollView] { [contentScrollView] }
override var minHeight: CGFloat { min(740, CurrentAppContext().frame.height - (view.safeAreaInsets.top + 32)) }
override var maximizedHeight: CGFloat { minHeight }
override var renderExternalHandle: Bool { false }
// MARK: -
override public func viewDidLoad() {
super.viewDidLoad()
contentView.backgroundColor = Theme.tableView2PresentedBackgroundColor
// We add the handle directly to the content view,
// so that it doesn't scroll with the table.
handleContainer.backgroundColor = Theme.tableView2PresentedBackgroundColor
contentView.addSubview(handleContainer)
handleContainer.autoPinWidthToSuperview()
handleContainer.autoPinEdge(toSuperviewEdge: .top)
let handle = UIView()
handle.backgroundColor = Theme.tableView2PresentedSeparatorColor
handle.autoSetDimensions(to: CGSize(width: 36, height: 5))
handle.layer.cornerRadius = 5 / 2
handleContainer.addSubview(handle)
handle.autoPinHeightToSuperview(withMargin: 12)
handle.autoHCenterInSuperview()
contentView.addSubview(contentScrollView)
stackView.axis = .vertical
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 24)
stackView.isLayoutMarginsRelativeArrangement = true
contentScrollView.addSubview(stackView)
stackView.autoPinHeightToSuperview()
stackView.autoPinWidth(toWidthOf: view)
contentScrollView.autoPinWidthToSuperview()
contentScrollView.autoPinEdge(toSuperviewEdge: .bottom)
contentScrollView.autoPinEdge(.top, to: .bottom, of: handleContainer)
buildContents()
}
override func themeDidChange() {
super.themeDidChange()
handleContainer.backgroundColor = Theme.tableView2PresentedBackgroundColor
contentView.backgroundColor = Theme.tableView2PresentedBackgroundColor
}
private func buildContents() {
// Header image
let image = UIImage(named: "sustainer-heart")
let imageView = UIImageView(image: image)
imageView.contentMode = .scaleAspectFit
stackView.addArrangedSubview(imageView)
stackView.setCustomSpacing(12, after: imageView)
// Header label
let titleLabel = UILabel()
titleLabel.textAlignment = .natural
titleLabel.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
titleLabel.text = NSLocalizedString(
"SUSTAINER_READ_MORE_TITLE",
comment: "Title for the signal sustainer read more view"
)
titleLabel.numberOfLines = 0
titleLabel.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel)
stackView.setCustomSpacing(12, after: titleLabel)
let firstDescriptionBlock = UILabel()
firstDescriptionBlock.textAlignment = .natural
firstDescriptionBlock.font = .ows_dynamicTypeBody
firstDescriptionBlock.text = NSLocalizedString(
"SUSTAINER_READ_MORE_DESCRIPTION_BLOCK_ONE",
comment: "First block of description text in read more sheet"
)
firstDescriptionBlock.numberOfLines = 0
firstDescriptionBlock.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(firstDescriptionBlock)
stackView.setCustomSpacing(32, after: firstDescriptionBlock)
let titleLabel2 = UILabel()
titleLabel2.textAlignment = .natural
titleLabel2.font = UIFont.ows_dynamicTypeTitle2.ows_semibold
titleLabel2.text = NSLocalizedString(
"SUSTAINER_READ_MORE_WHY_CONTRIBUTE",
comment: "Why Contribute title for the signal sustainer read more view"
)
titleLabel2.numberOfLines = 0
titleLabel2.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(titleLabel2)
stackView.setCustomSpacing(12, after: titleLabel2)
let secondDescriptionBlock = UILabel()
secondDescriptionBlock.textAlignment = .natural
secondDescriptionBlock.font = .ows_dynamicTypeBody
secondDescriptionBlock.text = NSLocalizedString(
"SUSTAINER_READ_MORE_DESCRIPTION_BLOCK_TWO",
comment: "Second block of description text in read more sheet"
)
secondDescriptionBlock.numberOfLines = 0
secondDescriptionBlock.lineBreakMode = .byWordWrapping
stackView.addArrangedSubview(secondDescriptionBlock)
}
}