Merge branch 'eb/badgeExpiry'

This commit is contained in:
Eugene Bistolas 2021-12-03 12:47:16 -10:00
commit c35cadfa79
9 changed files with 656 additions and 115 deletions

View File

@ -1008,6 +1008,7 @@
F00385FF273F6388000B5ABD /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00385FC273F6388000B5ABD /* SubscriptionManager.swift */; };
F0038600273F6388000B5ABD /* DonationUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00385FD273F6388000B5ABD /* DonationUtilities.swift */; };
F0038601273F6388000B5ABD /* Stripe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F00385FE273F6388000B5ABD /* Stripe.swift */; };
F02564D8274EDF4600D7B48A /* BadgeExpirationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F02564D7274EDF4600D7B48A /* BadgeExpirationSheet.swift */; };
F026749627405BF4005853DC /* SubscriptionReceiptCredentialRedemptionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = F026749527405BF4005853DC /* SubscriptionReceiptCredentialRedemptionJob.swift */; };
F05F51C926A90D6B00861034 /* ContextMenuActionsAccessory.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05F51C826A90D6B00861034 /* ContextMenuActionsAccessory.swift */; };
F0B872B6269CF6D900D26481 /* ContextMenuInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0B872B5269CF6D900D26481 /* ContextMenuInteraction.swift */; };
@ -2330,6 +2331,7 @@
F00385FC273F6388000B5ABD /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
F00385FD273F6388000B5ABD /* DonationUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DonationUtilities.swift; sourceTree = "<group>"; };
F00385FE273F6388000B5ABD /* Stripe.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Stripe.swift; sourceTree = "<group>"; };
F02564D7274EDF4600D7B48A /* BadgeExpirationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeExpirationSheet.swift; sourceTree = "<group>"; };
F026749527405BF4005853DC /* SubscriptionReceiptCredentialRedemptionJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionReceiptCredentialRedemptionJob.swift; sourceTree = "<group>"; };
F05F51C826A90D6B00861034 /* ContextMenuActionsAccessory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuActionsAccessory.swift; sourceTree = "<group>"; };
F0B872B5269CF6D900D26481 /* ContextMenuInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuInteraction.swift; sourceTree = "<group>"; };
@ -2583,6 +2585,7 @@
888017912741F33B00346E9A /* BoostViewController.swift */,
8880179327430DDB00346E9A /* BadgeThanksSheet.swift */,
881BB2892743531D00B609B8 /* BadgeDetailsSheet.swift */,
F02564D7274EDF4600D7B48A /* BadgeExpirationSheet.swift */,
887B380725F0355700685845 /* ChatsSettingsViewController.swift */,
3414896825C9B6490098E3ED /* CurrencyPickerViewController.swift */,
887B6DCC25F6C41500E677D4 /* Data Usage */,
@ -6333,6 +6336,7 @@
4C19A0FC227B356F007A0C7F /* DebugUIMessages+OWS.swift in Sources */,
F0B872B8269D079B00D26481 /* ContextMenuConfiguration.swift in Sources */,
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */,
F02564D8274EDF4600D7B48A /* BadgeExpirationSheet.swift in Sources */,
34ACA7E82733159600E47AD4 /* ChangePhoneNumberController.swift in Sources */,
34D1F0841F8678AA0066283D /* ConversationInputToolbar.m in Sources */,
4CBBFE4A2306F5D300B37450 /* LogViewController.swift in Sources */,

View File

@ -67,16 +67,7 @@ class AppSettingsViewController: OWSTableViewController2 {
}
private lazy var hasCurrentSubscription: Bool = {
var subscriberID: Data?
SDSDatabaseStorage.shared.read { transaction in
subscriberID = SubscriptionManager.getSubscriberID(transaction: transaction)
}
guard subscriberID != nil else {
return false
}
return true
return SubscriptionManager.hasCurrentSubscriptionCached()
}()
func updateTableContents() {

View File

@ -0,0 +1,264 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
import UIKit
import SignalMessaging
protocol BadgeExpirationSheetDelegate: AnyObject {
func badgeExpirationSheetActionButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet)
func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet)
}
class BadgeExpirationSheet: InteractiveSheetViewController {
override var interactiveScrollViews: [UIScrollView] { [tableViewController.tableView] }
override var renderExternalHandle: Bool { false }
private var shouldMakeVisibleAndPrimary = false
public var delegate: BadgeExpirationSheetDelegate?
var contentSizeHeight: CGFloat {
tableViewController.tableView.layoutIfNeeded()
return tableViewController.tableView.contentSize.height + tableViewController.tableView.adjustedContentInset.top
}
override var minimizedHeight: CGFloat {
return min(contentSizeHeight, maximizedHeight)
}
override var maximizedHeight: CGFloat {
min(contentSizeHeight, CurrentAppContext().frame.height - (view.safeAreaInsets.top + 32))
}
private let tableViewController = OWSTableViewController2()
private let handleContainer = UIView()
private let badge: ProfileBadge
public var badgeID: String {
return badge.id
}
private lazy var isCurrentSustainer = {
return SubscriptionManager.hasCurrentSubscriptionCached()
}()
required init(badge: ProfileBadge) {
owsAssertDebug(badge.assets != nil)
self.badge = badge
super.init()
tableViewController.shouldDeferInitialLoad = false
updateTableContents()
}
public required init() {
fatalError("init() has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
addChild(tableViewController)
contentView.addSubview(tableViewController.view)
tableViewController.view.autoPinEdgesToSuperviewEdges()
// 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 = tableViewController.separatorColor
handle.autoSetDimensions(to: CGSize(width: 36, height: 5))
handle.layer.cornerRadius = 5 / 2
handleContainer.addSubview(handle)
handle.autoPinHeightToSuperview(withMargin: 12)
handle.autoHCenterInSuperview()
updateViewState()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateViewState()
}
private var previousMinimizedHeight: CGFloat?
private func updateViewState() {
if minimizedHeight != previousMinimizedHeight {
heightConstraint?.constant = minimizedHeight
previousMinimizedHeight = minimizedHeight
}
}
override func themeDidChange() {
super.themeDidChange()
handleContainer.backgroundColor = Theme.tableView2PresentedBackgroundColor
updateTableContents()
}
var titleText: String {
switch badge.id {
case "BOOST": return NSLocalizedString(
"BADGE_EXPIRED_BOOST_TITLE",
comment: "Title for boost on the badge expiration sheet."
)
default: return NSLocalizedString(
"BADGE_EXPIRED_SUBSCRIPTION_TITLE",
comment: "Title for subscription on the badge expiration sheet."
)
}
}
var bodyText: String {
var formatText: String
switch (badgeID: badge.id, isSustainer: isCurrentSustainer) {
case (badgeID: "BOOST", isSustainer: true):
formatText = NSLocalizedString(
"BADGE_EXIPRED_BOOST_CURRENT_SUSTAINER_BODY_FORMAT",
comment: "String explaing to the user that their boost badge has expired while they are a current subscription sustainer on the badge expiry sheetsheet."
)
case (badgeID: "BOOST", isSustainer: false):
formatText = NSLocalizedString(
"BADGE_EXIPRED_BOOST_BODY_FORMAT",
comment: "String explaing to the user that their boost badge has expired on the badge expiry sheetsheet."
)
default:
formatText = NSLocalizedString(
"BADGE_EXIPRED_SUBSCRIPTION_BODY_FORMAT",
comment: "String explaing to the user that their subscription badge has expired on the badge expiry sheetsheet. Embed {badge name}."
)
}
return String(format: formatText, badge.localizedName)
}
var actionButtonText: String {
switch (badgeID: badge.id, isSustainer: isCurrentSustainer) {
case (badgeID: "BOOST", isSustainer: true):
return NSLocalizedString("BADGE_EXPIRED_BOOST_RENEWAL_BUTTON_SUSTAINER",
comment: "Button title for boost on the badge expiration sheet, used if the user is already a sustainer.")
case (badgeID: "BOOST", isSustainer: false):
return NSLocalizedString("BADGE_EXPIRED_BOOST_RENEWAL_BUTTON",
comment: "Button title for boost on the badge expiration sheet, used if the user is not already a sustainer.")
default:
return NSLocalizedString("BADGE_EXPIRED_SUBSCRIPTION_RENEWAL_BUTTON",
comment: "Button title for subscription on the badge expiration sheet.")
}
}
private func updateTableContents() {
let contents = OWSTableContents()
defer { tableViewController.contents = contents }
let headerSection = OWSTableSection()
headerSection.hasBackground = false
headerSection.customHeaderHeight = 1
contents.addSection(headerSection)
headerSection.add(.init(customCellBlock: { [weak self] in
let cell = OWSTableItem.newCell()
guard let self = self else { return cell }
cell.selectionStyle = .none
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(hMargin: 24, vMargin: 0)
stackView.isLayoutMarginsRelativeArrangement = true
cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
let badgeImageView = UIImageView()
badgeImageView.image = self.badge.assets?.universal112
badgeImageView.autoSetDimensions(to: CGSize(square: 112))
stackView.addArrangedSubview(badgeImageView)
stackView.setCustomSpacing(16, after: badgeImageView)
let titleLabel = UILabel()
titleLabel.font = .ows_dynamicTypeTitle2.ows_semibold
titleLabel.textColor = Theme.primaryTextColor
titleLabel.textAlignment = .center
titleLabel.numberOfLines = 0
titleLabel.text = self.titleText
stackView.addArrangedSubview(titleLabel)
stackView.setCustomSpacing(12, after: titleLabel)
let bodyLabel = UILabel()
bodyLabel.font = .ows_dynamicTypeBody
bodyLabel.textColor = Theme.primaryTextColor
bodyLabel.textAlignment = .center
bodyLabel.numberOfLines = 0
bodyLabel.text = self.bodyText
stackView.addArrangedSubview(bodyLabel)
stackView.setCustomSpacing(30, after: bodyLabel)
return cell
}, actionBlock: nil))
let buttonSection = OWSTableSection()
buttonSection.hasBackground = false
contents.addSection(buttonSection)
buttonSection.add(.init(customCellBlock: { [weak self] in
let cell = OWSTableItem.newCell()
cell.selectionStyle = .none
guard let self = self else { return cell }
let stackView = UIStackView()
stackView.axis = .vertical
stackView.alignment = .center
stackView.layoutMargins = UIEdgeInsets(top: 30, left: 24, bottom: 0, right: 24)
stackView.spacing = 16
stackView.isLayoutMarginsRelativeArrangement = true
cell.contentView.addSubview(stackView)
stackView.autoPinEdgesToSuperviewEdges()
let actionButton = OWSFlatButton.button(title: self.actionButtonText,
font: UIFont.ows_dynamicTypeBody.ows_semibold,
titleColor: .white,
backgroundColor: .ows_accentBlue,
target: self,
selector: #selector(self.didTapAction))
actionButton.autoSetHeightUsingFont()
actionButton.cornerRadius = 8
stackView.addArrangedSubview(actionButton)
actionButton.autoPinWidthToSuperviewMargins()
let notNowButton = OWSButton(title: CommonStrings.notNowButton) { [weak self] in
guard let self = self else { return }
self.didTapNotNow()
}
notNowButton.setTitleColor(Theme.accentBlueColor, for: .normal)
notNowButton.dimsWhenHighlighted = true
stackView.addArrangedSubview(notNowButton)
return cell
}, actionBlock: nil))
}
public override func willDismissInteractively() {
didTapNotNow()
super.willDismissInteractively()
}
@objc
func didTapAction() {
if let delegate = delegate {
dismiss(animated: true, completion: nil)
delegate.badgeExpirationSheetActionButtonTapped(self)
}
}
@objc
func didTapNotNow() {
dismiss(animated: true, completion: nil)
}
}

View File

@ -27,7 +27,19 @@ class SubscriptionViewController: OWSTableViewController2 {
} else if fetchedSubscriptionStatus == false || subscriptions == nil {
return .loading
} else {
return currentSubscription != nil ? .subscriptionExists : .subscriptionNotYetSetUp
guard let currentSubscription = currentSubscription else {
return .subscriptionNotYetSetUp
}
if !ignoreProfileBadgeStateForNewBadgeRedemption && profileSubscriptionBadges.isEmpty {
return .subscriptionNotYetSetUp
}
if currentSubscription.active {
return .subscriptionExists
} else {
return (subscriptionRedemptionFailureReason == .none) ? .subscriptionExists : .subscriptionNotYetSetUp
}
}
}
@ -40,12 +52,24 @@ class SubscriptionViewController: OWSTableViewController2 {
}
}
private var currentSubscription: Subscription?
private lazy var profileSubscriptionBadges: [OWSUserProfileBadgeInfo] = {
return SubscriptionManager.currentProfileSubscriptionBadges()
}()
private var selectedSubscription: SubscriptionLevel?
private var fetchedSubscriptionStatus = false
private var fetchedSubscriptionStatus = false {
didSet {
if fetchedSubscriptionStatus {
showBadgeExpirySheetIfNeeded()
}
}
}
private var ignoreProfileBadgeStateForNewBadgeRedemption = false
private var updatingSubscriptions = false
private var currentSubscription: Subscription?
private var persistedSubscriberID: Data?
private var persistedSubscriberCurrencyCode: String?
@ -218,11 +242,11 @@ class SubscriptionViewController: OWSTableViewController2 {
currencyCode = SubscriptionManager.getSubscriberCurrencyCode(transaction: transaction)
}
if let subscripberID = subscriberID, let currencyCode = currencyCode {
if let subscriberID = subscriberID, let currencyCode = currencyCode {
self.persistedSubscriberID = subscriberID
self.persistedSubscriberCurrencyCode = currencyCode
firstly {
SubscriptionManager.getCurrentSubscriptionStatus(for: subscripberID)
SubscriptionManager.getCurrentSubscriptionStatus(for: subscriberID)
}.done(on: .main) { subscription in
self.fetchedSubscriptionStatus = true
self.currentSubscription = subscription
@ -244,6 +268,31 @@ class SubscriptionViewController: OWSTableViewController2 {
}
}
func showBadgeExpirySheetIfNeeded() {
guard subscriptionViewState == .subscriptionNotYetSetUp, let subscriptions = subscriptions else {
return
}
let expiredBadgeID = SubscriptionManager.mostRecentlyExpiredBadgeIDWithSneakyTransaction()
guard let expiredBadgeID = expiredBadgeID else {
return
}
let subscriptionLevel = subscriptions.first { $0.badge.id == expiredBadgeID }
guard let subscriptionLevel = subscriptionLevel else {
owsFailDebug("Unable to find current subscription level for expired badge")
return
}
selectedSubscription = subscriptionLevel
updateTableContents()
let sheet = BadgeExpirationSheet(badge: subscriptionLevel.badge)
sheet.delegate = self
self.present(sheet, animated: true)
}
func updateTableContents() {
let contents = OWSTableContents()
defer {
@ -473,16 +522,8 @@ class SubscriptionViewController: OWSTableViewController2 {
))
// 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
}
}
let subscriptionLevel = subscriptionLevelForSubscription(currentSubscription)
if let subscriptionLevel = subscriptionLevel, let currentSubscription = currentSubscription {
section.add(.init(
customCellBlock: { [weak self] in
guard let self = self else { return UITableViewCell() }
@ -512,15 +553,10 @@ class SubscriptionViewController: OWSTableViewController2 {
containerStackView.addArrangedSubview(stackView)
stackView.autoPinWidthToSuperviewMargins()
guard let subscription = subscriptionLevel else {
owsFailDebug("Can't find a matching description")
return cell
}
let imageView = UIImageView()
imageView.setContentHuggingHigh()
if let badgeImage = subscription.badge.assets?.universal160 {
if let badgeImage = subscriptionLevel.badge.assets?.universal160 {
imageView.image = badgeImage
}
@ -543,7 +579,7 @@ class SubscriptionViewController: OWSTableViewController2 {
textStackView.alignment = .leading
textStackView.spacing = 4
let localizedBadgeName = subscription.name
let localizedBadgeName = subscriptionLevel.name
let titleLabel = UILabel()
titleLabel.text = localizedBadgeName
titleLabel.font = .ows_dynamicTypeBody.ows_semibold
@ -1217,6 +1253,8 @@ extension SubscriptionViewController: PKPaymentAuthorizationControllerDelegate {
throw SubscriptionError.assertion
}
self.ignoreProfileBadgeStateForNewBadgeRedemption = true
progressView.stopAnimating(success: true) {
backdropView.alpha = 0
} completion: {
@ -1484,3 +1522,15 @@ private class SubscriptionReadMoreSheet: InteractiveSheetViewController {
stackView.addArrangedSubview(secondDescriptionBlock)
}
}
extension SubscriptionViewController: BadgeExpirationSheetDelegate {
func badgeExpirationSheetActionButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) {
requestApplePayDonation()
}
func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) {
SubscriptionManager.clearMostRecentlyExpiredBadgeIDWithSneakyTransaction()
}
}

View File

@ -465,6 +465,8 @@ NSString *const kArchiveButtonPseudoGroup = @"kArchiveButtonPseudoGroup";
[self.searchResultsController viewDidAppear:animated];
[self showBadgeExpirationSheetIfNeeded];
self.hasEverAppeared = YES;
}

View File

@ -3,6 +3,8 @@
//
import Foundation
import SignalCoreKit
import SignalServiceKit
@objc
public extension HomeViewController {
@ -51,6 +53,57 @@ public extension HomeViewController {
// MARK: -
func showBadgeExpirationSheetIfNeeded() {
guard !hasEverAppeared else { // Do this once per launch
return
}
let expiredBadgeID = SubscriptionManager.mostRecentlyExpiredBadgeIDWithSneakyTransaction()
guard let expiredBadgeID = expiredBadgeID, expiredBadgeID != "BOOST" else { // TODO Add boost support
return
}
let shouldShow = SDSDatabaseStorage.shared.read { transaction in
SubscriptionManager.showExpirySheetOnHomeScreenKey(transaction: transaction)
}
guard shouldShow else {
return
}
// Fetch current subscriptions, required to populate badge assets
firstly {
SubscriptionManager.getSubscriptions()
}.done(on: .global()) { (subscriptions: [SubscriptionLevel]) in
for level in subscriptions {
if level.badge.id == expiredBadgeID {
// Fetch assets
firstly {
self.profileManager.badgeStore.populateAssetsOnBadge(level.badge)
}.done(on: .main) {
SDSDatabaseStorage.shared.write { transaction in
SubscriptionManager.setShowExpirySheetOnHomeScreenKey(show: false, transaction: transaction)
}
let badgeSheet = BadgeExpirationSheet(badge: level.badge)
badgeSheet.delegate = self
self.present(badgeSheet, animated: true)
}.catch { error in
owsFailDebug("Failed to fetch badge assets for expiry \(error)")
}
break
}
}
}.catch { error in
owsFailDebug("Failed to fetch subscriptions for expiry \(error)")
}
}
// MARK: -
func configureUnreadPaymentsBannerSingle(_ paymentsReminderView: UIView,
paymentModel: TSPaymentModel,
transaction: SDSAnyReadTransaction) {
@ -226,6 +279,8 @@ public enum ShowAppSettingsMode {
case paymentsTransferIn
case appearance
case avatarBuilder
case subscriptions
case boost
}
// MARK: -
@ -299,8 +354,24 @@ public extension HomeViewController {
let profile = ProfileSettingsViewController()
viewControllers += [ profile ]
completion = { profile.presentAvatarSettingsView() }
case .subscriptions:
let subscriptions = SubscriptionViewController()
viewControllers += [ subscriptions ]
case .boost:
let boost = BoostViewController()
viewControllers += [ boost ]
}
navigationController.setViewControllers(viewControllers, animated: false)
presentFormSheet(navigationController, animated: true, completion: completion)
}
}
extension HomeViewController: BadgeExpirationSheetDelegate {
func badgeExpirationSheetActionButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) {
SubscriptionManager.clearMostRecentlyExpiredBadgeIDWithSneakyTransaction()
let mode: ShowAppSettingsMode = badgeExpirationSheet.badgeID == "BOOST" ? .boost : .subscriptions
showAppSettings(mode: mode)
}
func badgeExpirationSheetNotNowButtonTapped(_ badgeExpirationSheet: BadgeExpirationSheet) { }
}

View File

@ -385,6 +385,30 @@
/* Text prompting the user to boost */
"BADGE_DETAILS_GIVE_A_BOOST" = "Give Signal a Boost";
/* String explaing to the user that their boost badge has expired on the badge expiry sheetsheet. */
"BADGE_EXIPRED_BOOST_BODY_FORMAT" = "Your Boost badge has expired, and is no longer visible on your profile.\n\nYou can continue using Signal but to support technology that is built for you, please consider becoming a monthly Sustainer.";
/* String explaing to the user that their boost badge has expired while they are a current subscription sustainer on the badge expiry sheetsheet. */
"BADGE_EXIPRED_BOOST_CURRENT_SUSTAINER_BODY_FORMAT" = "Your Boost badge has expired, and is no longer visible on your profile.\n\nYou can reactivate your Boost badge for another 30 days with a one-time contribution.";
/* String explaing to the user that their subscription badge has expired on the badge expiry sheetsheet. Embed {badge name}. */
"BADGE_EXIPRED_SUBSCRIPTION_BODY_FORMAT" = "Your Sustainer subscription was automatically cancelled because you were inactive for too long. Your %@ badge is no longer visible on your profile.\n\nYou can keep using Signal but to support the app and reactivate your badge, renew now.";
/* Button title for boost on the badge expiration sheet, used if the user is not already a sustainer. */
"BADGE_EXPIRED_BOOST_RENEWAL_BUTTON" = "Become a Sustainer";
/* Button title for boost on the badge expiration sheet, used if the user is already a sustainer. */
"BADGE_EXPIRED_BOOST_RENEWAL_BUTTON_SUSTAINER" = "Add a Boost";
/* Title for boost on the badge expiration sheet. */
"BADGE_EXPIRED_BOOST_TITLE" = "Badge Expired";
/* Button title for subscription on the badge expiration sheet. */
"BADGE_EXPIRED_SUBSCRIPTION_RENEWAL_BUTTON" = "Renew Subscription";
/* Title for subscription on the badge expiration sheet. */
"BADGE_EXPIRED_SUBSCRIPTION_TITLE" = "Subscription Cancelled";
/* Title for boost on the thank you sheet. */
"BADGE_THANKS_BOOST_TITLE" = "Thanks for the Boost!";

View File

@ -165,6 +165,7 @@ public class SubscriptionReceiptCredentailRedemptionOperation: OWSOperation, Dur
)
}.done(on: .global()) {
Logger.info("[Subscriptions] Successfully redeemed receipt credential presentation")
SubscriptionManager.mostRecentlyExpiredBadgeIDWithSneakyTransaction()
self.didSucceed()
}.catch(on: .global()) { error in
self.reportError(error)

View File

@ -91,12 +91,17 @@ public class SubscriptionManager: NSObject {
public static let SubscriptionJobQueueDidFinishJobNotification = NSNotification.Name("SubscriptionJobQueueDidFinishJobNotification")
public static let SubscriptionJobQueueDidFailJobNotification = NSNotification.Name("SubscriptionJobQueueDidFailJobNotification")
private static let subscriptionKVS = SDSKeyValueStore(collection: "SubscriptionKeyValueStore")
private static let subscriberIDKey = "subscriberID"
private static let subscriberCurrencyCodeKey = "subscriberCurrencyCode"
private static let lastSubscriptionExpirationKey = "subscriptionExpiration"
private static let lastSubscriptionHeartbeatKey = "subscriptionHeartbeat"
private static let lastSubscriptionReceiptRedemptionFailedKey = "lastSubscriptionReceiptRedemptionFailedKey"
private static let userManuallyCancelledSubscriptionKey = "userManuallyCancelledSubscriptionKey"
fileprivate static let subscriberIDKey = "subscriberID"
fileprivate static let subscriberCurrencyCodeKey = "subscriberCurrencyCode"
fileprivate static let lastSubscriptionExpirationKey = "subscriptionExpiration"
fileprivate static let lastSubscriptionHeartbeatKey = "subscriptionHeartbeat"
fileprivate static let lastSubscriptionReceiptRedemptionFailedKey = "lastSubscriptionReceiptRedemptionFailedKey"
fileprivate static let userManuallyCancelledSubscriptionKey = "userManuallyCancelledSubscriptionKey"
fileprivate static let knownUserSubscriptionBadgeIDsKey = "knownUserSubscriptionBadgeIDsKey"
fileprivate static let knownUserBoostBadgeIDsKey = "knownUserBoostBadgeIDsKey"
fileprivate static let mostRecentlyExpiredBadgeIDKey = "mostRecentlyExpiredBadgeIDKey"
fileprivate static let showExpirySheetOnHomeScreenKey = "showExpirySheetOnHomeScreenKey"
public static var terminateTransactionIfPossible = false
@ -155,8 +160,22 @@ public class SubscriptionManager: NSObject {
return false
}
return lastSubscriptionExpiryDate.isAfterNow
if currentProfileSubscriptionBadges().count == 0 {
return false
}
return lastSubscriptionExpiryDate.isAfterNow
}
public class func currentProfileSubscriptionBadges() -> [OWSUserProfileBadgeInfo] {
let snapshot = profileManagerImpl.localProfileSnapshot(shouldIncludeAvatar: true)
let profileBadges = snapshot.profileBadgeInfo ?? []
return profileBadges.compactMap { (badge: OWSUserProfileBadgeInfo) -> OWSUserProfileBadgeInfo? in
if badge.badgeId != "BOOST" {
return badge
}
return nil
}
}
public class func getCurrentSubscriptionStatus(for subscriberID: Data) -> Promise<Subscription?> {
@ -189,77 +208,6 @@ public class SubscriptionManager: NSObject {
// MARK: Subscription management
public static func getSubscriberID(transaction: SDSAnyReadTransaction) -> Data? {
guard let subscriberID = subscriptionKVS.getObject(
forKey: subscriberIDKey,
transaction: transaction
) as? Data else {
return nil
}
return subscriberID
}
public static func setSubscriberID(_ subscriberID: Data?, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(subscriberID,
key: subscriberIDKey,
transaction: transaction)
}
public static func getSubscriberCurrencyCode(transaction: SDSAnyReadTransaction) -> String? {
guard let subscriberCurrencyCode = subscriptionKVS.getObject(
forKey: subscriberCurrencyCodeKey,
transaction: transaction
) as? String else {
return nil
}
return subscriberCurrencyCode
}
public static func setSubscriberCurrencyCode(_ currencyCode: String?, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(currencyCode,
key: subscriberCurrencyCodeKey,
transaction: transaction)
}
public static func setLastReceiptRedemptionFailed(failureReason: SubscriptionRedemptionFailureReason, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setInt(failureReason.rawValue, key: lastSubscriptionReceiptRedemptionFailedKey, transaction: transaction)
}
public static func lastReceiptRedemptionFailed(transaction: SDSAnyReadTransaction) -> SubscriptionRedemptionFailureReason {
let intValue = subscriptionKVS.getInt(lastSubscriptionReceiptRedemptionFailedKey, transaction: transaction)
guard let intValue = intValue else {
return .none
}
if let reason = SubscriptionRedemptionFailureReason(rawValue: intValue) {
return reason
} else {
return .none
}
}
public static func userManuallyCancelledSubscription(transaction: SDSAnyReadTransaction) -> Bool {
return subscriptionKVS.getBool(userManuallyCancelledSubscriptionKey, transaction: transaction) ?? false
}
private static func setUserManuallyCancelledSubscription(_ userCancelled: Bool, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setBool(userCancelled, key: userManuallyCancelledSubscriptionKey, transaction: transaction)
}
private static func lastSubscriptionExpirationDate(transaction: SDSAnyReadTransaction) -> Date? {
return subscriptionKVS.getDate(lastSubscriptionExpirationKey, transaction: transaction)
}
private static func setLastSubscriptionExpirationDate(_ expirationDate: Date?, transaction: SDSAnyWriteTransaction) {
if let expirationDate = expirationDate {
subscriptionKVS.setDate(expirationDate, key: lastSubscriptionExpirationKey, transaction: transaction)
} else {
subscriptionKVS.removeValue(forKey: lastSubscriptionExpirationKey, transaction: transaction)
}
OWSSyncManager.shared.sendFetchLatestSubscriptionStatusSyncMessage()
}
private class func setupNewSubscriberID() throws -> Promise<Data> {
Logger.info("[Subscriptions] Setting up new subscriber ID")
let newSubscriberID = generateSubscriberID()
@ -306,6 +254,7 @@ public class SubscriptionManager: NSObject {
generatedSubscriberID = subscriberID
SDSDatabaseStorage.shared.write { transaction in
self.setUserManuallyCancelledSubscription(false, transaction: transaction)
self.setSubscriberID(subscriberID, transaction: transaction)
self.setSubscriberCurrencyCode(currencyCode, transaction: transaction)
self.storageServiceManager.recordPendingLocalAccountUpdates()
@ -675,11 +624,6 @@ public class SubscriptionManager: NSObject {
@objc
public class func performSubscriptionKeepAliveIfNecessary() {
guard tsAccountManager.isPrimaryDevice else {
Logger.info("[Subscriptions] Bailing out of heartbeat, this is not the primary device")
return
}
// Kick job queue
subscriptionJobQueue.runAnyQueuedRetry()
@ -709,6 +653,14 @@ public class SubscriptionManager: NSObject {
Logger.info("[Subscriptions] Performing subscription heartbeat")
// Update badge states first
mostRecentlyExpiredBadgeIDWithSneakyTransaction()
guard tsAccountManager.isPrimaryDevice else {
Logger.info("[Subscriptions] Bailing out of remaining heartbeat tasks, this is not the primary device")
return
}
guard let subscriberID = subscriberID, currencyCode != nil else {
Logger.info("[Subscriptions] No subscription + currency code found")
self.updateSubscriptionHeartbeatDate()
@ -751,6 +703,65 @@ public class SubscriptionManager: NSObject {
}
}
// Reconciles cached non-expired badge IDs with current profile badges, and returns the last expired ID if one exists.
// Also sets mostRecentlyExpiredBadgeID
public static func mostRecentlyExpiredBadgeIDWithSneakyTransaction() -> String? {
Logger.info("[Subscriptions] Checking if current badge set has changed")
// Get current badges
let snapshot = profileManagerImpl.localProfileSnapshot(shouldIncludeAvatar: true)
let currentBadges: [OWSUserProfileBadgeInfo] = snapshot.profileBadgeInfo ?? []
let currentSubscriberBadgeIDs = currentBadges.compactMap { (badge: OWSUserProfileBadgeInfo) -> String? in
if badge.badgeId != "BOOST" {
return badge.badgeId
}
return nil
}
let currentBoostBadgeIDs = currentBadges.compactMap { (badge: OWSUserProfileBadgeInfo) -> String? in
if badge.badgeId == "BOOST" {
return badge.badgeId
}
return nil
}
var persistedSubscriberBadgeIDs: [String] = [], persistedBoostBadgeIDs: [String] = [], expiringBadgeID: String?, userManuallyCancelled = false, showExpiryOnHomeScreen = false
SDSDatabaseStorage.shared.read { transaction in
persistedSubscriberBadgeIDs = self.knownUserSubscriptionBadgeIDs(transaction: transaction)
persistedBoostBadgeIDs = self.knownUserBoostBadgeIDs(transaction: transaction)
expiringBadgeID = self.mostRecentlyExpiredBadgeID(transaction: transaction)
userManuallyCancelled = self.userManuallyCancelledSubscription(transaction: transaction)
showExpiryOnHomeScreen = self.showExpirySheetOnHomeScreenKey(transaction: transaction)
}
var newExpiringBadgeID: String?
if currentSubscriberBadgeIDs.count == 0 && persistedSubscriberBadgeIDs.count > 0 && !userManuallyCancelled {
newExpiringBadgeID = persistedSubscriberBadgeIDs.first
} else if currentBoostBadgeIDs.count == 0 && persistedBoostBadgeIDs.count > 0 {
newExpiringBadgeID = persistedBoostBadgeIDs.first
}
if let newExpiringBadgeID = newExpiringBadgeID, newExpiringBadgeID != expiringBadgeID {
expiringBadgeID = newExpiringBadgeID
showExpiryOnHomeScreen = true
}
Logger.info("[Subscriptions] Current sub badges \(currentSubscriberBadgeIDs.count), persisted sub badges \(persistedSubscriberBadgeIDs.count)")
Logger.info("[Subscriptions] Current boost badges \(currentBoostBadgeIDs.count), persisted sub badges \(persistedBoostBadgeIDs.count)")
Logger.info("[Subscriptions] mostRecentlyExpiredBadgeID is \(expiringBadgeID ?? "none")")
// Persist new values
SDSDatabaseStorage.shared.write { transaction in
self.setKnownUserSubscriptionBadgeIDs(badgeIDs: currentSubscriberBadgeIDs, transaction: transaction)
self.setKnownUserBoostBadgeIDs(badgeIDs: currentBoostBadgeIDs, transaction: transaction)
self.setMostRecentlyExpiredBadgeID(badgeID: expiringBadgeID, transaction: transaction)
self.setShowExpirySheetOnHomeScreenKey(show: showExpiryOnHomeScreen, transaction: transaction)
}
return expiringBadgeID
}
private static func updateSubscriptionHeartbeatDate() {
SDSDatabaseStorage.shared.write { transaction in
// Update keepalive
@ -799,6 +810,129 @@ public class SubscriptionManager: NSObject {
}
}
extension SubscriptionManager {
public static func getSubscriberID(transaction: SDSAnyReadTransaction) -> Data? {
guard let subscriberID = subscriptionKVS.getObject(
forKey: subscriberIDKey,
transaction: transaction
) as? Data else {
return nil
}
return subscriberID
}
public static func setSubscriberID(_ subscriberID: Data?, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(subscriberID,
key: subscriberIDKey,
transaction: transaction)
}
public static func getSubscriberCurrencyCode(transaction: SDSAnyReadTransaction) -> String? {
guard let subscriberCurrencyCode = subscriptionKVS.getObject(
forKey: subscriberCurrencyCodeKey,
transaction: transaction
) as? String else {
return nil
}
return subscriberCurrencyCode
}
public static func setSubscriberCurrencyCode(_ currencyCode: String?, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(currencyCode,
key: subscriberCurrencyCodeKey,
transaction: transaction)
}
public static func setLastReceiptRedemptionFailed(failureReason: SubscriptionRedemptionFailureReason, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setInt(failureReason.rawValue, key: lastSubscriptionReceiptRedemptionFailedKey, transaction: transaction)
}
public static func lastReceiptRedemptionFailed(transaction: SDSAnyReadTransaction) -> SubscriptionRedemptionFailureReason {
let intValue = subscriptionKVS.getInt(lastSubscriptionReceiptRedemptionFailedKey, transaction: transaction)
guard let intValue = intValue else {
return .none
}
if let reason = SubscriptionRedemptionFailureReason(rawValue: intValue) {
return reason
} else {
return .none
}
}
public static func userManuallyCancelledSubscription(transaction: SDSAnyReadTransaction) -> Bool {
return subscriptionKVS.getBool(userManuallyCancelledSubscriptionKey, transaction: transaction) ?? false
}
private static func setUserManuallyCancelledSubscription(_ userCancelled: Bool, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setBool(userCancelled, key: userManuallyCancelledSubscriptionKey, transaction: transaction)
}
fileprivate static func lastSubscriptionExpirationDate(transaction: SDSAnyReadTransaction) -> Date? {
return subscriptionKVS.getDate(lastSubscriptionExpirationKey, transaction: transaction)
}
fileprivate static func setLastSubscriptionExpirationDate(_ expirationDate: Date?, transaction: SDSAnyWriteTransaction) {
guard let expirationDate = expirationDate else {
subscriptionKVS.removeValue(forKey: lastSubscriptionExpirationKey, transaction: transaction)
return
}
subscriptionKVS.setDate(expirationDate, key: lastSubscriptionExpirationKey, transaction: transaction)
}
fileprivate static func setKnownUserSubscriptionBadgeIDs(badgeIDs: [String], transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(badgeIDs, key: knownUserSubscriptionBadgeIDsKey, transaction: transaction)
}
fileprivate static func knownUserSubscriptionBadgeIDs(transaction: SDSAnyReadTransaction) -> [String] {
let ids = subscriptionKVS.getObject(forKey: knownUserSubscriptionBadgeIDsKey, transaction: transaction) as? [String]
return ids ?? []
}
fileprivate static func setKnownUserBoostBadgeIDs(badgeIDs: [String], transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setObject(badgeIDs, key: knownUserBoostBadgeIDsKey, transaction: transaction)
}
fileprivate static func knownUserBoostBadgeIDs(transaction: SDSAnyReadTransaction) -> [String] {
guard let ids = subscriptionKVS.getObject(forKey: knownUserBoostBadgeIDsKey, transaction: transaction) as? [String] else {
return []
}
return ids
}
fileprivate static func setMostRecentlyExpiredBadgeID(badgeID: String?, transaction: SDSAnyWriteTransaction) {
guard let badgeID = badgeID else {
subscriptionKVS.removeValue(forKey: mostRecentlyExpiredBadgeIDKey, transaction: transaction)
return
}
subscriptionKVS.setString(badgeID, key: mostRecentlyExpiredBadgeIDKey, transaction: transaction)
}
public static func mostRecentlyExpiredBadgeID(transaction: SDSAnyReadTransaction) -> String? {
subscriptionKVS.getString(mostRecentlyExpiredBadgeIDKey, transaction: transaction)
}
public static func clearMostRecentlyExpiredBadgeIDWithSneakyTransaction() {
SDSDatabaseStorage.shared.write { transaction in
self.setMostRecentlyExpiredBadgeID(badgeID: nil, transaction: transaction)
}
}
public static func setShowExpirySheetOnHomeScreenKey(show: Bool, transaction: SDSAnyWriteTransaction) {
subscriptionKVS.setBool(show, key: showExpirySheetOnHomeScreenKey, transaction: transaction)
}
public static func showExpirySheetOnHomeScreenKey(transaction: SDSAnyReadTransaction) -> Bool {
return subscriptionKVS.getBool(showExpirySheetOnHomeScreenKey, transaction: transaction) ?? false
}
}
@objc
public class OWSRetryableSubscriptionError: NSObject, CustomNSError, IsRetryableProvider {
@objc