Various improvements

This commit is contained in:
Ilya Laktyushin 2025-07-20 19:49:14 +02:00
parent 66f78a352a
commit c825438b72
11 changed files with 66 additions and 36 deletions

View File

@ -14736,8 +14736,8 @@ Sorry for the inconvenience.";
"Gift.Options.Gift.BuyLimitReached_any" = "You've already sent %@ of these gifts, and it's the limit."; "Gift.Options.Gift.BuyLimitReached_any" = "You've already sent %@ of these gifts, and it's the limit.";
"AgeVerification.Title" = "Age Verification"; "AgeVerification.Title" = "Age Verification";
"AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram."; "AgeVerification.Text" = "To access this content, you must confirm you are at least **18** years old.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Text.gb" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram."; "AgeVerification.Text.GB" = "To access this content, you must confirm you are at least **18** years old as required by UK law.\n\nThis is a one-time process using your phone's camera. Your selfie will not be stored by Telegram.";
"AgeVerification.Verify" = "Verify My Age"; "AgeVerification.Verify" = "Verify My Age";
"AgeVerification.Success.Title" = "Age check passed!"; "AgeVerification.Success.Title" = "Age check passed!";

View File

@ -12,6 +12,7 @@ public final class BotCheckoutController: ViewController {
public enum FetchError { public enum FetchError {
case generic case generic
case disallowedStarGifts case disallowedStarGifts
case starGiftsUserLimit
} }
public let form: BotPaymentForm public let form: BotPaymentForm
@ -58,6 +59,8 @@ public final class BotCheckoutController: ViewController {
switch error { switch error {
case .disallowedStarGift: case .disallowedStarGift:
return .disallowedStarGifts return .disallowedStarGifts
case .starGiftUserLimit:
return .starGiftsUserLimit
default: default:
return .generic return .generic
} }

View File

@ -180,6 +180,7 @@ public enum BotPaymentFormRequestError {
case noPaymentNeeded case noPaymentNeeded
case disallowedStarGift case disallowedStarGift
case starGiftResellTooEarly(Int32) case starGiftResellTooEarly(Int32)
case starGiftUserLimit
} }
extension BotPaymentInvoice { extension BotPaymentInvoice {
@ -488,6 +489,8 @@ func _internal_fetchBotPaymentForm(accountPeerId: PeerId, postbox: Postbox, netw
if let value = Int32(timeout) { if let value = Int32(timeout) {
return .fail(.starGiftResellTooEarly(value)) return .fail(.starGiftResellTooEarly(value))
} }
} else if error.errorDescription == "STARGIFT_USER_USAGE_LIMITED" {
return .fail(.starGiftUserLimit)
} }
return .fail(.generic) return .fail(.generic)
} }
@ -651,6 +654,7 @@ public enum SendBotPaymentFormError {
case alreadyPaid case alreadyPaid
case starGiftOutOfStock case starGiftOutOfStock
case disallowedStarGift case disallowedStarGift
case starGiftUserLimit
} }
public enum SendBotPaymentResult { public enum SendBotPaymentResult {

View File

@ -407,7 +407,12 @@ public func presentAgeVerification(context: AccountContext, parentController: Vi
} else { } else {
let infoScreen = AgeVerificationScreen(context: context, completion: { [weak parentController] check, availability in let infoScreen = AgeVerificationScreen(context: context, completion: { [weak parentController] check, availability in
if check { if check {
let scanScreen = FaceScanScreen(context: context, availability: availability, completion: { [weak parentController] passed in var requiredAge = 18
if let value = context.currentAppConfiguration.with({ $0 }).data?["verify_age_min"] as? Double {
requiredAge = Int(value)
}
let scanScreen = FaceScanScreen(context: context, availability: availability, requiredAge: requiredAge, completion: { [weak parentController] passed in
if passed { if passed {
let _ = updateAgeVerificationState(engine: context.engine, { _ in let _ = updateAgeVerificationState(engine: context.engine, { _ in
return AgeVerificationState(verificationPassed: passed) return AgeVerificationState(verificationPassed: passed)

View File

@ -20,20 +20,21 @@ import ZipArchive
import PlainButtonComponent import PlainButtonComponent
import MultilineTextComponent import MultilineTextComponent
private let requiredAge = 18
final class FaceScanScreenComponent: Component { final class FaceScanScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext let context: AccountContext
let availability: Signal<AgeVerificationAvailability, NoError> let availability: Signal<AgeVerificationAvailability, NoError>
let requiredAge: Int
init( init(
context: AccountContext, context: AccountContext,
availability: Signal<AgeVerificationAvailability, NoError> availability: Signal<AgeVerificationAvailability, NoError>,
requiredAge: Int
) { ) {
self.context = context self.context = context
self.availability = availability self.availability = availability
self.requiredAge = requiredAge
} }
static func ==(lhs: FaceScanScreenComponent, rhs: FaceScanScreenComponent) -> Bool { static func ==(lhs: FaceScanScreenComponent, rhs: FaceScanScreenComponent) -> Bool {
@ -109,7 +110,7 @@ final class FaceScanScreenComponent: Component {
self.backgroundColor = .black self.backgroundColor = .black
self.previewLayer.backgroundColor = UIColor.red.cgColor //self.previewLayer.backgroundColor = UIColor.red.cgColor
self.previewLayer.videoGravity = .resizeAspectFill self.previewLayer.videoGravity = .resizeAspectFill
self.layer.addSublayer(previewLayer) self.layer.addSublayer(previewLayer)
@ -240,7 +241,7 @@ final class FaceScanScreenComponent: Component {
let targetCenter = CGPoint(x: 0.5, y: 0.5) let targetCenter = CGPoint(x: 0.5, y: 0.5)
let distance = sqrt(pow(faceCenter.x - targetCenter.x, 2) + pow(faceCenter.y - targetCenter.y, 2)) let distance = sqrt(pow(faceCenter.x - targetCenter.x, 2) + pow(faceCenter.y - targetCenter.y, 2))
if distance < 0.35 { if distance < 0.24 {
switch processState { switch processState {
case .waitingForFace: case .waitingForFace:
self.processState = .positioning self.processState = .positioning
@ -306,7 +307,7 @@ final class FaceScanScreenComponent: Component {
} }
private func fillSegment(_ segmentIndex: Int) { private func fillSegment(_ segmentIndex: Int) {
guard !self.completedAngles.contains(segmentIndex) else { guard let component = self.component, !self.completedAngles.contains(segmentIndex) else {
return return
} }
self.completedAngles.insert(segmentIndex) self.completedAngles.insert(segmentIndex)
@ -320,7 +321,7 @@ final class FaceScanScreenComponent: Component {
if !self.ages.isEmpty { if !self.ages.isEmpty {
let averageAge = self.ages.reduce(0, +) / Double(self.ages.count) let averageAge = self.ages.reduce(0, +) / Double(self.ages.count)
if let environment = self.environment, let controller = environment.controller() as? FaceScanScreen { if let environment = self.environment, let controller = environment.controller() as? FaceScanScreen {
controller.completion(averageAge >= Double(requiredAge)) controller.completion(averageAge >= Double(component.requiredAge))
controller.dismiss(animated: true) controller.dismiss(animated: true)
} }
} else { } else {
@ -457,7 +458,6 @@ final class FaceScanScreenComponent: Component {
self.frameView.frame = frameViewFrame self.frameView.frame = frameViewFrame
self.frameView.update(size: frameViewFrame.size) self.frameView.update(size: frameViewFrame.size)
//TODO:localize
var instructionString = environment.strings.FaceScan_Instruction_Position var instructionString = environment.strings.FaceScan_Instruction_Position
switch self.processState { switch self.processState {
case .waitingForFace, .positioning: case .waitingForFace, .positioning:
@ -550,6 +550,7 @@ public final class FaceScanScreen: ViewControllerComponentContainer {
public init( public init(
context: AccountContext, context: AccountContext,
availability: Signal<AgeVerificationAvailability, NoError>, availability: Signal<AgeVerificationAvailability, NoError>,
requiredAge: Int,
completion: @escaping (Bool) -> Void completion: @escaping (Bool) -> Void
) { ) {
self.context = context self.context = context
@ -557,7 +558,8 @@ public final class FaceScanScreen: ViewControllerComponentContainer {
super.init(context: context, component: FaceScanScreenComponent( super.init(context: context, component: FaceScanScreenComponent(
context: context, context: context,
availability: availability availability: availability,
requiredAge: requiredAge
), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil) ), navigationBarAppearance: .none, theme: .default, updatedPresentationData: nil)
self.title = "" self.title = ""

View File

@ -363,6 +363,8 @@ final class GiftSetupScreenComponent: Component {
let entities = generateChatInputTextEntities(self.textInputState.text) let entities = generateChatInputTextEntities(self.textInputState.text)
var finalPrice: Int64 var finalPrice: Int64
var perUserLimit: Int32?
var giftFile: TelegramMediaFile?
let source: BotPaymentInvoiceSource let source: BotPaymentInvoiceSource
switch component.subject { switch component.subject {
case let .premium(product): case let .premium(product):
@ -377,6 +379,8 @@ final class GiftSetupScreenComponent: Component {
if self.includeUpgrade, let upgradeStars = starGift.upgradeStars { if self.includeUpgrade, let upgradeStars = starGift.upgradeStars {
finalPrice += upgradeStars finalPrice += upgradeStars
} }
perUserLimit = starGift.perUserLimit?.total
giftFile = starGift.file
source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities) source = .starGift(hideName: self.hideName, includeUpgrade: self.includeUpgrade, peerId: peerId, giftId: starGift.id, text: self.textInputState.text.string, entities: entities)
} }
@ -395,6 +399,8 @@ final class GiftSetupScreenComponent: Component {
switch error { switch error {
case .disallowedStarGifts: case .disallowedStarGifts:
return .fail(.disallowedStarGift) return .fail(.disallowedStarGift)
case .starGiftsUserLimit:
return .fail(.starGiftUserLimit)
default: default:
return .fail(.generic) return .fail(.generic)
} }
@ -468,6 +474,14 @@ final class GiftSetupScreenComponent: Component {
var errorText: String? var errorText: String?
switch error { switch error {
case .starGiftUserLimit:
if let perUserLimit, let giftFile {
let text = presentationData.strings.Gift_Options_Gift_BuyLimitReached(perUserLimit)
let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: giftFile, loop: true, title: nil, text: text, undoText: nil, customAction: nil), action: { _ in return false })
controller.present(undoController, in: .current)
return
}
return
case .starGiftOutOfStock: case .starGiftOutOfStock:
errorText = presentationData.strings.Gift_Send_ErrorOutOfStock errorText = presentationData.strings.Gift_Send_ErrorOutOfStock
case .disallowedStarGift: case .disallowedStarGift:

View File

@ -288,11 +288,14 @@ private final class GiftViewSheetContent: CombinedComponent {
} }
var minRequiredAmount = StarsAmount(value: 100, nanos: 0) var minRequiredAmount = StarsAmount(value: 100, nanos: 0)
var canUpgrade = false
if let resellStars = self.subject.arguments?.resellStars { if let resellStars = self.subject.arguments?.resellStars {
minRequiredAmount = StarsAmount(value: resellStars, nanos: 0) minRequiredAmount = StarsAmount(value: resellStars, nanos: 0)
} else if let arguments = self.subject.arguments, arguments.canUpgrade && arguments.upgradeStars == nil {
canUpgrade = true
} }
if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount { if let starsContext = context.starsContext, let state = starsContext.currentState, state.balance < minRequiredAmount || canUpgrade {
self.optionsDisposable = (context.engine.payments.starsTopUpOptions() self.optionsDisposable = (context.engine.payments.starsTopUpOptions()
|> deliverOnMainQueue).start(next: { [weak self] options in |> deliverOnMainQueue).start(next: { [weak self] options in
guard let self else { guard let self else {

View File

@ -23,17 +23,20 @@ final class AddGiftsScreenComponent: Component {
let context: AccountContext let context: AccountContext
let peerId: EnginePeer.Id let peerId: EnginePeer.Id
let collectionId: Int32 let collectionId: Int32
let remainingCount: Int32
let profileGifts: ProfileGiftsContext let profileGifts: ProfileGiftsContext
init( init(
context: AccountContext, context: AccountContext,
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
collectionId: Int32, collectionId: Int32,
remainingCount: Int32,
profileGifts: ProfileGiftsContext profileGifts: ProfileGiftsContext
) { ) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.collectionId = collectionId self.collectionId = collectionId
self.remainingCount = remainingCount
self.profileGifts = profileGifts self.profileGifts = profileGifts
} }
@ -128,7 +131,7 @@ final class AddGiftsScreenComponent: Component {
if let current = self.giftsListView { if let current = self.giftsListView {
giftsListView = current giftsListView = current
} else { } else {
giftsListView = GiftsListView(context: component.context, peerId: component.peerId, profileGifts: component.profileGifts, giftsCollections: nil, canSelect: true, ignoreCollection: component.collectionId) giftsListView = GiftsListView(context: component.context, peerId: component.peerId, profileGifts: component.profileGifts, giftsCollections: nil, canSelect: true, ignoreCollection: component.collectionId, remainingSelectionCount: component.remainingCount)
giftsListView.selectionUpdated = { [weak self] in giftsListView.selectionUpdated = { [weak self] in
guard let self else { guard let self else {
return return
@ -248,6 +251,7 @@ public final class AddGiftsScreen: ViewControllerComponentContainer {
context: AccountContext, context: AccountContext,
peerId: EnginePeer.Id, peerId: EnginePeer.Id,
collectionId: Int32, collectionId: Int32,
remainingCount: Int32,
completion: @escaping ([ProfileGiftsContext.State.StarGift]) -> Void completion: @escaping ([ProfileGiftsContext.State.StarGift]) -> Void
) { ) {
self.context = context self.context = context
@ -264,10 +268,10 @@ public final class AddGiftsScreen: ViewControllerComponentContainer {
context: context, context: context,
peerId: peerId, peerId: peerId,
collectionId: collectionId, collectionId: collectionId,
remainingCount: remainingCount,
profileGifts: self.profileGifts profileGifts: self.profileGifts
), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil) ), navigationBarAppearance: .default, theme: .default, updatedPresentationData: nil)
self.title = presentationData.strings.AddGifts_Title self.title = presentationData.strings.AddGifts_Title
self.navigationPresentation = .modal self.navigationPresentation = .modal

View File

@ -36,6 +36,7 @@ final class GiftsListView: UIView {
private let canSelect: Bool private let canSelect: Bool
private let ignoreCollection: Int32? private let ignoreCollection: Int32?
private let remainingSelectionCount: Int32
private var dataDisposable: Disposable? private var dataDisposable: Disposable?
@ -124,13 +125,14 @@ final class GiftsListView: UIView {
var contextAction: ((ProfileGiftsContext.State.StarGift, UIView, ContextGesture) -> Void)? var contextAction: ((ProfileGiftsContext.State.StarGift, UIView, ContextGesture) -> Void)?
var addToCollection: (() -> Void)? var addToCollection: (() -> Void)?
init(context: AccountContext, peerId: PeerId, profileGifts: ProfileGiftsContext, giftsCollections: ProfileGiftsCollectionsContext?, canSelect: Bool, ignoreCollection: Int32? = nil) { init(context: AccountContext, peerId: PeerId, profileGifts: ProfileGiftsContext, giftsCollections: ProfileGiftsCollectionsContext?, canSelect: Bool, ignoreCollection: Int32? = nil, remainingSelectionCount: Int32 = 0) {
self.context = context self.context = context
self.peerId = peerId self.peerId = peerId
self.profileGifts = profileGifts self.profileGifts = profileGifts
self.giftsCollections = giftsCollections self.giftsCollections = giftsCollections
self.canSelect = canSelect self.canSelect = canSelect
self.ignoreCollection = ignoreCollection self.ignoreCollection = ignoreCollection
self.remainingSelectionCount = remainingSelectionCount
if let value = context.currentAppConfiguration.with({ $0 }).data?["stargifts_pinned_to_top_limit"] as? Double { if let value = context.currentAppConfiguration.with({ $0 }).data?["stargifts_pinned_to_top_limit"] as? Double {
self.maxPinnedCount = Int(value) self.maxPinnedCount = Int(value)
@ -548,8 +550,10 @@ final class GiftsListView: UIView {
if self.selectedItemIds.contains(itemReferenceId) { if self.selectedItemIds.contains(itemReferenceId) {
self.selectedItemIds.remove(itemReferenceId) self.selectedItemIds.remove(itemReferenceId)
} else { } else {
self.selectedItemIds.insert(itemReferenceId) if self.selectedItemIds.count < self.remainingSelectionCount {
self.selectedItemsMap[itemReferenceId] = product self.selectedItemIds.insert(itemReferenceId)
self.selectedItemsMap[itemReferenceId] = product
}
} }
self.selectionUpdated() self.selectionUpdated()
self.updateScrolling(transition: .easeInOut(duration: 0.25)) self.updateScrolling(transition: .easeInOut(duration: 0.25))

View File

@ -242,7 +242,15 @@ public final class PeerInfoGiftsPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
public func addGiftsToCollection(id: Int32) { public func addGiftsToCollection(id: Int32) {
let screen = AddGiftsScreen(context: self.context, peerId: self.peerId, collectionId: id, completion: { [weak self] gifts in var collectionGiftsMaxCount: Int32 = 1000
if let value = self.context.currentAppConfiguration.with({ $0 }).data?["stargifts_collection_gifts_limit"] as? Double {
collectionGiftsMaxCount = Int32(value)
}
var remainingCount = collectionGiftsMaxCount
if let currentCount = self.giftsListView.profileGifts.currentState?.count {
remainingCount = max(0, collectionGiftsMaxCount - currentCount)
}
let screen = AddGiftsScreen(context: self.context, peerId: self.peerId, collectionId: id, remainingCount: remainingCount, completion: { [weak self] gifts in
guard let self else { guard let self else {
return return
} }

View File

@ -31,7 +31,6 @@ import PeerInfoScreen
import PeerInfoStoryGridScreen import PeerInfoStoryGridScreen
import ShareWithPeersScreen import ShareWithPeersScreen
import ChatEmptyNode import ChatEmptyNode
//import FaceScanScreen
import UndoUI import UndoUI
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
@ -237,22 +236,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
self.accountSettingsController = accountSettingsController self.accountSettingsController = accountSettingsController
self.rootTabController = tabBarController self.rootTabController = tabBarController
self.pushViewController(tabBarController, animated: false) self.pushViewController(tabBarController, animated: false)
// Queue.mainQueue().after(1.0) {
// let context = self.context
// let infoScreen = AgeVerificationScreen(context: context, completion: { [weak chatListController] proceed in
// if proceed {
// let scanScreen = FaceScanScreen(context: context, completion: { success in
// let controller = UndoOverlayController(presentationData: self.presentationData, content: .actionSucceeded(title: "Age check passed!", text: "You can now view this content.", cancel: nil, destructive: false), elevatedLayout: true, action: { _ in return true })
// Queue.mainQueue().after(0.1) {
// chatListController?.present(controller, in: .window(.root))
// }
// })
// chatListController?.push(scanScreen)
// }
// })
// chatListController.push(infoScreen)
// }
} }
public func updateRootControllers(showCallsTab: Bool) { public func updateRootControllers(showCallsTab: Bool) {