1361 lines
64 KiB
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)
|
|
}
|
|
}
|