Various fixes

This commit is contained in:
Ilya Laktyushin 2025-07-21 00:38:47 +02:00
parent afaba07b76
commit 9d703f5b60
14 changed files with 188 additions and 55 deletions

View file

@ -1296,7 +1296,7 @@ public protocol SharedAccountContext: AnyObject {
func makeIncomingMessagePrivacyScreen(context: AccountContext, value: GlobalPrivacySettings.NonContactChatsPrivacy, exceptions: SelectivePrivacySettings, update: @escaping (GlobalPrivacySettings.NonContactChatsPrivacy) -> Void) -> ViewController
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?)
func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?)
func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError>
func makeAffiliateProgramSetupScreen(context: AccountContext, initialData: AffiliateProgramSetupScreenInitialData) -> ViewController

View file

@ -1250,7 +1250,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
}

View file

@ -3282,7 +3282,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
interaction.dismissSearch()
}
@ -4268,7 +4269,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
}
}
@ -4287,7 +4289,8 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
interaction.dismissSearch()
} else {

View file

@ -405,6 +405,20 @@ public func presentAgeVerification(context: AccountContext, parentController: Vi
if state.verificationPassed {
completion()
} else {
let miniappPromise = Promise<EnginePeer?>(nil)
var useVerifyAgeBot = false
if let value = context.currentAppConfiguration.with({ $0 }).data?["force_verify_age_bot"] as? Bool, value {
useVerifyAgeBot = value
}
if useVerifyAgeBot, let verifyAgeBotUsername = context.currentAppConfiguration.with({ $0 }).data?["verify_age_bot_username"] as? String {
miniappPromise.set(context.engine.peers.resolvePeerByName(name: verifyAgeBotUsername, referrer: nil)
|> mapToSignal { result in
if case let .result(peer) = result {
return .single(peer)
}
return .complete()
})
}
let infoScreen = AgeVerificationScreen(context: context, completion: { [weak parentController] check, availability in
if check {
var requiredAge = 18
@ -412,24 +426,60 @@ public func presentAgeVerification(context: AccountContext, parentController: Vi
requiredAge = Int(value)
}
let scanScreen = FaceScanScreen(context: context, availability: availability, requiredAge: requiredAge, completion: { [weak parentController] passed in
if passed {
let _ = updateAgeVerificationState(engine: context.engine, { _ in
return AgeVerificationState(verificationPassed: passed)
}).start()
completion()
let navigationController = parentController?.navigationController
Queue.mainQueue().after(2.0) {
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AgeVerification_Success_Title, text: presentationData.strings.AgeVerification_Success_Text, cancel: nil, destructive: false), action: { _ in return true })
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .window(.root))
}
let success = { [weak parentController] in
let _ = updateAgeVerificationState(engine: context.engine, { _ in
return AgeVerificationState(verificationPassed: true)
}).start()
completion()
let navigationController = parentController?.navigationController
Queue.mainQueue().after(2.0) {
let controller = UndoOverlayController(presentationData: presentationData, content: .actionSucceeded(title: presentationData.strings.AgeVerification_Success_Title, text: presentationData.strings.AgeVerification_Success_Text, cancel: nil, destructive: false), action: { _ in return true })
(navigationController?.viewControllers.last as? ViewController)?.present(controller, in: .current)
}
}
let failure = { [weak parentController] in
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: presentationData.strings.AgeVerification_Fail_Title, text: presentationData.strings.AgeVerification_Fail_Text, customUndoText: nil, timeout: nil), action: { _ in return true })
parentController?.present(controller, in: .current)
}
let _ = (miniappPromise.get()
|> take(1)
|> deliverOnMainQueue).start(next: { peer in
if let peer, let parentController {
context.sharedContext.openWebApp(
context: context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil,
verifyAgeCompletion: { age in
if age >= requiredAge {
success()
} else {
failure()
}
}
)
} else {
let controller = UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_banned", scale: 0.066, colors: [:], title: presentationData.strings.AgeVerification_Fail_Title, text: presentationData.strings.AgeVerification_Fail_Text, customUndoText: nil, timeout: nil), action: { _ in return true })
parentController?.present(controller, in: .window(.root))
let scanScreen = FaceScanScreen(context: context, availability: availability, completion: { age in
if age >= requiredAge {
success()
} else {
failure()
}
})
parentController?.push(scanScreen)
}
})
parentController?.push(scanScreen)
}
})
parentController?.push(infoScreen)

View file

@ -25,16 +25,13 @@ final class FaceScanScreenComponent: Component {
let context: AccountContext
let availability: Signal<AgeVerificationAvailability, NoError>
let requiredAge: Int
init(
context: AccountContext,
availability: Signal<AgeVerificationAvailability, NoError>,
requiredAge: Int
availability: Signal<AgeVerificationAvailability, NoError>
) {
self.context = context
self.availability = availability
self.requiredAge = requiredAge
}
static func ==(lhs: FaceScanScreenComponent, rhs: FaceScanScreenComponent) -> Bool {
@ -307,7 +304,7 @@ final class FaceScanScreenComponent: Component {
}
private func fillSegment(_ segmentIndex: Int) {
guard let component = self.component, !self.completedAngles.contains(segmentIndex) else {
guard !self.completedAngles.contains(segmentIndex) else {
return
}
self.completedAngles.insert(segmentIndex)
@ -321,7 +318,7 @@ final class FaceScanScreenComponent: Component {
if !self.ages.isEmpty {
let averageAge = self.ages.reduce(0, +) / Double(self.ages.count)
if let environment = self.environment, let controller = environment.controller() as? FaceScanScreen {
controller.completion(averageAge >= Double(component.requiredAge))
controller.completion(Int(averageAge))
controller.dismiss(animated: true)
}
} else {
@ -437,7 +434,7 @@ final class FaceScanScreenComponent: Component {
let center = CGPoint(x: availableSize.width / 2, y: environment.statusBarHeight + 10.0 + widthRadius * 1.3)
var previewScale = 1.0
var previewScale = 0.85
if self.processState == .tracking || self.processState == .readyToStart || self.processState == .completed || self.transitioningToViewFinder {
let circlePath = CGPath(roundedRect: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), cornerWidth: radius, cornerHeight: radius, transform: nil)
path.addPath(circlePath)
@ -545,21 +542,19 @@ final class FaceScanScreenComponent: Component {
public final class FaceScanScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate let completion: (Bool) -> Void
fileprivate let completion: (Int) -> Void
public init(
context: AccountContext,
availability: Signal<AgeVerificationAvailability, NoError>,
requiredAge: Int,
completion: @escaping (Bool) -> Void
completion: @escaping (Int) -> Void
) {
self.context = context
self.completion = completion
super.init(context: context, component: FaceScanScreenComponent(
context: context,
availability: availability,
requiredAge: requiredAge
availability: availability
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.title = ""

View file

@ -1257,7 +1257,8 @@ final class AffiliateProgramSetupScreenComponent: Component {
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
} else if let navigationController = controller.navigationController as? NavigationController {
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(item.peer), subject: nil, keepStack: .always, animated: true, pushController: { [weak navigationController] chatController, animated, completion in

View file

@ -1451,7 +1451,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil
payload: nil,
verifyAgeCompletion: nil
)
})
}

View file

@ -201,7 +201,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 20, displayCharacterLimit: true, apply: { [weak self] value in
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Title, titleFont: .bold, subtitle: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Text, value: "", placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
guard let self, let value else {
return
}
@ -264,7 +264,7 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
return
}
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 20, displayCharacterLimit: true, apply: { [weak self] value in
let promptController = promptController(sharedContext: self.context.sharedContext, updatedPresentationData: nil, text: params.presentationData.strings.PeerInfo_Gifts_RenameCollection_Title, titleFont: .bold, value: collection.title, placeholder: params.presentationData.strings.PeerInfo_Gifts_CreateCollection_Placeholder, characterLimit: 12, displayCharacterLimit: true, apply: { [weak self] value in
guard let self, let value else {
return
}

View file

@ -900,7 +900,21 @@ final class AuthorizedApplicationContext {
}
if openAppIfAny, case let .user(user) = peer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp), let parentController = self.rootController.viewControllers.last as? ViewController {
self.context.sharedContext.openWebApp(context: self.context, parentController: parentController, updatedPresentationData: nil, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: true, payload: nil)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: parentController,
updatedPresentationData: nil,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: true,
payload: nil,
verifyAgeCompletion: nil
)
} else {
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: alwaysKeepMessageId || isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil, setupReply: false) } : nil, activateInput: activateInput ? .text : nil))
}

View file

@ -27,7 +27,8 @@ func openWebAppImpl(
simple: Bool,
source: ChatOpenWebViewSource,
skipTermsOfService: Bool,
payload: String?
payload: String?,
verifyAgeCompletion: ((Int) -> Void)?
) {
if context.isFrozen {
parentController.push(context.sharedContext.makeAccountFreezeInfoScreen(context: context))
@ -246,7 +247,7 @@ func openWebAppImpl(
navigationController = parentController.effectiveNavigationController
}
return navigationController ?? (context.sharedContext.mainWindow?.viewController as? NavigationController)
})
}, verifyAgeCompletion: verifyAgeCompletion)
controller.navigationPresentation = .flatModal
parentController.push(controller)
@ -351,7 +352,21 @@ public extension ChatControllerImpl {
}
self.chatDisplayNode.dismissInput()
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: EnginePeer(peer), chatPeer: EnginePeer(peer), threadId: self.chatLocation.threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: false, payload: nil)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: self,
updatedPresentationData: self.updatedPresentationData,
botPeer: EnginePeer(peer),
chatPeer: EnginePeer(peer),
threadId: self.chatLocation.threadId,
buttonText: buttonText,
url: url,
simple: simple,
source: source,
skipTermsOfService: false,
payload: nil,
verifyAgeCompletion: nil
)
}
fileprivate static func botRequestSwitchInline(context: AccountContext, controller: ChatControllerImpl?, peerId: EnginePeer.Id, botAddress: String, query: String, chatTypes: [ReplyMarkupButtonRequestPeerType]?, completion: @escaping () -> Void) -> Void {
@ -616,7 +631,21 @@ public extension ChatControllerImpl {
}
})
} else {
context.sharedContext.openWebApp(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: payload)
context.sharedContext.openWebApp(
context: context,
parentController: parentController,
updatedPresentationData: updatedPresentationData,
botPeer: botPeer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: false,
payload: payload,
verifyAgeCompletion: nil
)
}
}
}

View file

@ -8907,7 +8907,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
commit()
})
} else {
self.context.sharedContext.openWebApp(context: self.context, parentController: self, updatedPresentationData: self.updatedPresentationData, botPeer: peer, chatPeer: nil, threadId: nil, buttonText: "", url: "", simple: true, source: .generic, skipTermsOfService: false, payload: botAppStart.payload)
self.context.sharedContext.openWebApp(
context: self.context,
parentController: self,
updatedPresentationData: self.updatedPresentationData,
botPeer: peer,
chatPeer: nil,
threadId: nil,
buttonText: "",
url: "",
simple: true,
source: .generic,
skipTermsOfService: false,
payload: botAppStart.payload,
verifyAgeCompletion: nil
)
commit()
}
})

View file

@ -3882,8 +3882,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return incomingMessagePrivacyScreen(context: context, value: value, exceptions: exceptions, update: update)
}
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload)
public func openWebApp(context: AccountContext, parentController: ViewController, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, botPeer: EnginePeer, chatPeer: EnginePeer?, threadId: Int64?, buttonText: String, url: String, simple: Bool, source: ChatOpenWebViewSource, skipTermsOfService: Bool, payload: String?, verifyAgeCompletion: ((Int) -> Void)?) {
openWebAppImpl(context: context, parentController: parentController, updatedPresentationData: updatedPresentationData, botPeer: botPeer, chatPeer: chatPeer, threadId: threadId, buttonText: buttonText, url: url, simple: simple, source: source, skipTermsOfService: skipTermsOfService, payload: payload, verifyAgeCompletion: verifyAgeCompletion)
}
public func makeAffiliateProgramSetupScreenInitialData(context: AccountContext, peerId: EnginePeer.Id, mode: AffiliateProgramSetupScreenMode) -> Signal<AffiliateProgramSetupScreenInitialData, NoError> {

View file

@ -653,10 +653,14 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.animateTransitionIn()
})
}
@available(iOSApplicationExtension 15.0, iOS 15.0, *)
func webView(_ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void) {
decisionHandler(.prompt)
if self.controller?.isVerifyAgeBot == true && type == .camera {
decisionHandler(.grant)
} else {
decisionHandler(.prompt)
}
}
func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
@ -1756,6 +1760,12 @@ public final class WebAppController: ViewController, AttachmentContainable {
}
case "web_app_hide_keyboard":
self.view.window?.endEditing(true)
case "web_app_verify_age":
if let json, self.controller?.isVerifyAgeBot == true {
if let ageValue = json["age"] as? Double {
self.controller?.verifyAgeCompletion?(Int(ageValue))
}
}
default:
break
}
@ -3281,6 +3291,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
public var completion: () -> Void = {}
public var requestSwitchInline: (String, [ReplyMarkupButtonRequestPeerType]?, @escaping () -> Void) -> Void = { _, _, _ in }
public var verifyAgeCompletion: ((Int) -> Void)?
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, params: WebAppParameters, replyToMessageId: MessageId?, threadId: Int64?) {
self.context = context
self.source = params.source
@ -3323,16 +3335,20 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.navigationItem.leftBarButtonItem?.action = #selector(self.cancelPressed)
self.navigationItem.leftBarButtonItem?.target = self
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
if !self.isVerifyAgeBot {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.moreButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
}
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
self.navigationItem.titleView = titleView
self.titleView = titleView
if !self.isVerifyAgeBot {
let titleView = WebAppTitleView(context: self.context, theme: self.presentationData.theme)
titleView.title = WebAppTitle(title: params.botName, counter: self.presentationData.strings.WebApp_Miniapp, isVerified: params.botVerified)
self.navigationItem.titleView = titleView
self.titleView = titleView
}
self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
@ -3387,6 +3403,13 @@ public final class WebAppController: ViewController, AttachmentContainable {
self.presentationDataDisposable?.dispose()
}
private var isVerifyAgeBot: Bool {
if let ageBotUsername = self.context.currentAppConfiguration.with({ $0 }).data?["verify_age_bot_username"] as? String {
return self.botAddress == ageBotUsername
}
return false
}
public func beforeMaximize(navigationController: NavigationController, completion: @escaping () -> Void) {
switch self.source {
case .generic, .settings:
@ -3857,7 +3880,8 @@ public func standaloneWebAppController(
willDismiss: @escaping () -> Void = {},
didDismiss: @escaping () -> Void = {},
getNavigationController: @escaping () -> NavigationController? = { return nil },
getSourceRect: (() -> CGRect?)? = nil
getSourceRect: (() -> CGRect?)? = nil,
verifyAgeCompletion: ((Int) -> Void)? = nil
) -> ViewController {
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: .peer(id: params.peerId), buttons: [.standalone], initialButton: .standalone, fromMenu: params.source == .menu, hasTextInput: false, isFullSize: params.fullSize, makeEntityInputView: {
return nil
@ -3868,6 +3892,7 @@ public func standaloneWebAppController(
webAppController.completion = completion
webAppController.getNavigationController = getNavigationController
webAppController.requestSwitchInline = requestSwitchInline
webAppController.verifyAgeCompletion = verifyAgeCompletion
present(webAppController, webAppController.mediaPickerContext)
}
controller.willDismiss = willDismiss

View file

@ -1,5 +1,5 @@
{
"app": "11.14",
"app": "11.13.3",
"xcode": "16.2",
"bazel": "8.2.1:22ff65b05869f6160e5157b1b425a14a62085d71d8baef571f462b8fe5a703a3",
"macos": "15"