Various improvements

This commit is contained in:
Ilya Laktyushin 2023-02-06 18:28:18 +04:00
parent a5bd955643
commit 78b02192cf
7 changed files with 180 additions and 63 deletions

View File

@ -8891,3 +8891,5 @@ Sorry for the inconvenience.";
"Settings.RaiseToListenInfo" = "Raise to Listen allows you to quickly listen and reply to incoming audio messages by raising the phone to your ear.";
"Login.CodeSentCallText" = "Calling **%@** to dictate the code.";
"Premium.Purchase.OnlyOneSubscriptionAllowed" = "You have already purchased Telegram Premium for another account. You can only have one Telegram Premium subscription on one Apple ID.";

View File

@ -308,11 +308,17 @@ public final class InAppPurchaseManager: NSObject {
return signal
}
public func getValidTransactionIds() -> [String] {
public struct ReceiptPurchase: Equatable {
public let productId: String
public let transactionId: String
public let expirationDate: Date
}
public func getReceiptPurchases() -> [ReceiptPurchase] {
guard let data = getReceiptData(), let receipt = parseReceipt(data) else {
return []
}
return receipt.purchases.map { $0.transactionId }
return receipt.purchases.map { ReceiptPurchase(productId: $0.productId, transactionId: $0.transactionId, expirationDate: $0.expirationDate) }
}
}
@ -359,7 +365,6 @@ extension InAppPurchaseManager: SKPaymentTransactionObserver {
switch transaction.transactionState {
case .purchased:
Logger.shared.log("InAppPurchaseManager", "Account \(accountPeerId), transaction \(transaction.transactionIdentifier ?? ""), original transaction \(transaction.original?.transactionIdentifier ?? "none") purchased")
transactionState = .purchased(transactionId: transaction.transactionIdentifier)
transactionsToAssign.append(transaction)
case .restored:

View File

@ -7,6 +7,7 @@ private struct Asn1Tag {
static let sequence: Int32 = 0x10
static let set: Int32 = 0x11
static let utf8String: Int32 = 0x0c
static let date: Int32 = 0x16
}
private struct Asn1Entry {
@ -124,10 +125,12 @@ struct Receipt {
fileprivate struct Tag {
static let productIdentifier: Int32 = 1702
static let transactionIdentifier: Int32 = 1703
static let expirationDate: Int32 = 1708
}
let productId: String
let transactionId: String
let expirationDate: Date
}
let purchases: [Purchase]
@ -192,6 +195,27 @@ func parseReceipt(_ data: Data) -> Receipt? {
return Receipt(purchases: purchases)
}
private func parseRfc3339Date(_ str: String) -> Date? {
let posixLocale = Locale(identifier: "en_US_POSIX")
let formatter1 = DateFormatter()
formatter1.locale = posixLocale
formatter1.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssX5"
formatter1.timeZone = TimeZone(secondsFromGMT: 0)
let result = formatter1.date(from: str)
if result != nil {
return result
}
let formatter2 = DateFormatter()
formatter2.locale = posixLocale
formatter2.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSSSSX5"
formatter2.timeZone = TimeZone(secondsFromGMT: 0)
return formatter2.date(from: str)
}
private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? {
let root = parse(data)
guard root.tag == Asn1Tag.set else {
@ -200,6 +224,7 @@ private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? {
var productId: String?
var transactionId: String?
var expirationDate: Date?
let receiptAttributes = parseSequence(root.data)
for attribute in receiptAttributes {
@ -219,12 +244,16 @@ private func parsePurchaseAttributes(_ data: Data) -> Receipt.Purchase? {
let valEntry = parse(value)
guard valEntry.tag == Asn1Tag.utf8String else { return nil }
transactionId = String(bytes: valEntry.data, encoding: .utf8)
case Receipt.Purchase.Tag.expirationDate:
let valEntry = parse(value)
guard valEntry.tag == Asn1Tag.date else { return nil }
expirationDate = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
default:
break
}
}
guard let productId, let transactionId else {
guard let productId, let transactionId, let expirationDate else {
return nil
}
return Receipt.Purchase(productId: productId, transactionId: transactionId)
return Receipt.Purchase(productId: productId, transactionId: transactionId, expirationDate: expirationDate)
}

View File

@ -1196,14 +1196,28 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let otherPeerName: String?
let products: [PremiumProduct]?
let selectedProductId: String?
let validTransactionIds: [String]
let validPurchases: [InAppPurchaseManager.ReceiptPurchase]
let promoConfiguration: PremiumPromoConfiguration?
let present: (ViewController) -> Void
let selectProduct: (String) -> Void
let buy: () -> Void
let updateIsFocused: (Bool) -> Void
init(context: AccountContext, source: PremiumSource, isPremium: Bool?, justBought: Bool, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, validTransactionIds: [String], promoConfiguration: PremiumPromoConfiguration?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) {
init(
context: AccountContext,
source: PremiumSource,
isPremium: Bool?,
justBought: Bool,
otherPeerName: String?,
products: [PremiumProduct]?,
selectedProductId: String?,
validPurchases: [InAppPurchaseManager.ReceiptPurchase],
promoConfiguration: PremiumPromoConfiguration?,
present: @escaping (ViewController) -> Void,
selectProduct: @escaping (String) -> Void,
buy: @escaping () -> Void,
updateIsFocused: @escaping (Bool) -> Void
) {
self.context = context
self.source = source
self.isPremium = isPremium
@ -1211,7 +1225,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
self.otherPeerName = otherPeerName
self.products = products
self.selectedProductId = selectedProductId
self.validTransactionIds = validTransactionIds
self.validPurchases = validPurchases
self.promoConfiguration = promoConfiguration
self.present = present
self.selectProduct = selectProduct
@ -1241,7 +1255,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
if lhs.selectedProductId != rhs.selectedProductId {
return false
}
if lhs.validTransactionIds != rhs.validTransactionIds {
if lhs.validPurchases != rhs.validPurchases {
return false
}
if lhs.promoConfiguration != rhs.promoConfiguration {
@ -1256,7 +1270,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var products: [PremiumProduct]?
var selectedProductId: String?
var validTransactionIds: [String] = []
var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var isPremium: Bool?
@ -1276,7 +1290,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validTransactionIds.contains(transactionId) {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
@ -1373,7 +1387,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
let state = context.state
state.products = context.component.products
state.selectedProductId = context.component.selectedProductId
state.validTransactionIds = context.component.validTransactionIds
state.validPurchases = context.component.validPurchases
state.isPremium = context.component.isPremium
let theme = environment.theme
@ -1986,7 +2000,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
private(set) var products: [PremiumProduct]?
private(set) var selectedProductId: String?
fileprivate var validTransactionIds: [String] = []
fileprivate var validPurchases: [InAppPurchaseManager.ReceiptPurchase] = []
var isPremium: Bool?
var otherPeerName: String?
@ -2015,7 +2029,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
var canUpgrade: Bool {
if let products = self.products, let current = products.first(where: { $0.isCurrent }), let transactionId = current.transactionId {
if self.validTransactionIds.contains(transactionId) {
if self.validPurchases.contains(where: { $0.transactionId == transactionId }) {
return products.first(where: { $0.months > current.months }) != nil
} else {
return false
@ -2036,7 +2050,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
super.init()
self.validTransactionIds = context.inAppPurchaseManager?.getValidTransactionIds() ?? []
self.validPurchases = context.inAppPurchaseManager?.getReceiptPurchases() ?? []
let availableProducts: Signal<[InAppPurchaseManager.Product], NoError>
if let inAppPurchaseManager = context.inAppPurchaseManager {
@ -2136,8 +2150,29 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
let premiumProduct = self.products?.first(where: { $0.id == self.selectedProductId }), !self.inProgress else {
return
}
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
let isUpgrade = self.products?.first(where: { $0.isCurrent }) != nil
var hasActiveSubsciption = false
if let data = self.context.currentAppConfiguration.with({ $0 }).data, let _ = data["ios_killswitch_disable_receipt_check"] {
} else if !self.validPurchases.isEmpty && !isUpgrade {
let now = Date()
for purchase in self.validPurchases.reversed() {
if (purchase.productId.hasSuffix(".monthly") || purchase.productId.hasSuffix(".annual")) && purchase.expirationDate > now {
hasActiveSubsciption = true
}
}
}
if hasActiveSubsciption {
let errorText = presentationData.strings.Premium_Purchase_OnlyOneSubscriptionAllowed
let alertController = textAlertController(context: self.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
self.present(alertController)
return
}
addAppLogEvent(postbox: self.context.account.postbox, type: "premium.promo_screen_accept")
self.inProgress = true
@ -2173,7 +2208,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_fail")
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let errorText = presentationData.strings.Premium_Purchase_ErrorUnknown
let alertController = textAlertController(context: strongSelf.context, title: nil, text: errorText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
strongSelf.present(alertController)
@ -2198,7 +2232,6 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
strongSelf.updateInProgress(false)
strongSelf.updated(transition: .immediate)
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
var errorText: String?
switch error {
case .generic:
@ -2475,7 +2508,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent {
otherPeerName: state.otherPeerName,
products: state.products,
selectedProductId: state.selectedProductId,
validTransactionIds: state.validTransactionIds,
validPurchases: state.validPurchases,
promoConfiguration: state.promoConfiguration,
present: context.component.present,
selectProduct: { [weak state] productId in

View File

@ -188,7 +188,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
case .x0_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x1:
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.controlColor)))
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor)))
self.rateButton.accessibilityLabel = self.strings.VoiceOver_Media_PlaybackRate
self.rateButton.accessibilityValue = self.strings.VoiceOver_Media_PlaybackRateNormal
self.rateButton.accessibilityHint = self.strings.VoiceOver_Media_PlaybackRateChange
@ -378,7 +378,7 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
case .x0_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "0.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x1:
self.rateButton.setContent(.image(optionsRateImage(rate: "2X", color: self.theme.rootController.navigationBar.controlColor)))
self.rateButton.setContent(.image(optionsRateImage(rate: "1X", color: self.theme.rootController.navigationBar.controlColor)))
case .x1_5:
self.rateButton.setContent(.image(optionsRateImage(rate: "1.5X", color: self.theme.rootController.navigationBar.accentTextColor)))
case .x2:
@ -511,6 +511,8 @@ public final class MediaNavigationAccessoryHeaderNode: ASDisplayNode, UIScrollVi
if let rate = self.playbackBaseRate {
switch rate {
case .x1:
nextRate = .x1_5
case .x1_5:
nextRate = .x2
default:
nextRate = .x1

View File

@ -687,41 +687,41 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
}
strongSelf.context.sharedContext.mediaManager.playlistControl(.setBaseRate(baseRate), type: type)
var hasTooltip = false
strongSelf.forEachController({ controller in
if let controller = controller as? UndoOverlayController {
hasTooltip = true
controller.dismissWithCommitAction()
}
return true
})
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let slowdown: Bool?
if baseRate == .x1 {
slowdown = true
} else if baseRate == .x2 {
slowdown = false
} else {
slowdown = nil
}
if let slowdown = slowdown {
strongSelf.present(
UndoOverlayController(
presentationData: presentationData,
content: .audioRate(
slowdown: slowdown,
text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp
),
elevatedLayout: false,
animateInAsReplacement: hasTooltip,
action: { action in
return true
}
),
in: .current
)
}
// var hasTooltip = false
// strongSelf.forEachController({ controller in
// if let controller = controller as? UndoOverlayController {
// hasTooltip = true
// controller.dismissWithCommitAction()
// }
// return true
// })
//
// let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
// let slowdown: Bool?
// if baseRate == .x1 {
// slowdown = true
// } else if baseRate == .x2 {
// slowdown = false
// } else {
// slowdown = nil
// }
// if let slowdown = slowdown {
// strongSelf.present(
// UndoOverlayController(
// presentationData: presentationData,
// content: .audioRate(
// slowdown: slowdown,
// text: slowdown ? presentationData.strings.Conversation_AudioRateTooltipNormal : presentationData.strings.Conversation_AudioRateTooltipSpeedUp
// ),
// elevatedLayout: false,
// animateInAsReplacement: hasTooltip,
// action: { action in
// return true
// }
// ),
// in: .current
// )
// }
})
}
mediaAccessoryPanel.togglePlayPause = { [weak self] in

View File

@ -35,6 +35,47 @@ private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? {
})
}
private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
return generateImage(CGSize(width: 30.0, height: 16.0), rotatedContext: { size, context in
UIGraphicsPushContext(context)
context.clear(CGRect(origin: CGPoint(), size: size))
let lineWidth = 1.0 + UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(color.cgColor)
let string = NSMutableAttributedString(string: rate, font: Font.with(size: 11.0, design: .round, weight: .bold), textColor: color)
var offset = CGPoint(x: 1.0, y: 0.0)
var width: CGFloat
if rate.count >= 3 {
if rate == "0.5X" {
string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.5
} else {
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
offset.x += -0.3
}
width = 29.0
} else {
string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
width = 19.0
offset.x += -0.3
}
let path = UIBezierPath(roundedRect: CGRect(x: floorToScreenPixels((size.width - width) / 2.0), y: 0.0, width: width, height: 16.0).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 2.0, height: 2.0))
context.addPath(path.cgPath)
context.strokePath()
let boundingRect = string.boundingRect(with: size, options: [], context: nil)
string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + UIScreenPixel + floor((size.height - boundingRect.height) / 2.0)))
UIGraphicsPopContext()
})
}
private let digitsSet = CharacterSet(charactersIn: "0123456789")
private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat {
let text: String
@ -375,8 +416,10 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
}
let baseRate: AudioPlaybackRate
if !value.status.baseRate.isEqual(to: 1.0) {
if value.status.baseRate.isEqual(to: 2.0) {
baseRate = .x2
} else if value.status.baseRate.isEqual(to: 1.5) {
baseRate = .x1_5
} else {
baseRate = .x1
}
@ -715,10 +758,9 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
}
private func updateOrderButton(_ order: MusicPlaybackSettingsOrder) {
let baseColor = self.presentationData.theme.list.itemSecondaryTextColor
switch order {
case .regular:
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: baseColor)
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemSecondaryTextColor)
case .reversed:
self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemAccentColor)
case .random:
@ -741,9 +783,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
private func updateRateButton(_ baseRate: AudioPlaybackRate) {
switch baseRate {
case .x2:
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateActiveIcon(self.presentationData.theme), for: [])
self.rateButton.setImage(optionsRateImage(rate: "2X", color: self.presentationData.theme.list.itemAccentColor), for: [])
case .x1_5:
self.rateButton.setImage(optionsRateImage(rate: "1.5X", color: self.presentationData.theme.list.itemAccentColor), for: [])
default:
self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerMaximizedRateInactiveIcon(self.presentationData.theme), for: [])
self.rateButton.setImage(optionsRateImage(rate: "1X", color: self.presentationData.theme.list.itemSecondaryTextColor), for: [])
}
}
@ -959,12 +1003,14 @@ final class OverlayPlayerControlsNode: ASDisplayNode {
if let currentRate = self.currentRate {
switch currentRate {
case .x1:
nextRate = .x1_5
case .x1_5:
nextRate = .x2
default:
nextRate = .x1
}
} else {
nextRate = .x2
nextRate = .x1_5
}
self.control?(.setBaseRate(nextRate))
}