mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Various improvements
This commit is contained in:
parent
a651bb589d
commit
a8b02015ce
@ -14096,3 +14096,13 @@ Sorry for the inconvenience.";
|
|||||||
"Gift.Unpin.Unpin" = "Unpin";
|
"Gift.Unpin.Unpin" = "Unpin";
|
||||||
|
|
||||||
"ChatList.Search.Ad" = "Ad";
|
"ChatList.Search.Ad" = "Ad";
|
||||||
|
|
||||||
|
"Login.Fee.Title" = "SMS Fee";
|
||||||
|
"Login.Fee.SmsCost.Title" = "High SMS Costs";
|
||||||
|
"Login.Fee.SmsCost.Text" = "Telecom providers in your country (%@) charge Telegram very high prices for SMS.";
|
||||||
|
"Login.Fee.Verification.Title" = "Verification Required";
|
||||||
|
"Login.Fee.Verification.Text" = "Telegram needs to send you an SMS with a verification code to confirm your phone number.";
|
||||||
|
"Login.Fee.Support.Title" = "Support via [Telegram Premium >]()";
|
||||||
|
"Login.Fee.Support.Text" = "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.";
|
||||||
|
"Login.Fee.SignUp" = "Sign Up for %@";
|
||||||
|
"Login.Fee.GetPremiumForAWeek" = "Get Telegram Premium for 1 week";
|
||||||
|
@ -848,7 +848,13 @@ final class AuthorizationSequenceCodeEntryControllerNode: ASDisplayNode, UITextF
|
|||||||
|
|
||||||
let pasteSize = self.pasteButton.measure(layout.size)
|
let pasteSize = self.pasteButton.measure(layout.size)
|
||||||
let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0)
|
let pasteButtonSize = CGSize(width: pasteSize.width + 16.0, height: 24.0)
|
||||||
transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: layout.size.width - 40.0 - pasteButtonSize.width, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize))
|
let pasteOriginX: CGFloat
|
||||||
|
if case .compact = layout.metrics.widthClass {
|
||||||
|
pasteOriginX = layout.size.width - 40.0 - pasteButtonSize.width
|
||||||
|
} else {
|
||||||
|
pasteOriginX = self.textField.frame.maxX + 32.0
|
||||||
|
}
|
||||||
|
transition.updateFrame(node: self.pasteButton, frame: CGRect(origin: CGPoint(x: pasteOriginX, y: self.textField.frame.midY - pasteButtonSize.height / 2.0), size: pasteButtonSize))
|
||||||
self.hintArrowNode.isHidden = true
|
self.hintArrowNode.isHidden = true
|
||||||
} else if case .word = codeType {
|
} else if case .word = codeType {
|
||||||
self.hintButtonNode.alpha = 0.0
|
self.hintButtonNode.alpha = 0.0
|
||||||
|
@ -198,7 +198,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
let titleSize = self.title.update(
|
let titleSize = self.title.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
MultilineTextComponent(text: .plain(NSAttributedString(string: "SMS Fee", font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)))
|
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_Title, font: Font.bold(28.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)))
|
||||||
),
|
),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||||
@ -218,9 +218,9 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "cost",
|
id: "cost",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "High SMS Costs",
|
title: environment.strings.Login_Fee_SmsCost_Title,
|
||||||
titleColor: textColor,
|
titleColor: textColor,
|
||||||
text: "Telecom providers in your country (\(countryName)) charge Telegram very high prices for SMS.",
|
text: environment.strings.Login_Fee_SmsCost_Text(countryName).string,
|
||||||
textColor: secondaryTextColor,
|
textColor: secondaryTextColor,
|
||||||
iconName: "Premium/Authorization/Cost",
|
iconName: "Premium/Authorization/Cost",
|
||||||
iconColor: linkColor
|
iconColor: linkColor
|
||||||
@ -231,9 +231,9 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "verification",
|
id: "verification",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Verification Required",
|
title: environment.strings.Login_Fee_Verification_Title,
|
||||||
titleColor: textColor,
|
titleColor: textColor,
|
||||||
text: "Telegram needs to send you an SMS with a verification code to confirm your phone number.",
|
text: environment.strings.Login_Fee_Verification_Text,
|
||||||
textColor: secondaryTextColor,
|
textColor: secondaryTextColor,
|
||||||
iconName: "Premium/Authorization/Verification",
|
iconName: "Premium/Authorization/Verification",
|
||||||
iconColor: linkColor
|
iconColor: linkColor
|
||||||
@ -242,11 +242,11 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
)
|
)
|
||||||
items.append(
|
items.append(
|
||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "withdrawal",
|
id: "support",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Support via [Telegram Premium >]()",
|
title: environment.strings.Login_Fee_Support_Title,
|
||||||
titleColor: textColor,
|
titleColor: textColor,
|
||||||
text: "Sign up for a 1-week Telegram Premium subscription to help cover the SMS costs.",
|
text: environment.strings.Login_Fee_Support_Text,
|
||||||
textColor: secondaryTextColor,
|
textColor: secondaryTextColor,
|
||||||
iconName: "Premium/Authorization/Support",
|
iconName: "Premium/Authorization/Support",
|
||||||
iconColor: linkColor,
|
iconColor: linkColor,
|
||||||
@ -315,7 +315,8 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
priceString = "–"
|
priceString = "–"
|
||||||
}
|
}
|
||||||
let buttonString = "Sign up for \(priceString)"
|
|
||||||
|
let buttonString = environment.strings.Login_Fee_SignUp(priceString).string
|
||||||
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
let buttonAttributedString = NSMutableAttributedString(string: buttonString, font: Font.semibold(17.0), textColor: environment.theme.list.itemCheckColors.foregroundColor, paragraphAlignment: .center)
|
||||||
let buttonSize = self.button.update(
|
let buttonSize = self.button.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
@ -331,7 +332,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
|
|||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
VStack([
|
VStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
|
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(text: .plain(buttonAttributedString)))),
|
||||||
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: "Get Telegram Premium for 1 week", font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))))
|
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Login_Fee_GetPremiumForAWeek, font: Font.medium(11.0), textColor: environment.theme.list.itemCheckColors.foregroundColor.withAlphaComponent(0.7), paragraphAlignment: .center)))))
|
||||||
], spacing: 1.0)
|
], spacing: 1.0)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -300,6 +300,8 @@ private final class ScrollContent: CombinedComponent {
|
|||||||
horizontalAlignment: .center,
|
horizontalAlignment: .center,
|
||||||
maximumNumberOfLines: 0,
|
maximumNumberOfLines: 0,
|
||||||
lineSpacing: 0.2,
|
lineSpacing: 0.2,
|
||||||
|
highlightColor: linkColor.withAlphaComponent(0.1),
|
||||||
|
highlightInset: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: -8.0),
|
||||||
highlightAction: { attributes in
|
highlightAction: { attributes in
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||||
@ -598,6 +600,7 @@ private final class ParagraphComponent: CombinedComponent {
|
|||||||
horizontalAlignment: .natural,
|
horizontalAlignment: .natural,
|
||||||
maximumNumberOfLines: 0,
|
maximumNumberOfLines: 0,
|
||||||
lineSpacing: 0.2,
|
lineSpacing: 0.2,
|
||||||
|
highlightColor: accentColor.withAlphaComponent(0.1),
|
||||||
highlightAction: { attributes in
|
highlightAction: { attributes in
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] {
|
||||||
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
return NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)
|
||||||
@ -949,6 +952,7 @@ public class AdsInfoScreen: ViewController {
|
|||||||
context: controller.context,
|
context: controller.context,
|
||||||
theme: self.presentationData.theme,
|
theme: self.presentationData.theme,
|
||||||
title: self.presentationData.strings.AdsInfo_Understood,
|
title: self.presentationData.strings.AdsInfo_Understood,
|
||||||
|
showBackground: controller.mode != .search,
|
||||||
action: { [weak self] in
|
action: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -992,7 +996,7 @@ public class AdsInfoScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var defaultTopInset: CGFloat {
|
private var defaultTopInset: CGFloat {
|
||||||
guard let layout = self.currentLayout else {
|
guard let layout = self.currentLayout, let controller = self.controller else {
|
||||||
return 210.0
|
return 210.0
|
||||||
}
|
}
|
||||||
if case .compact = layout.metrics.widthClass {
|
if case .compact = layout.metrics.widthClass {
|
||||||
@ -1006,9 +1010,13 @@ public class AdsInfoScreen: ViewController {
|
|||||||
let contentHeight = self.containerExternalState.contentHeight
|
let contentHeight = self.containerExternalState.contentHeight
|
||||||
let footerHeight = self.footerHeight
|
let footerHeight = self.footerHeight
|
||||||
if contentHeight > 0.0 {
|
if contentHeight > 0.0 {
|
||||||
let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0
|
if case .search = controller.mode {
|
||||||
if delta > 0.0 {
|
return (layout.size.height - containerTopInset) - contentHeight
|
||||||
defaultTopInset += delta
|
} else {
|
||||||
|
let delta = (layout.size.height - defaultTopInset - containerTopInset) - contentHeight - footerHeight - 16.0
|
||||||
|
if delta > 0.0 {
|
||||||
|
defaultTopInset += delta
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return defaultTopInset
|
return defaultTopInset
|
||||||
@ -1029,7 +1037,7 @@ public class AdsInfoScreen: ViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
@objc func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||||
guard let layout = self.currentLayout else {
|
guard let layout = self.currentLayout, let controller = self.controller else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1064,6 +1072,9 @@ public class AdsInfoScreen: ViewController {
|
|||||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||||
|
|
||||||
var translation = recognizer.translation(in: self.view).y
|
var translation = recognizer.translation(in: self.view).y
|
||||||
|
if case .search = controller.mode {
|
||||||
|
translation = max(0.0, translation)
|
||||||
|
}
|
||||||
|
|
||||||
var currentOffset = topInset + translation
|
var currentOffset = topInset + translation
|
||||||
|
|
||||||
@ -1111,8 +1122,12 @@ public class AdsInfoScreen: ViewController {
|
|||||||
|
|
||||||
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
let contentOffset = scrollView?.contentOffset.y ?? 0.0
|
||||||
|
|
||||||
let translation = recognizer.translation(in: self.view).y
|
var translation = recognizer.translation(in: self.view).y
|
||||||
var velocity = recognizer.velocity(in: self.view)
|
var velocity = recognizer.velocity(in: self.view)
|
||||||
|
if case .search = controller.mode {
|
||||||
|
translation = max(0.0, translation)
|
||||||
|
velocity.y = max(0.0, velocity.y)
|
||||||
|
}
|
||||||
|
|
||||||
if self.isExpanded {
|
if self.isExpanded {
|
||||||
if contentOffset > 0.1 {
|
if contentOffset > 0.1 {
|
||||||
@ -1435,12 +1450,14 @@ private final class FooterComponent: Component {
|
|||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
let title: String
|
let title: String
|
||||||
|
let showBackground: Bool
|
||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
init(context: AccountContext, theme: PresentationTheme, title: String, action: @escaping () -> Void) {
|
init(context: AccountContext, theme: PresentationTheme, title: String, showBackground: Bool, action: @escaping () -> Void) {
|
||||||
self.context = context
|
self.context = context
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.showBackground = showBackground
|
||||||
self.action = action
|
self.action = action
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1454,6 +1471,9 @@ private final class FooterComponent: Component {
|
|||||||
if lhs.title != rhs.title {
|
if lhs.title != rhs.title {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.showBackground != rhs.showBackground {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1494,6 +1514,9 @@ private final class FooterComponent: Component {
|
|||||||
self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor
|
self.separator.backgroundColor = component.theme.rootController.tabBar.separatorColor.cgColor
|
||||||
transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
transition.setFrame(layer: self.separator, frame: CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
self.backgroundView.isHidden = !component.showBackground
|
||||||
|
self.separator.isHidden = !component.showBackground
|
||||||
|
|
||||||
let buttonSize = self.button.update(
|
let buttonSize = self.button.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(
|
component: AnyComponent(
|
||||||
|
@ -1868,15 +1868,15 @@ public class StarsTransactionScreen: ViewControllerComponentContainer {
|
|||||||
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
guard let self, let navigationController = self.navigationController as? NavigationController else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.dismissAnimated()
|
||||||
|
|
||||||
let _ = (context.engine.privacy.requestAccountPrivacySettings()
|
let _ = (context.engine.privacy.requestAccountPrivacySettings()
|
||||||
|> deliverOnMainQueue).start(next: { [weak self, weak navigationController] privacySettings in
|
|> deliverOnMainQueue).start(next: { [weak navigationController] privacySettings in
|
||||||
let controller = context.sharedContext.makeIncomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in
|
let controller = context.sharedContext.makeIncomingMessagePrivacyScreen(context: context, value: privacySettings.globalSettings.nonContactChatsPrivacy, exceptions: privacySettings.noPaidMessages, update: { settingValue in
|
||||||
let _ = context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue).start()
|
let _ = context.engine.privacy.updateNonContactChatsPrivacy(value: settingValue).start()
|
||||||
})
|
})
|
||||||
navigationController?.pushViewController(controller)
|
Queue.mainQueue().after(0.4) {
|
||||||
|
navigationController?.pushViewController(controller)
|
||||||
Queue.mainQueue().after(1.0) {
|
|
||||||
self?.dismissAnimated()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1558,25 +1558,46 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
controller.parentController()?.lockOrientation = lock
|
controller.parentController()?.lockOrientation = lock
|
||||||
}
|
}
|
||||||
case "web_app_device_storage_save_key":
|
case "web_app_device_storage_save_key":
|
||||||
if let json, let requestId = json["req_id"] as? String, let key = json["key"] as? String, let value = json["value"] {
|
if let json, let requestId = json["req_id"] as? String {
|
||||||
var effectiveValue: String?
|
if let key = json["key"] as? String {
|
||||||
if let stringValue = value as? String {
|
let value = json["value"]
|
||||||
effectiveValue = stringValue
|
|
||||||
|
var effectiveValue: String?
|
||||||
|
if let stringValue = value as? String {
|
||||||
|
effectiveValue = stringValue
|
||||||
|
} else if value is NSNull {
|
||||||
|
effectiveValue = nil
|
||||||
|
} else {
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": "VALUE_INVALID"
|
||||||
|
]
|
||||||
|
self.webView?.sendEvent(name: "device_storage_failed", data: data.string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = self.context.engine.peers.setBotStorageValue(peerId: controller.botId, key: key, value: effectiveValue).start(error: { [weak self] error in
|
||||||
|
var errorValue = "UNKNOWN_ERROR"
|
||||||
|
if case .quotaExceeded = error {
|
||||||
|
errorValue = "QUOTA_EXCEEDED"
|
||||||
|
}
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": errorValue
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "device_storage_failed", data: data.string)
|
||||||
|
}, completed: { [weak self] in
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "device_storage_key_saved", data: data.string)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
effectiveValue = nil
|
|
||||||
}
|
|
||||||
let _ = self.context.engine.peers.setBotStorageValue(peerId: controller.botId, key: key, value: effectiveValue).start(error: { [weak self] _ in
|
|
||||||
let data: JSON = [
|
let data: JSON = [
|
||||||
"req_id": requestId,
|
"req_id": requestId,
|
||||||
"error": "UNKNOWN_ERROR"
|
"error": "KEY_INVALID"
|
||||||
]
|
]
|
||||||
self?.webView?.sendEvent(name: "device_storage_failed", data: data.string)
|
self.webView?.sendEvent(name: "device_storage_failed", data: data.string)
|
||||||
}, completed: { [weak self] in
|
}
|
||||||
let data: JSON = [
|
|
||||||
"req_id": requestId
|
|
||||||
]
|
|
||||||
self?.webView?.sendEvent(name: "device_storage_key_saved", data: data.string)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
case "web_app_device_storage_get_key":
|
case "web_app_device_storage_get_key":
|
||||||
if let json, let requestId = json["req_id"] as? String {
|
if let json, let requestId = json["req_id"] as? String {
|
||||||
@ -1607,6 +1628,78 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self?.webView?.sendEvent(name: "device_storage_cleared", data: data.string)
|
self?.webView?.sendEvent(name: "device_storage_cleared", data: data.string)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
case "web_app_secure_storage_save_key":
|
||||||
|
if let json, let requestId = json["req_id"] as? String {
|
||||||
|
if let key = json["key"] as? String {
|
||||||
|
let value = json["value"]
|
||||||
|
|
||||||
|
var effectiveValue: String?
|
||||||
|
if let stringValue = value as? String {
|
||||||
|
effectiveValue = stringValue
|
||||||
|
} else if value is NSNull {
|
||||||
|
effectiveValue = nil
|
||||||
|
} else {
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": "VALUE_INVALID"
|
||||||
|
]
|
||||||
|
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let _ = (WebAppSecureStorage.setValue(userId: self.context.account.peerId, botId: controller.botId, key: key, value: effectiveValue)
|
||||||
|
|> deliverOnMainQueue).start(error: { [weak self] error in
|
||||||
|
var errorValue = "UNKNOWN_ERROR"
|
||||||
|
if case .quotaExceeded = error {
|
||||||
|
errorValue = "QUOTA_EXCEEDED"
|
||||||
|
}
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": errorValue
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
|
||||||
|
}, completed: { [weak self] in
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "secure_storage_key_saved", data: data.string)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": "KEY_INVALID"
|
||||||
|
]
|
||||||
|
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "web_app_secure_storage_get_key":
|
||||||
|
if let json, let requestId = json["req_id"] as? String {
|
||||||
|
if let key = json["key"] as? String {
|
||||||
|
let _ = (WebAppSecureStorage.getValue(userId: self.context.account.peerId, botId: controller.botId, key: key)
|
||||||
|
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"value": value ?? NSNull()
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "secure_storage_key_received", data: data.string)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId,
|
||||||
|
"error": "KEY_INVALID"
|
||||||
|
]
|
||||||
|
self.webView?.sendEvent(name: "secure_storage_failed", data: data.string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "web_app_secure_storage_clear":
|
||||||
|
if let json, let requestId = json["req_id"] as? String {
|
||||||
|
let _ = (WebAppSecureStorage.clearStorage(userId: self.context.account.peerId, botId: controller.botId)
|
||||||
|
|> deliverOnMainQueue).start(completed: { [weak self] in
|
||||||
|
let data: JSON = [
|
||||||
|
"req_id": requestId
|
||||||
|
]
|
||||||
|
self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string)
|
||||||
|
})
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
147
submodules/WebUI/Sources/WebAppSecureStorage.swift
Normal file
147
submodules/WebUI/Sources/WebAppSecureStorage.swift
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
import SwiftSignalKit
|
||||||
|
import TelegramCore
|
||||||
|
|
||||||
|
final class WebAppSecureStorage {
|
||||||
|
enum Error {
|
||||||
|
case quotaExceeded
|
||||||
|
case unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
static private let maxKeyCount = 10
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
static private func keyPrefix(userId: EnginePeer.Id, botId: EnginePeer.Id) -> String {
|
||||||
|
return "A\(UInt64(bitPattern: userId.toInt64()))WebBot\(UInt64(bitPattern: botId.toInt64()))Key_"
|
||||||
|
}
|
||||||
|
|
||||||
|
static private func makeQuery(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String) -> [String: Any] {
|
||||||
|
let identifier = self.keyPrefix(userId: userId, botId: botId) + key
|
||||||
|
return [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: identifier,
|
||||||
|
kSecAttrService as String: "TMASecureStorage"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
static private func countKeys(userId: EnginePeer.Id, botId: EnginePeer.Id) -> Int {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: "TMASecureStorage",
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||||
|
kSecReturnAttributes as String: true
|
||||||
|
]
|
||||||
|
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
if status == errSecSuccess, let items = result as? [[String: Any]] {
|
||||||
|
let relevantPrefix = self.keyPrefix(userId: userId, botId: botId)
|
||||||
|
let count = items.filter {
|
||||||
|
if let account = $0[kSecAttrAccount as String] as? String {
|
||||||
|
return account.hasPrefix(relevantPrefix)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}.count
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setValue(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String, value: String?) -> Signal<Never, WebAppSecureStorage.Error> {
|
||||||
|
var query = makeQuery(userId: userId, botId: botId, key: key)
|
||||||
|
if value == nil {
|
||||||
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
|
if status == errSecSuccess || status == errSecItemNotFound {
|
||||||
|
return .complete()
|
||||||
|
} else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let valueData = value?.data(using: .utf8) else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
query[kSecAttrAccessible as String] = kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
|
||||||
|
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||||
|
if status == errSecSuccess {
|
||||||
|
let updateQuery: [String: Any] = [
|
||||||
|
kSecValueData as String: valueData
|
||||||
|
]
|
||||||
|
let updateStatus = SecItemUpdate(query as CFDictionary, updateQuery as CFDictionary)
|
||||||
|
if updateStatus == errSecSuccess {
|
||||||
|
return .complete()
|
||||||
|
} else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
} else if status == errSecItemNotFound {
|
||||||
|
let currentCount = countKeys(userId: userId, botId: botId)
|
||||||
|
if currentCount >= maxKeyCount {
|
||||||
|
return .fail(.quotaExceeded)
|
||||||
|
}
|
||||||
|
|
||||||
|
query[kSecValueData as String] = valueData
|
||||||
|
|
||||||
|
let createStatus = SecItemAdd(query as CFDictionary, nil)
|
||||||
|
if createStatus == errSecSuccess {
|
||||||
|
return .complete()
|
||||||
|
} else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getValue(userId: EnginePeer.Id, botId: EnginePeer.Id, key: String) -> Signal<String?, WebAppSecureStorage.Error> {
|
||||||
|
var query = makeQuery(userId: userId, botId: botId, key: key)
|
||||||
|
query[kSecReturnData as String] = true
|
||||||
|
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||||
|
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
|
||||||
|
if status == errSecSuccess, let data = result as? Data, let value = String(data: data, encoding: .utf8) {
|
||||||
|
return .single(value)
|
||||||
|
} else if status == errSecItemNotFound {
|
||||||
|
return .single(nil)
|
||||||
|
} else {
|
||||||
|
return .fail(.unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func clearStorage(userId: EnginePeer.Id, botId: EnginePeer.Id) -> Signal<Never, WebAppSecureStorage.Error> {
|
||||||
|
let serviceQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: "TMASecureStorage",
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitAll,
|
||||||
|
kSecReturnAttributes as String: true
|
||||||
|
]
|
||||||
|
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(serviceQuery as CFDictionary, &result)
|
||||||
|
|
||||||
|
if status == errSecSuccess, let items = result as? [[String: Any]] {
|
||||||
|
let relevantPrefix = self.keyPrefix(userId: userId, botId: botId)
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if let account = item[kSecAttrAccount as String] as? String, account.hasPrefix(relevantPrefix) {
|
||||||
|
let deleteQuery: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecAttrService as String: "TMASecureStorage"
|
||||||
|
]
|
||||||
|
|
||||||
|
SecItemDelete(deleteQuery as CFDictionary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return .complete()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user