From e15dca9bcc12f7cc8b276f0faff656f4472b9004 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Mon, 29 Aug 2022 05:25:29 +0200 Subject: [PATCH] Various fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 1 + ...orizationSequenceCodeEntryController.swift | 33 +++- .../AuthorizationSequenceController.swift | 7 +- ...rizationSequenceEmailEntryController.swift | 31 +++- ...tionSequenceEmailEntryControllerNode.swift | 20 ++- ...rizationSequencePhoneEntryController.swift | 60 +++++-- ...tionSequencePhoneEntryControllerNode.swift | 167 ++++++++++++------ submodules/Display/Source/TextNode.swift | 30 ++-- .../Sources/InAppPurchaseManager.swift | 39 +++- .../PremiumUI/Sources/PremiumGiftScreen.swift | 1 - .../Sources/PremiumIntroScreen.swift | 115 ++++++++---- .../PrivacyAndSecurityController.swift | 115 ++++++++---- .../TelegramCore/Sources/Authorization.swift | 2 +- 13 files changed, 438 insertions(+), 183 deletions(-) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index a9b4783420..d5f9f3b2d6 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -7962,6 +7962,7 @@ Sorry for the inconvenience."; "Emoji.FrequentlyUsed" = "Recently Used"; "Premium.Annual" = "Annual"; +"Premium.Semiannual" = "Semiannual"; "Premium.Monthly" = "Monthly"; "Login.PhoneNumberConfirmation" = "Is this the correct number?"; diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift index 4bc44291d0..db80bf494f 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceCodeEntryController.swift @@ -11,6 +11,8 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { return self.displayNode as! AuthorizationSequenceCodeEntryControllerNode } + private var validLayout: ContainerViewLayout? + private let strings: PresentationStrings private let theme: PresentationTheme private let openUrl: (String) -> Void @@ -30,12 +32,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { public var inProgress: Bool = false { didSet { -// if self.inProgress { -// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) -// self.navigationItem.rightBarButtonItem = item -// } else { -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) -// } + self.updateNavigationItems() self.controllerNode.inProgress = self.inProgress } } @@ -52,9 +49,7 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.hasActiveInput = true self.statusBar.statusBarStyle = theme.intro.statusBarStyle.style - -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) - + self.attemptNavigation = { _ in return false } @@ -124,6 +119,19 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { self.controllerNode.animateError(text: text) } + func updateNavigationItems() { + guard let layout = self.validLayout, layout.size.width < 360.0 else { + return + } + + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.theme.rootController.navigationBar.accentTextColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + } + public func updateData(number: String, email: String?, codeType: SentAuthorizationCodeType, nextType: AuthorizationCodeNextType?, timeout: Int32?, termsOfService: (UnauthorizedAccountTermsOfService, Bool)?) { self.termsOfService = termsOfService if self.data?.0 != number || self.data?.1 != email || self.data?.2 != codeType || self.data?.3 != nextType || self.data?.4 != timeout { @@ -144,6 +152,13 @@ public final class AuthorizationSequenceCodeEntryController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + let hadLayout = self.validLayout != nil + self.validLayout = layout + + if !hadLayout { + self.updateNavigationItems() + } + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index 80af48a4ef..581cdacede 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -249,7 +249,9 @@ public final class AuthorizationSequenceController: NavigationController, MFMail controller.present(strongSelf.sharedContext.makeProxySettingsController(sharedContext: strongSelf.sharedContext, account: strongSelf.account), in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) })) } - controller.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), in: .window(.root)) + (controller.navigationController as? NavigationController)?.presentOverlay(controller: standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: text, actions: actions), inGlobal: true, blockInteraction: true) + + controller.dismissConfirmation() } })) } @@ -645,7 +647,6 @@ public final class AuthorizationSequenceController: NavigationController, MFMail guard let strongSelf = self else { return } -// lastController?.inProgress = false switch result { case let .signUp(data): let _ = beginSignUp(account: strongSelf.account, data: data).start() @@ -655,8 +656,6 @@ public final class AuthorizationSequenceController: NavigationController, MFMail }, error: { [weak self, weak lastController] error in Queue.mainQueue().async { if let strongSelf = self, let lastController = lastController { -// controller.inProgress = false - let text: String switch error { case .limitExceeded: diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift index fe838c9e05..9a763b828d 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryController.swift @@ -17,6 +17,8 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { return self.displayNode as! AuthorizationSequenceEmailEntryControllerNode } + private var validLayout: ContainerViewLayout? + private let presentationData: PresentationData public var proceedWithEmail: ((String) -> Void)? @@ -28,12 +30,7 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { public var inProgress: Bool = false { didSet { -// if self.inProgress { -// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) -// self.navigationItem.rightBarButtonItem = item -// } else { -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) -// } + self.updateNavigationItems() self.controllerNode.inProgress = self.inProgress } } @@ -56,8 +53,6 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { self.navigationBar?.backPressed = { back() } - -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -87,6 +82,19 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { self.controllerNode.activateInput() } + func updateNavigationItems() { + guard let layout = self.validLayout, layout.size.width < 360.0 else { + return + } + + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + } + public func updateData(appleSignInAllowed: Bool) { var appleSignInAllowed = appleSignInAllowed if #available(iOS 13.0, *) { @@ -104,6 +112,13 @@ public final class AuthorizationSequenceEmailEntryController: ViewController { override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + let hadLayout = self.validLayout != nil + self.validLayout = layout + + if !hadLayout { + self.updateNavigationItems() + } + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift index e9b005f837..73bb5b80f8 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceEmailEntryControllerNode.swift @@ -87,7 +87,6 @@ final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UIText self.animationNode = DefaultAnimatedStickerNodeImpl() self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "IntroMail"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - self.animationNode.visibility = true self.titleNode = ASTextNode() self.titleNode.isUserInteractionEnabled = false @@ -189,11 +188,12 @@ final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UIText var insets = layout.insets(options: []) insets.top = layout.statusBarHeight ?? 20.0 - if let inputHeight = layout.inputHeight { insets.bottom = max(inputHeight, insets.bottom) } + let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0 + self.titleNode.attributedText = NSAttributedString(string: self.mode == .setup ? self.strings.Login_AddEmailTitle : self.strings.Login_EnterNewEmailTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) let animationSize = CGSize(width: 100.0, height: 100.0) @@ -204,15 +204,25 @@ final class AuthorizationSequenceEmailEntryControllerNode: ASDisplayNode, UIText let proceedSize = CGSize(width: layout.size.width - 48.0, height: proceedHeight) var items: [AuthorizationLayoutItem] = [] - items.append(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) - self.animationNode.updateLayout(size: animationSize) - items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + items.append(AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: titleInset, maxValue: titleInset), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) items.append(AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) items.append(AuthorizationLayoutItem(node: self.codeField, size: CGSize(width: layout.size.width - 88.0, height: 44.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 22.0, maxValue: 40.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) items.append(AuthorizationLayoutItem(node: self.codeSeparatorNode, size: CGSize(width: layout.size.width - 48.0, height: UIScreenPixel), spacingBefore: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0))) + if layout.size.width > 320.0 { + items.insert(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), at: 0) + self.animationNode.updateLayout(size: animationSize) + self.proceedNode.isHidden = false + self.animationNode.isHidden = false + self.animationNode.visibility = true + } else { + insets.top = navigationBarHeight + self.proceedNode.isHidden = true + self.animationNode.isHidden = true + } + let inset: CGFloat = 24.0 let buttonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - proceedSize.width) / 2.0), y: layout.size.height - insets.bottom - proceedSize.height - inset), size: proceedSize) transition.updateFrame(node: self.proceedNode, frame: buttonFrame) diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift index 6427aa57fe..4a0b573690 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryController.swift @@ -17,6 +17,8 @@ final class AuthorizationSequencePhoneEntryController: ViewController { return self.displayNode as! AuthorizationSequencePhoneEntryControllerNode } + private var validLayout: ContainerViewLayout? + private let sharedContext: SharedAccountContext private var account: UnauthorizedAccount private let isTestingEnvironment: Bool @@ -43,12 +45,7 @@ final class AuthorizationSequencePhoneEntryController: ViewController { var inProgress: Bool = false { didSet { -// if self.inProgress { -// let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) -// self.navigationItem.rightBarButtonItem = item -// } else { -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) -// } + self.updateNavigationItems() self.controllerNode.inProgress = self.inProgress self.confirmationController?.inProgress = self.inProgress } @@ -89,7 +86,6 @@ final class AuthorizationSequencePhoneEntryController: ViewController { if !otherAccountPhoneNumbers.1.isEmpty { self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed)) } -// self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) } required init(coder aDecoder: NSCoder) { @@ -104,6 +100,19 @@ final class AuthorizationSequencePhoneEntryController: ViewController { self.back() } + func updateNavigationItems() { + guard let layout = self.validLayout, layout.size.width < 360.0 else { + return + } + + if self.inProgress { + let item = UIBarButtonItem(customDisplayNode: ProgressNavigationButtonNode(color: self.presentationData.theme.rootController.navigationBar.accentTextColor)) + self.navigationItem.rightBarButtonItem = item + } else { + self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Next, style: .done, target: self, action: #selector(self.nextPressed)) + } + } + func updateData(countryCode: Int32, countryName: String?, number: String) { self.currentData = (countryCode, countryName, number) if self.isNodeLoaded { @@ -207,6 +216,13 @@ final class AuthorizationSequencePhoneEntryController: ViewController { override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) + let hadLayout = self.validLayout != nil + self.validLayout = layout + + if !hadLayout { + self.updateNavigationItems() + } + self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition) if self.shouldAnimateIn, let inputHeight = layout.inputHeight, inputHeight > 0.0 { @@ -217,6 +233,11 @@ final class AuthorizationSequencePhoneEntryController: ViewController { } } + func dismissConfirmation() { + self.confirmationController?.dismissAnimated() + self.confirmationController = nil + } + @objc func nextPressed() { let (_, _, number) = self.controllerNode.codeAndNumber if !number.isEmpty { @@ -239,16 +260,27 @@ final class AuthorizationSequencePhoneEntryController: ViewController { actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_OK, action: {})) self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.Login_PhoneNumberAlreadyAuthorized, actions: actions), in: .window(.root)) } else { - let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber + if let validLayout = self.validLayout, validLayout.size.width > 320.0 { + let (code, formattedNumber) = self.controllerNode.formattedCodeAndNumber - let confirmationController = PhoneConfirmationController(theme: self.presentationData.theme, strings: self.presentationData.strings, code: code, number: formattedNumber, sourceController: self) - confirmationController.proceed = { [weak self] in - if let strongSelf = self { - strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts) + let confirmationController = PhoneConfirmationController(theme: self.presentationData.theme, strings: self.presentationData.strings, code: code, number: formattedNumber, sourceController: self) + confirmationController.proceed = { [weak self] in + if let strongSelf = self { + strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts) + } } + (self.navigationController as? NavigationController)?.presentOverlay(controller: confirmationController, inGlobal: true, blockInteraction: true) + self.confirmationController = confirmationController + } else { + var actions: [TextAlertAction] = [] + actions.append(TextAlertAction(type: .genericAction, title: self.presentationData.strings.Login_Edit, action: {})) + actions.append(TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Login_Yes, action: { [weak self] in + if let strongSelf = self { + strongSelf.loginWithNumber?(strongSelf.controllerNode.currentNumber, strongSelf.controllerNode.syncContacts) + } + })) + self.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: logInNumber, text: self.presentationData.strings.Login_PhoneNumberConfirmation, actions: actions), in: .window(.root)) } - (self.navigationController as? NavigationController)?.presentOverlay(controller: confirmationController, inGlobal: true, blockInteraction: true) - self.confirmationController = confirmationController } } else { self.hapticFeedback.error() diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift index c1798516aa..fe7a92f9b0 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePhoneEntryControllerNode.swift @@ -227,7 +227,7 @@ private final class PhoneAndCountryNode: ASDisplayNode { self.phoneBackground.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height - 57.0), size: CGSize(width: size.width - inset, height: 57.0)) let countryCodeFrame = CGRect(origin: CGPoint(x: 18.0, y: size.height - 58.0), size: CGSize(width: 71.0, height: 57.0)) - let numberFrame = CGRect(origin: CGPoint(x: 107.0, y: size.height - 58.0), size: CGSize(width: size.width - 96.0 - 8.0, height: 57.0)) + let numberFrame = CGRect(origin: CGPoint(x: 107.0, y: size.height - 58.0), size: CGSize(width: size.width - 96.0 - 8.0 - 24.0, height: 57.0)) let placeholderFrame = numberFrame.offsetBy(dx: 0.0, dy: 17.0 - UIScreenPixel) let phoneInputFrame = countryCodeFrame.union(numberFrame) @@ -360,7 +360,6 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.animationNode = DefaultAnimatedStickerNodeImpl() self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "IntroPhone"), width: 256, height: 256, playbackMode: .once, mode: .direct(cachePathPrefix: nil)) - self.animationNode.visibility = true self.managedAnimationNode = ManagedPhoneAnimationNode() self.managedAnimationNode.isHidden = true @@ -479,9 +478,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { func animateIn(buttonFrame: CGRect, buttonTitle: String, animationSnapshot: UIView, textSnapshot: UIView) { self.proceedNode.animateTitle(to: self.strings.Login_Continue) - - let duration: Double = 0.3 - + self.animationSnapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak self] _ in self?.animationSnapshotView?.removeFromSuperview() self?.animationSnapshotView = nil @@ -507,7 +504,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { for node in nodes { node.alpha = 1.0 - node.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } } @@ -518,11 +515,13 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { var insets = layout.insets(options: []) insets.top = layout.statusBarHeight ?? 20.0 - if let inputHeight = layout.inputHeight, !inputHeight.isZero { insets.bottom = max(inputHeight, insets.bottom) } + let titleInset: CGFloat = layout.size.width > 320.0 ? 18.0 : 0.0 + let additionalBottomInset: CGFloat = layout.size.width > 320.0 ? 80.0 : 10.0 + self.titleNode.attributedText = NSAttributedString(string: strings.Login_PhoneTitle, font: Font.bold(28.0), textColor: self.theme.list.itemPrimaryTextColor) let inset: CGFloat = 24.0 @@ -534,11 +533,23 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { let proceedSize = CGSize(width: layout.size.width - inset * 2.0, height: proceedHeight) var items: [AuthorizationLayoutItem] = [ - AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), - AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), + AuthorizationLayoutItem(node: self.titleNode, size: titleSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: titleInset, maxValue: titleInset), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), AuthorizationLayoutItem(node: self.noticeNode, size: noticeSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 18.0, maxValue: 18.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), AuthorizationLayoutItem(node: self.phoneAndCountryNode, size: CGSize(width: layout.size.width, height: 115.0), spacingBefore: AuthorizationLayoutItemSpacing(weight: 30.0, maxValue: 30.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), ] + + if layout.size.width > 320.0 { + items.insert(AuthorizationLayoutItem(node: self.animationNode, size: animationSize, spacingBefore: AuthorizationLayoutItemSpacing(weight: 10.0, maxValue: 10.0), spacingAfter: AuthorizationLayoutItemSpacing(weight: 0.0, maxValue: 0.0)), at: 0) + self.proceedNode.isHidden = false + self.animationNode.isHidden = false + self.animationNode.visibility = true + } else { + insets.top = navigationBarHeight + self.proceedNode.isHidden = true + self.animationNode.isHidden = true + self.managedAnimationNode.isHidden = true + } + let contactSyncSize = self.contactSyncNode.updateLayout(width: layout.size.width) if self.hasOtherAccounts { self.contactSyncNode.isHidden = false @@ -558,7 +569,7 @@ final class AuthorizationSequencePhoneEntryControllerNode: ASDisplayNode { self.animationNode.updateLayout(size: animationSize) - let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - 80.0)), items: items, transition: transition, failIfDoesNotFit: false) + let _ = layoutAuthorizationItems(bounds: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: layout.size.height - insets.top - insets.bottom - additionalBottomInset)), items: items, transition: transition, failIfDoesNotFit: false) transition.updateFrame(node: self.managedAnimationNode, frame: self.animationNode.frame) } @@ -696,7 +707,12 @@ final class PhoneConfirmationController: ViewController { var proceed: () -> Void = {} - class Node: ASDisplayNode { + class Node: ASDisplayNode { + private let theme: PresentationTheme + + private let code: String + private let number: String + private let dimNode: ASDisplayNode private let backgroundNode: ASDisplayNode @@ -714,7 +730,14 @@ final class PhoneConfirmationController: ViewController { var proceed: () -> Void = {} var cancel: () -> Void = {} + private var validLayout: ContainerViewLayout? + init(theme: PresentationTheme, strings: PresentationStrings, code: String, number: String) { + self.theme = theme + + self.code = code + self.number = number + self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) @@ -753,7 +776,6 @@ final class PhoneConfirmationController: ViewController { self.codeTargetNode.displaysAsynchronously = false self.codeTargetNode.attributedText = NSAttributedString(string: code, font: largeFont, textColor: theme.list.itemPrimaryTextColor) - self.phoneTargetNode = ImmediateTextNode() self.phoneTargetNode.displaysAsynchronously = false @@ -801,6 +823,9 @@ final class PhoneConfirmationController: ViewController { } func animateIn(codeNode: ASDisplayNode, numberNode: ASDisplayNode, buttonNode: ASDisplayNode) { + guard let layout = self.validLayout else { + return + } let codeFrame = codeNode.convert(codeNode.bounds, to: nil) let numberFrame = numberNode.convert(numberNode.bounds, to: nil) let buttonFrame = buttonNode.convert(buttonNode.bounds, to: nil) @@ -811,43 +836,45 @@ final class PhoneConfirmationController: ViewController { self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - let codeSize = self.codeSourceNode.updateLayout(self.frame.size) + let duration: Double = 0.25 + + let codeSize = self.codeSourceNode.updateLayout(layout.size) self.codeSourceNode.frame = CGRect(origin: CGPoint(x: codeFrame.midX - codeSize.width / 2.0, y: codeFrame.midY - codeSize.height / 2.0), size: codeSize) - let numberSize = self.phoneSourceNode.updateLayout(self.frame.size) + let numberSize = self.phoneSourceNode.updateLayout(layout.size) self.phoneSourceNode.frame = CGRect(origin: CGPoint(x: numberFrame.minX, y: numberFrame.midY - numberSize.height / 2.0), size: numberSize) let targetScale = codeSize.height / self.codeTargetNode.frame.height let sourceScale = self.codeTargetNode.frame.height / codeSize.height - self.codeSourceNode.layer.animateScale(from: 1.0, to: sourceScale, duration: 0.3) - self.codeSourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - self.codeSourceNode.layer.animatePosition(from: self.codeSourceNode.position, to: self.codeTargetNode.position, duration: 0.3) + self.codeSourceNode.layer.animateScale(from: 1.0, to: sourceScale, duration: duration) + self.codeSourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + self.codeSourceNode.layer.animatePosition(from: self.codeSourceNode.position, to: self.codeTargetNode.position, duration: duration) - self.phoneSourceNode.layer.animateScale(from: 1.0, to: sourceScale, duration: 0.3) - self.phoneSourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3) - self.phoneSourceNode.layer.animatePosition(from: self.phoneSourceNode.position, to: self.phoneTargetNode.position, duration: 0.3) + self.phoneSourceNode.layer.animateScale(from: 1.0, to: sourceScale, duration: duration) + self.phoneSourceNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration) + self.phoneSourceNode.layer.animatePosition(from: self.phoneSourceNode.position, to: self.phoneTargetNode.position, duration: duration) - self.codeTargetNode.layer.animateScale(from: targetScale, to: 1.0, duration: 0.3) - self.codeTargetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.codeTargetNode.layer.animatePosition(from: self.codeSourceNode.position, to: self.codeTargetNode.position, duration: 0.3) + self.codeTargetNode.layer.animateScale(from: targetScale, to: 1.0, duration: duration) + self.codeTargetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.codeTargetNode.layer.animatePosition(from: self.codeSourceNode.position, to: self.codeTargetNode.position, duration: duration) - self.phoneTargetNode.layer.animateScale(from: targetScale, to: 1.0, duration: 0.3) - self.phoneTargetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.phoneTargetNode.layer.animatePosition(from: self.phoneSourceNode.position, to: self.phoneTargetNode.position, duration: 0.3) + self.phoneTargetNode.layer.animateScale(from: targetScale, to: 1.0, duration: duration) + self.phoneTargetNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.phoneTargetNode.layer.animatePosition(from: self.phoneSourceNode.position, to: self.phoneTargetNode.position, duration: duration) self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) - self.backgroundNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 14.0, y: codeFrame.minY), size: CGSize(width: self.backgroundNode.frame.width - 12.0, height: buttonFrame.maxY + 18.0 - codeFrame.minY)), to: self.backgroundNode.frame, duration: 0.3) + self.backgroundNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 14.0, y: codeFrame.minY), size: CGSize(width: self.backgroundNode.frame.width - 12.0, height: buttonFrame.maxY + 18.0 - codeFrame.minY)), to: self.backgroundNode.frame, duration: duration) - self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.textNode.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3) - self.textNode.layer.animatePosition(from: CGPoint(x: -100.0, y: -45.0), to: CGPoint(), duration: 0.3, additive: true) + self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.textNode.layer.animateScale(from: 0.5, to: 1.0, duration: duration) + self.textNode.layer.animatePosition(from: CGPoint(x: -100.0, y: -45.0), to: CGPoint(), duration: duration, additive: true) - self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.cancelButton.layer.animateScale(from: 0.5, to: 1.0, duration: 0.3) - self.cancelButton.layer.animatePosition(from: CGPoint(x: -100.0, y: -70.0), to: CGPoint(), duration: 0.3, additive: true) + self.cancelButton.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.cancelButton.layer.animateScale(from: 0.5, to: 1.0, duration: duration) + self.cancelButton.layer.animatePosition(from: CGPoint(x: -100.0, y: -70.0), to: CGPoint(), duration: duration, additive: true) - self.proceedNode.layer.animatePosition(from: buttonFrame.center, to: self.proceedNode.position, duration: 0.3) + self.proceedNode.layer.animatePosition(from: buttonFrame.center, to: self.proceedNode.position, duration: duration) } func animateOut(codeNode: ASDisplayNode, numberNode: ASDisplayNode, buttonNode: ASDisplayNode, completion: @escaping () -> Void) { @@ -857,6 +884,8 @@ final class PhoneConfirmationController: ViewController { self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + let duration: Double = 0.25 + let codeSize = self.codeSourceNode.updateLayout(self.frame.size) self.codeSourceNode.frame = CGRect(origin: CGPoint(x: codeFrame.midX - codeSize.width / 2.0, y: codeFrame.midY - codeSize.height / 2.0), size: codeSize) @@ -866,45 +895,48 @@ final class PhoneConfirmationController: ViewController { let targetScale = codeSize.height / self.codeTargetNode.frame.height let sourceScale = self.codeTargetNode.frame.height / codeSize.height - self.codeSourceNode.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3) - self.codeSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.codeSourceNode.layer.animatePosition(from: self.codeTargetNode.position, to: self.codeSourceNode.position, duration: 0.3) + self.codeSourceNode.layer.animateScale(from: sourceScale, to: 1.0, duration: duration) + self.codeSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.codeSourceNode.layer.animatePosition(from: self.codeTargetNode.position, to: self.codeSourceNode.position, duration: duration) - self.phoneSourceNode.layer.animateScale(from: sourceScale, to: 1.0, duration: 0.3) - self.phoneSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) - self.phoneSourceNode.layer.animatePosition(from: self.phoneTargetNode.position, to: self.phoneSourceNode.position, duration: 0.3) + self.phoneSourceNode.layer.animateScale(from: sourceScale, to: 1.0, duration: duration) + self.phoneSourceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration) + self.phoneSourceNode.layer.animatePosition(from: self.phoneTargetNode.position, to: self.phoneSourceNode.position, duration: duration) - self.codeTargetNode.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3) - self.codeTargetNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.codeTargetNode.layer.animatePosition(from: self.codeTargetNode.position, to: self.codeSourceNode.position, duration: 0.3) + self.codeTargetNode.layer.animateScale(from: 1.0, to: targetScale, duration: duration) + self.codeTargetNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false) + self.codeTargetNode.layer.animatePosition(from: self.codeTargetNode.position, to: self.codeSourceNode.position, duration: duration) - Queue.mainQueue().after(0.25) { + Queue.mainQueue().after(0.23) { codeNode.isHidden = false numberNode.isHidden = false buttonNode.isHidden = false } - self.phoneTargetNode.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3) - self.phoneTargetNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in + self.phoneTargetNode.layer.animateScale(from: 1.0, to: targetScale, duration: duration) + self.phoneTargetNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, removeOnCompletion: false, completion: { _ in completion() }) - self.phoneTargetNode.layer.animatePosition(from: self.phoneTargetNode.position, to: self.phoneSourceNode.position, duration: 0.3) + self.phoneTargetNode.layer.animatePosition(from: self.phoneTargetNode.position, to: self.phoneSourceNode.position, duration: duration) self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, delay: 0.1, removeOnCompletion: false) - self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 14.0, y: codeFrame.minY), size: CGSize(width: self.backgroundNode.frame.width - 12.0, height: buttonFrame.maxY + 18.0 - codeFrame.minY)), duration: 0.3) + self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 14.0, y: codeFrame.minY), size: CGSize(width: self.backgroundNode.frame.width - 12.0, height: buttonFrame.maxY + 18.0 - codeFrame.minY)), duration: duration) self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.textNode.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, removeOnCompletion: false) - self.textNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -100.0, y: -45.0), duration: 0.3, removeOnCompletion: false, additive: true) + self.textNode.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) + self.textNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -100.0, y: -45.0), duration: duration, removeOnCompletion: false, additive: true) self.cancelButton.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.cancelButton.layer.animateScale(from: 1.0, to: 0.5, duration: 0.3, removeOnCompletion: false) - self.cancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -100.0, y: -70.0), duration: 0.3, removeOnCompletion: false, additive: true) + self.cancelButton.layer.animateScale(from: 1.0, to: 0.5, duration: duration, removeOnCompletion: false) + self.cancelButton.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -100.0, y: -70.0), duration: duration, removeOnCompletion: false, additive: true) - self.proceedNode.layer.animatePosition(from: self.proceedNode.position, to: buttonFrame.center, duration: 0.3, removeOnCompletion: false) + self.proceedNode.layer.animatePosition(from: self.proceedNode.position, to: buttonFrame.center, duration: duration, removeOnCompletion: false) } func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + let hadLayout = self.validLayout != nil + self.validLayout = layout + let sideInset: CGFloat = 8.0 let innerInset: CGFloat = 18.0 @@ -913,16 +945,33 @@ final class PhoneConfirmationController: ViewController { let backgroundSize = CGSize(width: layout.size.width - sideInset * 2.0, height: 243.0) let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - backgroundSize.width) / 2.0), y: layout.size.height - backgroundSize.height - 260.0), size: backgroundSize) transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame) + + let maxWidth = layout.size.width - 20.0 + if !hadLayout { + var fontSize = 34.0 + if layout.size.width < 375.0 { + fontSize = 30.0 + } + + let largeFont = Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers]) + + self.codeTargetNode.attributedText = NSAttributedString(string: self.code, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor) + let targetString = NSMutableAttributedString(string: self.number, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor) + targetString.addAttribute(NSAttributedString.Key.kern, value: 1.6, range: NSRange(location: 0, length: targetString.length)) + self.phoneTargetNode.attributedText = targetString + } - let codeSize = self.codeTargetNode.updateLayout(backgroundSize) - let numberSize = self.phoneTargetNode.updateLayout(backgroundSize) + let spacing: CGFloat = 10.0 - let totalWidth = codeSize.width + numberSize.width + 10.0 + let codeSize = self.codeTargetNode.updateLayout(CGSize(width: maxWidth, height: .greatestFiniteMagnitude)) + let numberSize = self.phoneTargetNode.updateLayout(CGSize(width: maxWidth - codeSize.width - spacing, height: .greatestFiniteMagnitude)) + + let totalWidth = codeSize.width + numberSize.width + spacing let codeFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - totalWidth) / 2.0), y: 30.0), size: codeSize) transition.updateFrame(node: self.codeTargetNode, frame: codeFrame.offsetBy(dx: backgroundFrame.minX, dy: backgroundFrame.minY)) - let numberFrame = CGRect(origin: CGPoint(x: codeFrame.maxX + 10.0, y: 30.0), size: numberSize) + let numberFrame = CGRect(origin: CGPoint(x: codeFrame.maxX + spacing, y: 30.0), size: numberSize) transition.updateFrame(node: self.phoneTargetNode, frame: numberFrame.offsetBy(dx: backgroundFrame.minX, dy: backgroundFrame.minY)) let textSize = self.textNode.updateLayout(backgroundSize) @@ -972,6 +1021,10 @@ final class PhoneConfirmationController: ViewController { } } + func dismissAnimated() { + self.controllerNode.cancel() + } + func transitionOut() { self.controllerNode.cancel() diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 1e318d6804..75d5733895 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -2136,21 +2136,21 @@ open class TextView: UIView { } } - if !line.strikethroughs.isEmpty { - for strikethrough in line.strikethroughs { - var textColor: UIColor? - layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in - if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { - textColor = color - } - } - if let textColor = textColor { - context.setFillColor(textColor.cgColor) - } - let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) - context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0)) - } - } +// if !line.strikethroughs.isEmpty { +// for strikethrough in line.strikethroughs { +// var textColor: UIColor? +// layout.attributedString?.enumerateAttributes(in: NSMakeRange(line.range.location, line.range.length), options: []) { attributes, range, _ in +// if range == strikethrough.range, let color = attributes[NSAttributedString.Key.foregroundColor] as? UIColor { +// textColor = color +// } +// } +// if let textColor = textColor { +// context.setFillColor(textColor.cgColor) +// } +// let frame = strikethrough.frame.offsetBy(dx: lineFrame.minX, dy: lineFrame.minY) +// context.fill(CGRect(x: frame.minX, y: frame.minY - 5.0, width: frame.width, height: 1.0)) +// } +// } if !line.spoilers.isEmpty { if layout.displaySpoilers { diff --git a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift index 234029b427..07830dcede 100644 --- a/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift +++ b/submodules/InAppPurchaseManager/Sources/InAppPurchaseManager.swift @@ -10,6 +10,7 @@ import PersistentStringHash private let productIdentifiers = [ "org.telegram.telegramPremium.annual", + "org.telegram.telegramPremium.semiannual", "org.telegram.telegramPremium.monthly", "org.telegram.telegramPremium.twelveMonths", "org.telegram.telegramPremium.sixMonths", @@ -17,7 +18,7 @@ private let productIdentifiers = [ ] private func isSubscriptionProductId(_ id: String) -> Bool { - return id.hasSuffix(".monthly") || id.hasSuffix(".annual") + return id.hasSuffix(".monthly") || id.hasSuffix(".annual") || id.hasSuffix(".semiannual") } private extension NSDecimalNumber { @@ -30,6 +31,22 @@ private extension NSDecimalNumber { raiseOnUnderflow: false, raiseOnDivideByZero: false)) } + + func prettyPrice() -> NSDecimalNumber { + return self.multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .plain, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + } } public final class InAppPurchaseManager: NSObject { @@ -57,7 +74,7 @@ public final class InAppPurchaseManager: NSObject { } else if #available(iOS 11.2, *) { return self.skProduct.subscriptionPeriod != nil } else { - return self.id.hasSuffix(".monthly") || self.id.hasSuffix(".annual") + return self.id.hasSuffix(".monthly") || self.id.hasSuffix(".annual") || self.id.hasSuffix(".semiannual") } } @@ -66,13 +83,27 @@ public final class InAppPurchaseManager: NSObject { } public func pricePerMonth(_ monthsCount: Int) -> String { - let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).round(2) + let price = self.skProduct.price.dividing(by: NSDecimalNumber(value: monthsCount)).prettyPrice().round(2) return self.numberFormatter.string(from: price) ?? "" } public func defaultPrice(_ value: NSDecimalNumber, monthsCount: Int) -> String { let price = value.multiplying(by: NSDecimalNumber(value: monthsCount)).round(2) - return self.numberFormatter.string(from: price) ?? "" + let prettierPrice = price + .multiplying(by: NSDecimalNumber(value: 2)) + .rounding(accordingToBehavior: + NSDecimalNumberHandler( + roundingMode: .up, + scale: Int16(0), + raiseOnExactness: false, + raiseOnOverflow: false, + raiseOnUnderflow: false, + raiseOnDivideByZero: false + ) + ) + .dividing(by: NSDecimalNumber(value: 2)) + .subtracting(NSDecimalNumber(value: 0.01)) + return self.numberFormatter.string(from: prettierPrice) ?? "" } public var priceValue: NSDecimalNumber { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 780dcc5039..c5e48d9060 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -548,7 +548,6 @@ private final class PremiumGiftScreenComponent: CombinedComponent { context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)) ).start(next: { [weak self] products, peer in if let strongSelf = self { - var gifts: [PremiumGiftProduct] = [] for option in strongSelf.options { if let product = products.first(where: { $0.id == option.storeProductId }), !product.isSubscription { diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 732f2a3428..b21a81d7fc 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -305,6 +305,27 @@ struct PremiumIntroConfiguration { } } +private struct PremiumProduct: Equatable { + let option: PremiumPromoConfiguration.PremiumProductOption + let storeProduct: InAppPurchaseManager.Product + + var id: String { + return self.storeProduct.id + } + + var months: Int32 { + return self.option.months + } + + var price: String { + return self.storeProduct.price + } + + var pricePerMonth: String { + return self.storeProduct.pricePerMonth(Int(self.months)) + } +} + final class PremiumOptionComponent: CombinedComponent { let title: String let subtitle: String @@ -419,7 +440,7 @@ final class PremiumOptionComponent: CombinedComponent { ) var spacing: CGFloat = 0.0 - var subtitleHeight: CGFloat = 0.0 + var subtitleSize = CGSize() if !component.subtitle.isEmpty { spacing = 2.0 @@ -447,7 +468,7 @@ final class PremiumOptionComponent: CombinedComponent { context.add(subtitle .position(CGPoint(x: insets.left + subtitle.size.width / 2.0, y: insets.top + title.size.height + spacing + subtitle.size.height / 2.0)) ) - subtitleHeight = subtitle.size.height + subtitleSize = subtitle.size insets.top -= 2.0 insets.bottom -= 2.0 @@ -512,10 +533,19 @@ final class PremiumOptionComponent: CombinedComponent { .position(CGPoint(x: insets.left + title.size.width / 2.0, y: insets.top + title.size.height / 2.0)) ) - let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleHeight + insets.bottom) + let size = CGSize(width: context.availableSize.width, height: insets.top + title.size.height + spacing + subtitleSize.height + insets.bottom) + + let distance = context.availableSize.width - insets.left - insets.right - label.size.width - subtitleSize.width + + let labelY: CGFloat + if distance > 8.0 { + labelY = size.height / 2.0 + } else { + labelY = insets.top + title.size.height / 2.0 + } context.add(label - .position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: size.height / 2.0)) + .position(CGPoint(x: context.availableSize.width - insets.right - label.size.width / 2.0, y: labelY)) ) context.add(check @@ -970,20 +1000,22 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let source: PremiumSource let isPremium: Bool? let otherPeerName: String? - let products: [InAppPurchaseManager.Product]? + let products: [PremiumProduct]? let selectedProductId: String? + 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?, otherPeerName: String?, products: [InAppPurchaseManager.Product]?, selectedProductId: String?, present: @escaping (ViewController) -> Void, selectProduct: @escaping (String) -> Void, buy: @escaping () -> Void, updateIsFocused: @escaping (Bool) -> Void) { + init(context: AccountContext, source: PremiumSource, isPremium: Bool?, otherPeerName: String?, products: [PremiumProduct]?, selectedProductId: String?, 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 self.otherPeerName = otherPeerName self.products = products self.selectedProductId = selectedProductId + self.promoConfiguration = promoConfiguration self.present = present self.selectProduct = selectProduct self.buy = buy @@ -1009,6 +1041,9 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { if lhs.selectedProductId != rhs.selectedProductId { return false } + if lhs.promoConfiguration != rhs.promoConfiguration { + return false + } return true } @@ -1016,15 +1051,14 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { final class State: ComponentState { private let context: AccountContext - var products: [InAppPurchaseManager.Product]? + var products: [PremiumProduct]? var selectedProductId: String? var isPremium: Bool? private var disposable: Disposable? private(set) var configuration = PremiumIntroConfiguration.defaultValue - private(set) var promoConfiguration: PremiumPromoConfiguration? - + private var stickersDisposable: Disposable? private var preloadDisposableSet = DisposableSet() @@ -1042,13 +1076,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { super.init() self.disposable = (context.engine.data.subscribe( - TelegramEngine.EngineData.Item.Configuration.App(), - TelegramEngine.EngineData.Item.Configuration.PremiumPromo() + TelegramEngine.EngineData.Item.Configuration.App() ) - |> deliverOnMainQueue).start(next: { [weak self] appConfiguration, promoConfiguration in + |> deliverOnMainQueue).start(next: { [weak self] appConfiguration in if let strongSelf = self { strongSelf.configuration = PremiumIntroConfiguration.with(appConfiguration: appConfiguration) - strongSelf.promoConfiguration = promoConfiguration strongSelf.updated(transition: .immediate) if let identifier = source.identifier { @@ -1070,10 +1102,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { addAppLogEvent(postbox: strongSelf.context.account.postbox, type: "premium.promo_screen_show", data: json) } } - - for (_, video) in promoConfiguration.videos { - strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) - } } }) @@ -1253,7 +1281,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let shortestOptionPrice: (Int64, NSDecimalNumber) if let product = products.first(where: { $0.id.hasSuffix(".monthly") }) { - shortestOptionPrice = (Int64(Float(product.priceCurrencyAndAmount.amount)), product.priceValue) + shortestOptionPrice = (Int64(Float(product.storeProduct.priceCurrencyAndAmount.amount)), product.storeProduct.priceValue) } else { shortestOptionPrice = (1, NSDecimalNumber(decimal: 1)) } @@ -1261,16 +1289,17 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { var i = 0 for product in products { let giftTitle: String - let months: Float + let months = product.months + if product.id.hasSuffix(".monthly") { giftTitle = strings.Premium_Monthly - months = 1 + } else if product.id.hasSuffix(".semiannual") { + giftTitle = strings.Premium_Semiannual } else { giftTitle = strings.Premium_Annual - months = 12 } - let discountValue = Int((1.0 - Float(product.priceCurrencyAndAmount.amount) / months / Float(shortestOptionPrice.0)) * 100.0) + let discountValue = Int((1.0 - Float(product.storeProduct.priceCurrencyAndAmount.amount) / Float(months) / Float(shortestOptionPrice.0)) * 100.0) let discount: String if discountValue > 0 { discount = "-\(discountValue)%" @@ -1278,12 +1307,12 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { discount = "" } - let defaultPrice = product.defaultPrice(shortestOptionPrice.1, monthsCount: Int(months)) + let defaultPrice = product.storeProduct.defaultPrice(shortestOptionPrice.1, monthsCount: Int(months)) var subtitle = "" var pricePerMonth = product.price if months > 1 { - pricePerMonth = product.pricePerMonth(Int(months)) + pricePerMonth = product.storeProduct.pricePerMonth(Int(months)) if discountValue > 0 { subtitle = "**\(defaultPrice)** \(product.price)" @@ -1552,7 +1581,7 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { let termsString: MultilineTextComponent.TextContent if isGiftView { termsString = .plain(NSAttributedString()) - } else if let promoConfiguration = context.state.promoConfiguration { + } else if let promoConfiguration = context.component.promoConfiguration { let attributedString = stringWithAppliedEntities(promoConfiguration.status, entities: promoConfiguration.statusEntities, baseColor: termsTextColor, linkColor: environment.theme.list.itemAccentColor, baseFont: termsFont, linkFont: termsFont, boldFont: boldTermsFont, italicFont: italicTermsFont, boldItalicFont: boldItalicTermsFont, fixedFont: monospaceTermsFont, blockQuoteFont: termsFont, message: nil) termsString = .plain(attributedString) } else { @@ -1723,8 +1752,10 @@ private final class PremiumIntroScreenComponent: CombinedComponent { var inProgress = false - var products: [InAppPurchaseManager.Product]? - var selectedProductId: String? + private(set) var promoConfiguration: PremiumPromoConfiguration? + + private(set) var products: [PremiumProduct]? + private(set) var selectedProductId: String? var isPremium: Bool? var otherPeerName: String? @@ -1740,6 +1771,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { private var disposable: Disposable? private var paymentDisposable = MetaDisposable() private var activationDisposable = MetaDisposable() + private var preloadDisposableSet = DisposableSet() var price: String? { return self.products?.first(where: { $0.id == self.selectedProductId })?.price @@ -1792,20 +1824,37 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.disposable = combineLatest( queue: Queue.mainQueue(), availableProducts, + context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.PremiumPromo()), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)) |> map { peer -> Bool in return peer?.isPremium ?? false }, otherPeerName - ).start(next: { [weak self] products, isPremium, otherPeerName in + ).start(next: { [weak self] availableProducts, promoConfiguration, isPremium, otherPeerName in if let strongSelf = self { + strongSelf.promoConfiguration = promoConfiguration + let hadProducts = strongSelf.products != nil - strongSelf.products = products.filter { $0.isSubscription } - if !hadProducts { - strongSelf.selectedProductId = strongSelf.products?.last?.id + + var products: [PremiumProduct] = [] + for option in promoConfiguration.premiumProductOptions { + if let product = availableProducts.first(where: { $0.id == option.storeProductId }), product.isSubscription { + products.append(PremiumProduct(option: option, storeProduct: product)) + } } + + strongSelf.products = products strongSelf.isPremium = isPremium strongSelf.otherPeerName = otherPeerName + + if !hadProducts { + strongSelf.selectedProductId = strongSelf.products?.last?.id + + for (_, video) in promoConfiguration.videos { + strongSelf.preloadDisposableSet.add(preloadVideoResource(postbox: context.account.postbox, resourceReference: .standalone(resource: video.resource), duration: 3.0).start()) + } + } + strongSelf.updated(transition: .immediate) } }) @@ -1833,6 +1882,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { self.paymentDisposable.dispose() self.activationDisposable.dispose() self.emojiFileDisposable?.dispose() + self.preloadDisposableSet.dispose() } func buy() { @@ -1851,7 +1901,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { |> deliverOnMainQueue).start(next: { [weak self] available in if let strongSelf = self { if available { - strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct) + strongSelf.paymentDisposable.set((inAppPurchaseManager.buyProduct(premiumProduct.storeProduct) |> deliverOnMainQueue).start(next: { [weak self] status in if let strongSelf = self, case .purchased = status { strongSelf.activationDisposable.set((strongSelf.context.account.postbox.peerView(id: strongSelf.context.account.peerId) @@ -2153,6 +2203,7 @@ private final class PremiumIntroScreenComponent: CombinedComponent { otherPeerName: state.otherPeerName, products: state.products, selectedProductId: state.selectedProductId, + promoConfiguration: state.promoConfiguration, present: context.component.present, selectProduct: { [weak state] productId in state?.selectProduct(productId) diff --git a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift index 08a458e78b..46012666c8 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/PrivacyAndSecurityController.swift @@ -539,8 +539,18 @@ private func privacyAndSecurityControllerEntries( return entries } -class PrivacyAndSecurityControllerImpl: ItemListController { - +class PrivacyAndSecurityControllerImpl: ItemListController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { + var authorizationCompletion: (ASAuthorizationCredential) -> Void = { _ in } + + @available(iOS 13.0, *) + public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { + self.authorizationCompletion(authorization) + } + + @available(iOS 13.0, *) + public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { + return self.view.window! + } } public func privacyAndSecurityController(context: AccountContext, initialSettings: AccountPrivacySettings? = nil, updatedSettings: ((AccountPrivacySettings?) -> Void)? = nil, updatedBlockedPeers: ((BlockedPeersContext?) -> Void)? = nil, updatedHasTwoStepAuth: ((Bool) -> Void)? = nil, focusOnItemTag: PrivacyAndSecurityEntryTag? = nil, activeSessionsContext: ActiveSessionsContext? = nil, webSessionsContext: WebSessionsContext? = nil, blockedPeersContext: BlockedPeersContext? = nil, hasTwoStepAuth: Bool? = nil, loginEmailPattern: Signal? = nil) -> ViewController { @@ -1083,6 +1093,27 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting let codeController = AuthorizationSequenceCodeEntryController(presentationData: presentationData, openUrl: { _ in }, back: { dismissCodeControllerImpl?() }) + + let emailChangeCompletion = { [weak codeController] in + codeController?.animateSuccess() + Queue.mainQueue().after(0.75) { + if let navigationController = getNavigationControllerImpl?() { + let controllers = navigationController.viewControllers.filter { controller in + if controller is AuthorizationSequenceEmailEntryController || controller is AuthorizationSequenceCodeEntryController { + return false + } else { + return true + } + } + navigationController.setViewControllers(controllers, animated: true) + + navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in + return false + })) + } + } + } + codeController.loginWithCode = { [weak codeController] code in actionsDisposable.add((verifyLoginEmailChange(account: context.account, code: .emailCode(code)) |> deliverOnMainQueue).start(error: { error in @@ -1120,39 +1151,57 @@ public func privacyAndSecurityController(context: AccountContext, initialSetting presentControllerImpl?(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])) } } - }, completed: { [weak codeController] in - codeController?.animateSuccess() - Queue.mainQueue().after(0.75) { - if let navigationController = getNavigationControllerImpl?() { - let controllers = navigationController.viewControllers.filter { controller in - if controller is AuthorizationSequenceEmailEntryController || controller is AuthorizationSequenceCodeEntryController { - return false - } else { - return true - } - } - navigationController.setViewControllers(controllers, animated: true) - - navigationController.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .emoji(name: "IntroLetter", text: presentationData.strings.Login_EmailChanged), elevatedLayout: false, animateInAsReplacement: false, action: { _ in - return false - })) - } - } + }, completed: { + emailChangeCompletion() })) } - codeController.signInWithApple = { -// if #available(iOS 13.0, *) { -// let appleIdProvider = ASAuthorizationAppleIDProvider() -// let passwordProvider = ASAuthorizationPasswordProvider() -// let request = appleIdProvider.createRequest() -// -// let passwordRequest = passwordProvider.createRequest() -// -// let authorizationController = ASAuthorizationController(authorizationRequests: [request, passwordRequest]) -// authorizationController.delegate = strongSelf -// authorizationController.presentationContextProvider = strongSelf -// authorizationController.performRequests() -// } + codeController.signInWithApple = { [weak controller, weak codeController] in + if #available(iOS 13.0, *) { + let appleIdProvider = ASAuthorizationAppleIDProvider() + let passwordProvider = ASAuthorizationPasswordProvider() + let request = appleIdProvider.createRequest() + + let passwordRequest = passwordProvider.createRequest() + + let authorizationController = ASAuthorizationController(authorizationRequests: [request, passwordRequest]) + authorizationController.delegate = controller + authorizationController.presentationContextProvider = controller + authorizationController.performRequests() + + controller.authorizationCompletion = { [weak controller, weak codeController] credentials in + switch authorization.credential { + case let appleIdCredential as ASAuthorizationAppleIDCredential: + guard let tokenData = appleIdCredential.identityToken, let token = String(data: tokenData, encoding: .utf8) else { + codeController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + return + } + actionsDisposable.add((verifyLoginEmailChange(account: context.account, code: .appleToken(token)) + |> deliverOnMainQueue).start(error: { error in + let text: String + switch error { + case .limitExceeded: + text = presentationData.strings.Login_CodeFloodError + case .generic, .codeExpired: + text = presentationData.strings.Login_UnknownError + case .invalidCode: + text = presentationData.strings.Login_InvalidCodeError + case .timeout: + text = presentationData.strings.Login_NetworkError + case .invalidEmailToken: + text = presentationData.strings.Login_InvalidEmailTokenError + case .emailNotAllowed: + text = presentationData.strings.Login_EmailNotAllowedError + } + codeController?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) + }, completed: { [weak controller] in + controller?.authorizationCompletion = nil + emailChangeCompletion() + })) + default: + break + } + } + } } codeController.updateData(number: "", email: email, codeType: .email(emailPattern: "", length: data.length, nextPhoneLoginDate: nil, appleSignInAllowed: false, setup: true), nextType: nil, timeout: nil, termsOfService: nil) pushControllerImpl?(codeController, true) diff --git a/submodules/TelegramCore/Sources/Authorization.swift b/submodules/TelegramCore/Sources/Authorization.swift index 9f8d40caa1..6253c8107e 100644 --- a/submodules/TelegramCore/Sources/Authorization.swift +++ b/submodules/TelegramCore/Sources/Authorization.swift @@ -432,7 +432,7 @@ public func verifyLoginEmailChange(account: Account, code: AuthorizationCode.Ema } } |> mapToSignal { _ -> Signal in - return .never() + return .complete() } }