Wallet improvements

This commit is contained in:
Ilya Laktyushin 2019-09-27 08:54:38 +03:00
parent 4d5f28aa4c
commit 5bb1ba67b7
52 changed files with 1248 additions and 400 deletions

View File

@ -44,6 +44,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -44,6 +44,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -44,6 +44,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -44,6 +44,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -44,6 +44,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -2549,6 +2549,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -2570,6 +2572,8 @@
ReferencedContainer = "container:Project.xcodeproj"> ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference> </BuildableReference>
</BuildableProductRunnable> </BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -167,6 +167,7 @@ public enum ResolvedUrl {
case share(url: String?, text: String?, to: String?) case share(url: String?, text: String?, to: String?)
case wallpaper(WallpaperUrlParameter) case wallpaper(WallpaperUrlParameter)
case theme(String) case theme(String)
case wallet(address: String, amount: Int64?, comment: String?)
} }
public enum NavigateToChatKeepStack { public enum NavigateToChatKeepStack {
@ -368,6 +369,11 @@ public final class ContactSelectionControllerParams {
} }
} }
public enum OpenWalletContext {
case generic
case send(address: String, amount: Int64?, comment: String?)
}
public let defaultContactLabel: String = "_$!<Mobile>!$_" public let defaultContactLabel: String = "_$!<Mobile>!$_"
public enum CreateGroupMode { public enum CreateGroupMode {
@ -434,6 +440,7 @@ public protocol SharedAccountContext: class {
func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void) func openAddContact(context: AccountContext, firstName: String, lastName: String, phoneNumber: String, label: String, present: @escaping (ViewController, Any?) -> Void, pushController: @escaping (ViewController) -> Void, completed: @escaping () -> Void)
func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void) func openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void) func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
func openWallet(context: AccountContext, walletContext: OpenWalletContext, present: @escaping (ViewController) -> Void)
func navigateToCurrentCall() func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get } var hasOngoingCall: ValuePromise<Bool> { get }

View File

@ -60,11 +60,13 @@ private final class CameraContext {
self.session.startRunning() self.session.startRunning()
} }
func stopCapture() { func stopCapture(invalidate: Bool = false) {
self.session.beginConfiguration() if invalidate {
self.input.invalidate(for: self.session) self.session.beginConfiguration()
self.output.invalidate(for: self.session) self.input.invalidate(for: self.session)
self.session.commitConfiguration() self.output.invalidate(for: self.session)
self.session.commitConfiguration()
}
self.session.stopRunning() self.session.stopRunning()
} }
@ -143,10 +145,10 @@ public final class Camera {
} }
} }
public func stopCapture() { public func stopCapture(invalidate: Bool = false) {
self.queue.async { self.queue.async {
if let context = self.contextRef?.takeUnretainedValue() { if let context = self.contextRef?.takeUnretainedValue() {
context.stopCapture() context.stopCapture(invalidate: invalidate)
} }
} }
} }

View File

@ -813,9 +813,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in
(self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value
}, completion: { }, completion: {
completedEffect = true completedEffect = true
intermediateCompletion() intermediateCompletion()
}) })
} }
self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false)

View File

@ -524,8 +524,8 @@ open class ItemListController<Entry: ItemListNodeEntry>: ViewController, KeyShor
} }
} }
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode) { public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true) {
(self.displayNode as! ItemListControllerNode<Entry>).listNode.ensureItemNodeVisible(itemNode) (self.displayNode as! ItemListControllerNode<Entry>).listNode.ensureItemNodeVisible(itemNode, animated: animated)
} }
public func afterLayout(_ f: @escaping () -> Void) { public func afterLayout(_ f: @escaping () -> Void) {

View File

@ -37,12 +37,14 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
let action: (() -> Void)? let action: (() -> Void)?
let textUpdated: (String) -> Void let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> Void)?
let updatedFocus: ((Bool) -> Void)?
let maxLength: ItemListMultilineInputItemTextLimit? let maxLength: ItemListMultilineInputItemTextLimit?
let minimalHeight: CGFloat? let minimalHeight: CGFloat?
let inlineAction: ItemListMultilineInputInlineAction? let inlineAction: ItemListMultilineInputInlineAction?
public let tag: ItemListItemTag? public let tag: ItemListItemTag?
public init(theme: PresentationTheme, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) { public init(theme: PresentationTheme, text: String, placeholder: String, maxLength: ItemListMultilineInputItemTextLimit?, sectionId: ItemListSectionId, style: ItemListStyle, capitalization: Bool = true, autocorrection: Bool = true, returnKeyType: UIReturnKeyType = .default, minimalHeight: CGFloat? = nil, textUpdated: @escaping (String) -> Void, shouldUpdateText: @escaping (String) -> Bool = { _ in return true }, processPaste: ((String) -> Void)? = nil, updatedFocus: ((Bool) -> Void)? = nil, tag: ItemListItemTag? = nil, action: (() -> Void)? = nil, inlineAction: ItemListMultilineInputInlineAction? = nil) {
self.theme = theme self.theme = theme
self.text = text self.text = text
self.placeholder = placeholder self.placeholder = placeholder
@ -55,6 +57,8 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
self.minimalHeight = minimalHeight self.minimalHeight = minimalHeight
self.textUpdated = textUpdated self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.tag = tag self.tag = tag
self.action = action self.action = action
self.inlineAction = inlineAction self.inlineAction = inlineAction
@ -365,15 +369,27 @@ public class ItemListMultilineInputItemNode: ListViewItemNode, ASEditableTextNod
self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset))) self.textClippingNode.frame = CGRect(origin: CGPoint(x: leftInset, y: textTopInset), size: CGSize(width: max(0.0, params.width - leftInset - params.rightInset), height: max(0.0, contentSize.height - textTopInset - textBottomInset)))
} }
public func editableTextNodeDidBeginEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(true)
}
public func editableTextNodeDidFinishEditing(_ editableTextNode: ASEditableTextNode) {
self.item?.updatedFocus?(false)
}
public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { public func editableTextNode(_ editableTextNode: ASEditableTextNode, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
if let item = self.item { if let item = self.item {
if text.count > 1, let processPaste = item.processPaste {
processPaste(text)
return false
}
if let action = item.action, text == "\n" { if let action = item.action, text == "\n" {
action() action()
return false return false
} }
var newText: String = editableTextNode.textView.text let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
newText.replaceSubrange(newText.index(newText.startIndex, offsetBy: range.lowerBound) ..< newText.index(newText.startIndex, offsetBy: range.upperBound), with: text)
if !item.shouldUpdateText(newText) { if !item.shouldUpdateText(newText) {
return false return false
} }

View File

@ -400,8 +400,7 @@ public class ItemListSingleLineInputItemNode: ListViewItemNode, UITextFieldDeleg
@objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { @objc public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let item = self.item { if let item = self.item {
var newText = textField.text ?? "" let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
newText.replaceSubrange(newText.index(newText.startIndex, offsetBy: range.lowerBound) ..< newText.index(newText.startIndex, offsetBy: range.upperBound), with: string)
if !item.shouldUpdateText(newText) { if !item.shouldUpdateText(newText) {
return false return false
} }

View File

@ -66,7 +66,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
_backgroundView.image = backgroundImage; _backgroundView.image = backgroundImage;
[_wrapperView addSubview:_backgroundView]; [_wrapperView addSubview:_backgroundView];
_countLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, -0.5f, frame.size.width, frame.size.height)]; _countLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, -0.5f, frame.size.width + 1.0, frame.size.height)];
_countLabel.backgroundColor = [UIColor clearColor]; _countLabel.backgroundColor = [UIColor clearColor];
_countLabel.font = [TGFont roundedFontOfSize:17]; _countLabel.font = [TGFont roundedFontOfSize:17];
_countLabel.text = [TGStringUtils stringWithLocalizedNumber:0]; _countLabel.text = [TGStringUtils stringWithLocalizedNumber:0];
@ -292,7 +292,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
if (sizeToFit) if (sizeToFit)
[_countLabel sizeToFit]; [_countLabel sizeToFit];
CGFloat labelWidth = CGRound(_countLabel.frame.size.width); CGFloat labelWidth = ceilf(_countLabel.frame.size.width);
CGFloat labelOrigin = 0.0f; CGFloat labelOrigin = 0.0f;
if (![self _useRtlLayout]) if (![self _useRtlLayout])

View File

@ -10,7 +10,7 @@ public enum QrCodeIcon {
case custom(UIImage?) case custom(UIImage?)
} }
public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = nil, icon: QrCodeIcon, ecl: String = "M", scale: CGFloat = 0.0) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = nil, icon: QrCodeIcon, ecl: String = "M") -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return Signal<CIImage, NoError> { subscriber in return Signal<CIImage, NoError> { subscriber in
if let data = string.data(using: .isoLatin1, allowLossyConversion: false), let filter = CIFilter(name: "CIQRCodeGenerator") { if let data = string.data(using: .isoLatin1, allowLossyConversion: false), let filter = CIFilter(name: "CIQRCodeGenerator") {
filter.setValue(data, forKey: "inputMessage") filter.setValue(data, forKey: "inputMessage")
@ -25,7 +25,7 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n
} }
|> map { inputImage in |> map { inputImage in
return { arguments in return { arguments in
let context = DrawingContext(size: arguments.drawingSize, scale: scale, clear: true) let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true)
let drawingRect = arguments.drawingRect let drawingRect = arguments.drawingRect
let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize) let fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize)

View File

@ -29,6 +29,8 @@
shouldUseLaunchSchemeArgsEnv = "YES"> shouldUseLaunchSchemeArgsEnv = "YES">
<Testables> <Testables>
</Testables> </Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@ -40,6 +42,17 @@
debugDocumentVersioning = "YES" debugDocumentVersioning = "YES"
debugServiceExtension = "internal" debugServiceExtension = "internal"
allowLocationSimulation = "YES"> allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "E66DC04E89A74F8D00000000"
BuildableName = "libSwiftSignalKit.dylib"
BlueprintName = "SwiftSignalKit#shared"
ReferencedContainer = "container:SwiftSignalKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction> </LaunchAction>
<ProfileAction <ProfileAction
buildConfiguration = "Release" buildConfiguration = "Release"

View File

@ -38,10 +38,10 @@ public final class ShareProxyServerActionSheetController: ActionSheetController
})) }))
items.append(ActionSheetButtonItem(title: strings.SocksProxySetup_ShareQRCode, action: { [weak self] in items.append(ActionSheetButtonItem(title: strings.SocksProxySetup_ShareQRCode, action: { [weak self] in
self?.dismissAnimated() self?.dismissAnimated()
let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy, scale: 1.0) let _ = (qrCode(string: link, color: .black, backgroundColor: .white, icon: .proxy)
|> map { generator -> UIImage? in |> map { generator -> UIImage? in
let imageSize = CGSize(width: 512.0, height: 512.0) let imageSize = CGSize(width: 512.0, height: 512.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets())) let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage() return context?.generateImage()
} }
|> deliverOnMainQueue).start(next: { image in |> deliverOnMainQueue).start(next: { image in

View File

@ -12,6 +12,7 @@ import AccountContext
import ShareController import ShareController
import SearchBarNode import SearchBarNode
import SearchUI import SearchUI
import ActivityIndicator
private enum LanguageListSection: ItemListSectionId { private enum LanguageListSection: ItemListSectionId {
case official case official
@ -194,12 +195,12 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController
if self.hasValidLayout { if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty { while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition() self.dequeueTransitions()
} }
} }
} }
private func dequeueTransition() { private func dequeueTransitions() {
if let transition = self.enqueuedTransitions.first { if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0) self.enqueuedTransitions.remove(at: 0)
@ -248,7 +249,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController
if !self.hasValidLayout { if !self.hasValidLayout {
self.hasValidLayout = true self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty { while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition() self.dequeueTransitions()
} }
} }
} }
@ -265,17 +266,18 @@ private struct LanguageListNodeTransition {
let insertions: [ListViewInsertItem] let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem] let updates: [ListViewUpdateItem]
let firstTime: Bool let firstTime: Bool
let isLoading: Bool
let animated: Bool let animated: Bool
} }
private func preparedLanguageListNodeTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, forceUpdate: Bool, animated: Bool) -> LanguageListNodeTransition { private func preparedLanguageListNodeTransition(theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [LanguageListEntry], to toEntries: [LanguageListEntry], openSearch: @escaping () -> Void, selectLocalization: @escaping (LocalizationInfo) -> Void, setItemWithRevealedOptions: @escaping (String?, String?) -> Void, removeItem: @escaping (String) -> Void, firstTime: Bool, isLoading: Bool, forceUpdate: Bool, animated: Bool) -> LanguageListNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate) let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries, allUpdated: forceUpdate)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(theme: theme, strings: strings, searchMode: false, openSearch: openSearch, selectLocalization: selectLocalization, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem), directionHint: nil) }
return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, animated: animated) return LanguageListNodeTransition(deletions: deletions, insertions: insertions, updates: updates, firstTime: firstTime, isLoading: isLoading, animated: animated)
} }
final class LocalizationListControllerNode: ViewControllerTracingNode { final class LocalizationListControllerNode: ViewControllerTracingNode {
@ -292,7 +294,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
private var containerLayout: (ContainerViewLayout, CGFloat)? private var containerLayout: (ContainerViewLayout, CGFloat)?
let listNode: ListView let listNode: ListView
private var queuedTransitions: [LanguageListNodeTransition] = [] private var queuedTransitions: [LanguageListNodeTransition] = []
private var activityIndicator: ActivityIndicator?
private var searchDisplayController: SearchDisplayController? private var searchDisplayController: SearchDisplayController?
private let presentationDataValue = Promise<(PresentationTheme, PresentationStrings)>() private let presentationDataValue = Promise<(PresentationTheme, PresentationStrings)>()
@ -408,7 +410,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
} }
} }
let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.0, presentationData.1)) let previousEntriesAndPresentationData = previousEntriesHolder.swap((entries, presentationData.0, presentationData.1))
let transition = preparedLanguageListNodeTransition(theme: presentationData.0, strings: presentationData.1, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.0 || previousEntriesAndPresentationData?.2 !== presentationData.1, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count) let transition = preparedLanguageListNodeTransition(theme: presentationData.0, strings: presentationData.1, from: previousEntriesAndPresentationData?.0 ?? [], to: entries, openSearch: openSearch, selectLocalization: { [weak self] info in self?.selectLocalization(info) }, setItemWithRevealedOptions: setItemWithRevealedOptions, removeItem: removeItem, firstTime: previousEntriesAndPresentationData == nil, isLoading: entries.isEmpty, forceUpdate: previousEntriesAndPresentationData?.1 !== presentationData.0 || previousEntriesAndPresentationData?.2 !== presentationData.1, animated: (previousEntriesAndPresentationData?.0.count ?? 0) >= entries.count)
strongSelf.enqueueTransition(transition) strongSelf.enqueueTransition(transition)
}) })
self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start() self.updatedDisposable = synchronizedLocalizationListState(postbox: context.account.postbox, network: context.account.network).start()
@ -469,6 +471,11 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
if let activityIndicator = self.activityIndicator {
let indicatorSize = activityIndicator.measure(CGSize(width: 100.0, height: 100.0))
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - indicatorSize.width) / 2.0), y: updateSizeAndInsets.insets.top + 50.0 + floor((layout.size.height - updateSizeAndInsets.insets.top - updateSizeAndInsets.insets.bottom - indicatorSize.height - 50.0) / 2.0)), size: indicatorSize))
}
if !hadValidLayout { if !hadValidLayout {
self.dequeueTransitions() self.dequeueTransitions()
} }
@ -483,26 +490,38 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
} }
private func dequeueTransitions() { private func dequeueTransitions() {
if self.containerLayout != nil { guard let (layout, navigationBarHeight) = self.containerLayout else {
while !self.queuedTransitions.isEmpty { return
let transition = self.queuedTransitions.removeFirst() }
while !self.queuedTransitions.isEmpty {
var options = ListViewDeleteAndInsertOptions() let transition = self.queuedTransitions.removeFirst()
if transition.firstTime {
options.insert(.Synchronous) var options = ListViewDeleteAndInsertOptions()
options.insert(.LowLatency) if transition.firstTime {
} else if transition.animated { options.insert(.Synchronous)
options.insert(.AnimateInsertion) options.insert(.LowLatency)
} } else if transition.animated {
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in options.insert(.AnimateInsertion)
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
}
})
} }
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(true)
}
if transition.isLoading, strongSelf.activityIndicator == nil {
let activityIndicator = ActivityIndicator(type: .custom(strongSelf.presentationData.theme.list.itemAccentColor, 22.0, 1.0, false))
strongSelf.activityIndicator = activityIndicator
strongSelf.insertSubnode(activityIndicator, aboveSubnode: strongSelf.listNode)
strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate)
} else if !transition.isLoading, let activityIndicator = strongSelf.activityIndicator {
strongSelf.activityIndicator = nil
activityIndicator.removeFromSupernode()
}
}
})
} }
} }

View File

@ -51,6 +51,8 @@ extension SettingsSearchableItemIcon {
return PresentationResourcesSettings.watch return PresentationResourcesSettings.watch
case .passport: case .passport:
return PresentationResourcesSettings.passport return PresentationResourcesSettings.passport
case .wallet:
return PresentationResourcesSettings.wallet
case .support: case .support:
return PresentationResourcesSettings.support return PresentationResourcesSettings.support
case .faq: case .faq:

View File

@ -27,6 +27,7 @@ enum SettingsSearchableItemIcon {
case appearance case appearance
case language case language
case watch case watch
case wallet
case passport case passport
case support case support
case faq case faq
@ -858,9 +859,9 @@ func settingsSearchableItems(context: AccountContext, notificationExceptionsList
}) })
allItems.append(passport) allItems.append(passport)
if true || hasWallet { if hasWallet {
let wallet = SettingsSearchableItem(id: .wallet(0), title: "Wallet", alternate: synonyms("Wallet"), icon: .passport, breadcrumbs: [], present: { context, _, present in let wallet = SettingsSearchableItem(id: .wallet(0), title: "Gram Wallet", alternate: synonyms(""), icon: .wallet, breadcrumbs: [], present: { context, _, present in
openWallet(context: context, push: { c in context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in
present(.push, c) present(.push, c)
}) })
}) })

View File

@ -574,12 +574,12 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
let languageName = presentationData.strings.primaryComponent.localizedName let languageName = presentationData.strings.primaryComponent.localizedName
entries.append(.language(presentationData.theme, PresentationResourcesSettings.language, presentationData.strings.Settings_AppLanguage, languageName.isEmpty ? presentationData.strings.Localization_LanguageName : languageName)) entries.append(.language(presentationData.theme, PresentationResourcesSettings.language, presentationData.strings.Settings_AppLanguage, languageName.isEmpty ? presentationData.strings.Localization_LanguageName : languageName))
if hasWallet || experimentalUISettings.wallets {
entries.append(.wallet(presentationData.theme, PresentationResourcesSettings.wallet, "Gram Wallet", ""))
}
if hasPassport { if hasPassport {
entries.append(.passport(presentationData.theme, PresentationResourcesSettings.passport, presentationData.strings.Settings_Passport, "")) entries.append(.passport(presentationData.theme, PresentationResourcesSettings.passport, presentationData.strings.Settings_Passport, ""))
} }
if hasWallet || experimentalUISettings.wallets {
entries.append(.wallet(presentationData.theme, PresentationResourcesSettings.passport, "Wallet", ""))
}
if hasWatchApp { if hasWatchApp {
entries.append(.watch(presentationData.theme, PresentationResourcesSettings.watch, presentationData.strings.Settings_AppleWatch, "")) entries.append(.watch(presentationData.theme, PresentationResourcesSettings.watch, presentationData.strings.Settings_AppleWatch, ""))
@ -850,7 +850,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let _ = (contextValue.get() let _ = (contextValue.get()
|> deliverOnMainQueue |> deliverOnMainQueue
|> take(1)).start(next: { context in |> take(1)).start(next: { context in
openWallet(context: context, push: { c in context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in
pushControllerImpl?(c) pushControllerImpl?(c)
}) })
}) })
@ -1098,8 +1098,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
) )
) )
let hasWallet = .single(false) let hasWallet = contextValue.get()
|> then(contextValue.get()
|> mapToSignal { context in |> mapToSignal { context in
return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration]) return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> Bool in |> map { view -> Bool in
@ -1107,7 +1106,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let configuration = WalletConfiguration.with(appConfiguration: appConfiguration) let configuration = WalletConfiguration.with(appConfiguration: appConfiguration)
return configuration.enabled return configuration.enabled
} }
}) }
let hasPassport = ValuePromise<Bool>(false) let hasPassport = ValuePromise<Bool>(false)
let updatePassport: () -> Void = { let updatePassport: () -> Void = {
@ -1618,36 +1617,3 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
return items return items
} }
} }
func openWallet(context: AccountContext, push: @escaping (ViewController) -> Void) {
guard let tonContext = context.tonContext else {
return
}
let _ = (combineLatest(queue: .mainQueue(),
availableWallets(postbox: context.account.postbox),
tonContext.keychain.encryptionPublicKey()
)
|> deliverOnMainQueue).start(next: { wallets, currentPublicKey in
if wallets.wallets.isEmpty {
if let _ = currentPublicKey {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
} else {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageNotAvailable))
}
} else {
let walletInfo = wallets.wallets[0].info
if let currentPublicKey = currentPublicKey {
if currentPublicKey == walletInfo.encryptedSecret.publicKey {
let _ = (walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
push(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
})
} else {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.changed)))
}
} else {
push(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.notAvailable)))
}
}
})
}

View File

@ -384,7 +384,7 @@ public class WallpaperGalleryController: ViewController {
let wallpaper = wallpaper.withUpdatedSettings(updatedSettings) let wallpaper = wallpaper.withUpdatedSettings(updatedSettings)
let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in let _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
themeSpecificChatWallpapers[current.theme.index] = wallpaper themeSpecificChatWallpapers[current.theme.index] = wallpaper
return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations) return PresentationThemeSettings(chatWallpaper: wallpaper, theme: current.theme, themeSpecificAccentColors: current.themeSpecificAccentColors, themeSpecificChatWallpapers: themeSpecificChatWallpapers, fontSize: current.fontSize, automaticThemeSwitchSetting: current.automaticThemeSwitchSetting, largeEmoji: current.largeEmoji, disableAnimations: current.disableAnimations)
}) |> deliverOnMainQueue).start(completed: { }) |> deliverOnMainQueue).start(completed: {

View File

@ -15,7 +15,8 @@ public struct PresentationResourcesSettings {
public static let dataAndStorage = UIImage(bundleImageName: "Settings/MenuIcons/DataAndStorage")?.precomposed() public static let dataAndStorage = UIImage(bundleImageName: "Settings/MenuIcons/DataAndStorage")?.precomposed()
public static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed() public static let appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.precomposed()
public static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed() public static let language = UIImage(bundleImageName: "Settings/MenuIcons/Language")?.precomposed()
public static let wallet = UIImage(bundleImageName: "Settings/MenuIcons/Wallet")?.precomposed()
public static let passport = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed() public static let passport = UIImage(bundleImageName: "Settings/MenuIcons/Passport")?.precomposed()
public static let watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed() public static let watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed()

View File

@ -20,6 +20,7 @@ public func normalizeArabicNumeralString(_ string: String, type: ArabicNumeralSt
("7", "٧", "۷"), ("7", "٧", "۷"),
("8", "٨", "۸"), ("8", "٨", "۸"),
("9", "٩", "۹"), ("9", "٩", "۹"),
(",", "٫", "٫")
] ]
for (western, arabic, persian) in numerals { for (western, arabic, persian) in numerals {
switch type { switch type {

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "ic_ton@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "ic_ton@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "ic_gallery (3).pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

View File

@ -0,0 +1,22 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "QrGem@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "QrGem@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -24,6 +24,8 @@ import WatchBridge
import LegacyDataImport import LegacyDataImport
import SettingsUI import SettingsUI
import AppBundle import AppBundle
import WalletUI
import UrlHandling
private let handleVoipNotifications = false private let handleVoipNotifications = false
@ -1479,6 +1481,9 @@ final class SharedApplicationContext {
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {}) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {})
} else if let confirmationCode = parseConfirmationCodeUrl(url) { } else if let confirmationCode = parseConfirmationCodeUrl(url) {
authContext.rootController.applyConfirmationCode(confirmationCode) authContext.rootController.applyConfirmationCode(confirmationCode)
} else if let _ = parseWalletUrl(url) {
let presentationData = authContext.sharedContext.currentPresentationData.with { $0 }
authContext.rootController.currentWindow?.present(standardTextAlertController(theme: AlertControllerTheme(presentationTheme: presentationData.theme), title: nil, text: "Please log in to your account to use Gram Wallet.", actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), on: .root, blockInteraction: false, completion: {})
} }
} }
}) })

View File

@ -46,6 +46,7 @@ import UrlHandling
import ReactionSelectionNode import ReactionSelectionNode
import MessageReactionListUI import MessageReactionListUI
import AppBundle import AppBundle
import WalletUI
public enum ChatControllerPeekActions { public enum ChatControllerPeekActions {
case standard case standard
@ -5078,8 +5079,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
}, recognizedQRCode: { [weak self] code in }, recognizedQRCode: { [weak self] code in
if let strongSelf = self, let (host, port, username, password, secret) = parseProxyUrl(code) { if let strongSelf = self {
strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret)) if let (host, port, username, password, secret) = parseProxyUrl(code) {
strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret))
} else if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) {
strongSelf.openResolved(ResolvedUrl.wallet(address: parsedWalletUrl.address, amount: parsedWalletUrl.amount, comment: parsedWalletUrl.comment))
}
} }
}, presentSchedulePicker: { [weak self] done in }, presentSchedulePicker: { [weak self] done in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -822,6 +822,8 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
break break
case .theme: case .theme:
break break
case .wallet:
break
} }
} }
})) }))

View File

@ -324,5 +324,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
controller?.dismiss() controller?.dismiss()
})) }))
dismissInput() dismissInput()
case let .wallet(address, amount, comment):
context.sharedContext.openWallet(context: context, walletContext: .send(address: address, amount: amount, comment: comment)) { c in
navigationController?.pushViewController(c)
}
} }
} }

View File

@ -14,6 +14,7 @@ import AccountContext
import UrlEscaping import UrlEscaping
import PassportUI import PassportUI
import UrlHandling import UrlHandling
import WalletUI
public struct ParsedSecureIdUrl { public struct ParsedSecureIdUrl {
public let peerId: PeerId public let peerId: PeerId
@ -140,6 +141,12 @@ func formattedConfirmationCode(_ code: Int) -> String {
func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) { func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, url: String, forceExternal: Bool, presentationData: PresentationData, navigationController: NavigationController?, dismissInput: @escaping () -> Void) {
if url.hasPrefix("ton://") { if url.hasPrefix("ton://") {
if let url = URL(string: url), let parsedUrl = parseWalletUrl(url) {
context.sharedContext.openWallet(context: context, walletContext: .send(address: parsedUrl.address, amount: parsedUrl.amount, comment: parsedUrl.comment)) { c in
navigationController?.pushViewController(c)
}
}
return return
} }

View File

@ -14,6 +14,7 @@ import PeersNearbyUI
import PeerInfoUI import PeerInfoUI
import SettingsUI import SettingsUI
import UrlHandling import UrlHandling
import WalletUI
private enum CallStatusText: Equatable { private enum CallStatusText: Equatable {
case none case none
@ -1016,6 +1017,45 @@ public final class SharedAccountContextImpl: SharedAccountContext {
public func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?) -> ListViewItem { public func makeChatMessagePreviewItem(context: AccountContext, message: Message, theme: PresentationTheme, strings: PresentationStrings, wallpaper: TelegramWallpaper, fontSize: PresentationFontSize, dateTimeFormat: PresentationDateTimeFormat, nameOrder: PresentationPersonNameOrder, forcedResourceStatus: FileMediaResourceStatus?) -> ListViewItem {
return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: .peer(message.id.peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, isScheduledMessages: false, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus), controllerInteraction: defaultChatControllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()), disableDate: true, additionalContent: nil) return ChatMessageItem(presentationData: ChatPresentationData(theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), fontSize: fontSize, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameOrder, disableAnimations: false, largeEmoji: false, animatedEmojiScale: 1.0, isPreview: true), context: context, chatLocation: .peer(message.id.peerId), associatedData: ChatMessageItemAssociatedData(automaticDownloadPeerType: .contact, automaticDownloadNetworkType: .cellular, isRecentActions: false, isScheduledMessages: false, contactsPeerIds: Set(), animatedEmojiStickers: [:], forcedResourceStatus: forcedResourceStatus), controllerInteraction: defaultChatControllerInteraction, content: .message(message: message, read: true, selection: .none, attributes: ChatMessageEntryAttributes()), disableDate: true, additionalContent: nil)
} }
public func openWallet(context: AccountContext, walletContext: OpenWalletContext, present: @escaping (ViewController) -> Void) {
guard let tonContext = context.tonContext else {
return
}
let _ = (combineLatest(queue: .mainQueue(),
availableWallets(postbox: context.account.postbox),
tonContext.keychain.encryptionPublicKey()
)
|> deliverOnMainQueue).start(next: { wallets, currentPublicKey in
if wallets.wallets.isEmpty {
if let _ = currentPublicKey {
present(WalletSplashScreen(context: context, tonContext: tonContext, mode: .intro))
} else {
present(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageNotAvailable))
}
} else {
let walletInfo = wallets.wallets[0].info
if let currentPublicKey = currentPublicKey {
if currentPublicKey == walletInfo.encryptedSecret.publicKey {
let _ = (walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|> deliverOnMainQueue).start(next: { address in
switch walletContext {
case .generic:
present(WalletInfoScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
case let .send(address, amount, comment):
present(walletSendScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address, amount: amount, comment: comment))
}
})
} else {
present(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.changed)))
}
} else {
present(WalletSplashScreen(context: context, tonContext: tonContext, mode: .secureStorageReset(.notAvailable)))
}
}
})
}
} }
private let defaultChatControllerInteraction = ChatControllerInteraction.default private let defaultChatControllerInteraction = ChatControllerInteraction.default

View File

@ -372,6 +372,11 @@ public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? {
} }
public func resolveUrlImpl(account: Account, url: String) -> Signal<ResolvedUrl, NoError> { public func resolveUrlImpl(account: Account, url: String) -> Signal<ResolvedUrl, NoError> {
if url.hasPrefix("ton://") {
if let url = URL(string: url), let parsedUrl = parseWalletUrl(url) {
return .single(.wallet(address: parsedUrl.address, amount: parsedUrl.amount, comment: parsedUrl.comment))
}
}
let schemes = ["http://", "https://", ""] let schemes = ["http://", "https://", ""]
let baseTelegramMePaths = ["telegram.me", "t.me"] let baseTelegramMePaths = ["telegram.me", "t.me"]
for basePath in baseTelegramMePaths { for basePath in baseTelegramMePaths {
@ -430,3 +435,42 @@ public func resolveInstantViewUrl(account: Account, url: String) -> Signal<Resol
} }
} }
} }
public struct ParsedWalletUrl {
public let address: String
public let amount: Int64?
public let comment: String?
}
private let invalidWalletAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
private func isValidWalletAddress(_ address: String) -> Bool {
if address.count != 48 || address.rangeOfCharacter(from: invalidWalletAddressCharacters) != nil {
return false
}
return true
}
public func parseWalletUrl(_ url: URL) -> ParsedWalletUrl? {
guard url.scheme == "ton" else {
return nil
}
var address: String?
if let host = url.host, isValidWalletAddress(host) {
address = host.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
}
var amount: Int64?
var comment: String?
if let query = url.query, let components = URLComponents(string: "/?" + query), let queryItems = components.queryItems {
for queryItem in queryItems {
if let value = queryItem.value {
if queryItem.name == "amount", !value.isEmpty, let amountValue = Int64(value) {
amount = amountValue
} else if queryItem.name == "text", !value.isEmpty {
comment = value
}
}
}
}
return address.flatMap { ParsedWalletUrl(address: $0, amount: amount, comment: comment) }
}

View File

@ -27,6 +27,7 @@ static_library(
"//submodules/MergeLists:MergeLists", "//submodules/MergeLists:MergeLists",
"//submodules/TelegramStringFormatting:TelegramStringFormatting", "//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/GlassButtonNode:GlassButtonNode", "//submodules/GlassButtonNode:GlassButtonNode",
"//submodules/UrlHandling:UrlHandling",
], ],
frameworks = [ frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework", "$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -115,13 +115,15 @@ final class WalletInfoEmptyItemNode: ListViewItemNode {
let title = "Wallet Created" let title = "Wallet Created"
let text = "Your wallet address" let text = "Your wallet address"
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.bold(32.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets())) let textColor = UIColor.black
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets())) let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: Font.bold(32.0), textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: text, font: Font.regular(16.0), textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
var addressString = item.address var addressString = item.address
addressString.insert("\n", at: addressString.index(addressString.startIndex, offsetBy: addressString.count / 2)) addressString.insert("\n", at: addressString.index(addressString.startIndex, offsetBy: addressString.count / 2))
let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: addressString, font: Font.monospace(16.0), textColor: item.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets())) let (addressLayout, addressApply) = makeAddressLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: addressString, font: Font.monospace(16.0), textColor: textColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - sideInset * 2.0, height: .greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.1, cutout: nil, insets: UIEdgeInsets()))
let contentVerticalOrigin: CGFloat = 32.0 let contentVerticalOrigin: CGFloat = 32.0

View File

@ -513,7 +513,7 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.headerNode = WalletInfoHeaderNode(account: account, theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction) self.headerNode = WalletInfoHeaderNode(account: account, theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
self.listNode = ListView() self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.listNode.verticalScrollIndicatorColor = UIColor(white: 0.0, alpha: 0.3)
self.listNode.verticalScrollIndicatorFollowsOverscroll = true self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.listNode.isHidden = true self.listNode.isHidden = true
@ -869,26 +869,3 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
} }
} }
} }
func formatBalanceText(_ value: Int64, decimalSeparator: String) -> String {
var balanceText = "\(abs(value))"
while balanceText.count < 10 {
balanceText.insert("0", at: balanceText.startIndex)
}
balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9))
while true {
if balanceText.hasSuffix("0") {
if balanceText.hasSuffix("\(decimalSeparator)0") {
break
} else {
balanceText.removeLast()
}
} else {
break
}
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
}
return balanceText
}

View File

@ -86,12 +86,6 @@ private let descriptionFont = Font.regular(15.0)
private let dateFont = Font.regular(14.0) private let dateFont = Font.regular(14.0)
private let directionFont = Font.regular(15.0) private let directionFont = Font.regular(15.0)
private func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
class WalletInfoTransactionItemNode: ListViewItemNode { class WalletInfoTransactionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode private let topStripeNode: ASDisplayNode

View File

@ -12,12 +12,17 @@ class WalletQrCodeItem: ListViewItem, ItemListItem {
let address: String let address: String
let sectionId: ItemListSectionId let sectionId: ItemListSectionId
let style: ItemListStyle let style: ItemListStyle
let action: (() -> Void)?
let longTapAction: (() -> Void)?
public let isAlwaysPlain: Bool = true
init(theme: PresentationTheme, address: String, sectionId: ItemListSectionId, style: ItemListStyle) { init(theme: PresentationTheme, address: String, sectionId: ItemListSectionId, style: ItemListStyle, action: @escaping () -> Void, longTapAction: @escaping () -> Void) {
self.theme = theme self.theme = theme
self.address = address self.address = address
self.sectionId = sectionId self.sectionId = sectionId
self.style = style self.style = style
self.action = action
self.longTapAction = longTapAction
} }
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) { func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
@ -55,10 +60,6 @@ class WalletQrCodeItem: ListViewItem, ItemListItem {
} }
class WalletQrCodeItemNode: ListViewItemNode { class WalletQrCodeItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let imageNode: TransformImageNode private let imageNode: TransformImageNode
private var item: WalletQrCodeItem? private var item: WalletQrCodeItem?
@ -68,16 +69,6 @@ class WalletQrCodeItemNode: ListViewItemNode {
} }
init() { init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.imageNode = TransformImageNode() self.imageNode = TransformImageNode()
super.init(layerBacked: false, dynamicBounce: false) super.init(layerBacked: false, dynamicBounce: false)
@ -85,6 +76,37 @@ class WalletQrCodeItemNode: ListViewItemNode {
self.addSubnode(self.imageNode) self.addSubnode(self.imageNode)
} }
override func didLoad() {
super.didLoad()
let recognizer = TapLongTapOrDoubleTapGestureRecognizer(target: self, action: #selector(self.tapLongTapOrDoubleTapGesture(_:)))
recognizer.tapActionAtPoint = { [weak self] point in
return .waitForSingleTap
}
recognizer.highlight = { [weak self] point in
self?.imageNode.alpha = point != nil ? 0.4 : 1.0
}
self.view.addGestureRecognizer(recognizer)
}
@objc func tapLongTapOrDoubleTapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
switch recognizer.state {
case .ended:
if let (gesture, location) = recognizer.lastRecognizedGestureAndLocation {
switch gesture {
case .tap:
self.item?.action?()
case .longTap:
self.item?.longTapAction?()
default:
break
}
}
default:
break
}
}
func asyncLayout() -> (_ item: WalletQrCodeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { func asyncLayout() -> (_ item: WalletQrCodeItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let makeImageLayout = self.imageNode.asyncLayout() let makeImageLayout = self.imageNode.asyncLayout()
@ -98,7 +120,7 @@ class WalletQrCodeItemNode: ListViewItemNode {
updatedTheme = item.theme updatedTheme = item.theme
} }
if currentItem?.address != item.address { if currentItem?.address != item.address || updatedTheme != nil {
updatedAddress = item.address updatedAddress = item.address
} }
@ -106,22 +128,16 @@ class WalletQrCodeItemNode: ListViewItemNode {
let insets: UIEdgeInsets let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel let separatorHeight = UIScreenPixel
let inset: CGFloat = 12.0 let inset: CGFloat = 0.0
var imageSize = CGSize(width: 256.0, height: 256.0) var imageSize = CGSize(width: 128.0, height: 128.0)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil)) let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
let itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style { switch item.style {
case .plain: case .plain:
itemBackgroundColor = item.theme.list.plainBackgroundColor contentSize = CGSize(width: params.width, height: imageSize.height + 30.0)
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: imageSize.height + inset * 2.0)
insets = itemListNeighborsPlainInsets(neighbors) insets = itemListNeighborsPlainInsets(neighbors)
case .blocks: case .blocks:
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor contentSize = CGSize(width: params.width, height: imageSize.height + 30.0)
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: imageSize.height + inset * 2.0)
insets = itemListNeighborsGroupedInsets(neighbors) insets = itemListNeighborsGroupedInsets(neighbors)
} }
@ -131,69 +147,13 @@ class WalletQrCodeItemNode: ListViewItemNode {
if let strongSelf = self { if let strongSelf = self {
strongSelf.item = item strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
if let updatedAddress = updatedAddress { if let updatedAddress = updatedAddress {
strongSelf.imageNode.setSignal(qrCode(string: updatedAddress, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Settings/Wallet/IntroIcon")), ecl: "Q"), attemptSynchronously: true) strongSelf.imageNode.setSignal(qrCode(string: updatedAddress, color: item.theme.list.itemPrimaryTextColor.withAlphaComponent(0.77), backgroundColor: item.theme.list.blocksBackgroundColor, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")), ecl: "Q"), attemptSynchronously: true)
} }
let _ = imageApply() let _ = imageApply()
let leftInset: CGFloat
switch item.style {
case .plain:
leftInset = 35.0 + params.leftInset
if strongSelf.backgroundNode.supernode != nil { strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: (params.width - imageSize.width) / 2.0, y: 0.0), size: imageSize)
strongSelf.backgroundNode.removeFromSupernode()
}
if strongSelf.topStripeNode.supernode != nil {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
case .blocks:
leftInset = 16.0 + params.leftInset
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
strongSelf.topStripeNode.isHidden = false
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 16.0 + params.leftInset
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
}
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: (params.width - imageSize.width) / 2.0, y: 12.0), size: imageSize)
} }
}) })
} }

View File

@ -9,6 +9,7 @@ import SwiftSignalKit
import TelegramCore import TelegramCore
import Camera import Camera
import GlassButtonNode import GlassButtonNode
import UrlHandling
private func generateFrameImage() -> UIImage? { private func generateFrameImage() -> UIImage? {
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
@ -47,12 +48,13 @@ private func generateFrameImage() -> UIImage? {
public final class WalletQrScanScreen: ViewController { public final class WalletQrScanScreen: ViewController {
private let context: AccountContext private let context: AccountContext
private let completion: (String, Int64?, String?) -> Void private let completion: (ParsedWalletUrl) -> Void
private var presentationData: PresentationData private var presentationData: PresentationData
private var disposable: Disposable? private var codeDisposable: Disposable?
private var inForegroundDisposable: Disposable?
public init(context: AccountContext, completion: @escaping (String, Int64?, String?) -> Void) { public init(context: AccountContext, completion: @escaping (ParsedWalletUrl) -> Void) {
self.context = context self.context = context
self.completion = completion self.completion = completion
@ -70,6 +72,14 @@ public final class WalletQrScanScreen: ViewController {
self.navigationBar?.intrinsicCanTransitionInline = false self.navigationBar?.intrinsicCanTransitionInline = false
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil) self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.inForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> deliverOnMainQueue).start(next: { [weak self] inForeground in
guard let strongSelf = self else {
return
}
(strongSelf.displayNode as! WalletQrScanScreenNode).updateInForeground(inForeground)
})
} }
required init(coder aDecoder: NSCoder) { required init(coder aDecoder: NSCoder) {
@ -77,7 +87,8 @@ public final class WalletQrScanScreen: ViewController {
} }
deinit { deinit {
self.disposable?.dispose() self.codeDisposable?.dispose()
self.inForegroundDisposable?.dispose()
} }
@objc private func backPressed() { @objc private func backPressed() {
@ -89,12 +100,7 @@ public final class WalletQrScanScreen: ViewController {
self.displayNodeDidLoad() self.displayNodeDidLoad()
// (self.displayNode as! WalletQrScanScreenNode).focusedCode.get() self.codeDisposable = (((self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
// |> map { code -> String? in
// return code?.message
// } |> distinctUntilChanged
self.disposable = (((self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
|> map { code -> String? in |> map { code -> String? in
return code?.message return code?.message
} }
@ -106,8 +112,9 @@ public final class WalletQrScanScreen: ViewController {
guard let strongSelf = self, let code = code else { guard let strongSelf = self, let code = code else {
return return
} }
let cleanString = code.replacingOccurrences(of: "ton://", with: "") if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) {
strongSelf.completion(cleanString, nil, nil) strongSelf.completion(parsedWalletUrl)
}
}) })
} }
@ -128,6 +135,7 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
private let leftDimNode: ASDisplayNode private let leftDimNode: ASDisplayNode
private let rightDimNode: ASDisplayNode private let rightDimNode: ASDisplayNode
private let frameNode: ASImageNode private let frameNode: ASImageNode
private let galleryButtonNode: GlassButtonNode
private let torchButtonNode: GlassButtonNode private let torchButtonNode: GlassButtonNode
private let titleNode: ImmediateTextNode private let titleNode: ImmediateTextNode
@ -168,6 +176,7 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
self.frameNode = ASImageNode() self.frameNode = ASImageNode()
self.frameNode.image = generateFrameImage() self.frameNode.image = generateFrameImage()
self.galleryButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraGalleryIcon")!, label: nil)
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil) self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
self.titleNode = ImmediateTextNode() self.titleNode = ImmediateTextNode()
@ -189,14 +198,25 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
self.addSubnode(self.leftDimNode) self.addSubnode(self.leftDimNode)
self.addSubnode(self.rightDimNode) self.addSubnode(self.rightDimNode)
self.addSubnode(self.frameNode) self.addSubnode(self.frameNode)
self.addSubnode(self.galleryButtonNode)
self.addSubnode(self.torchButtonNode) self.addSubnode(self.torchButtonNode)
self.addSubnode(self.titleNode) self.addSubnode(self.titleNode)
self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside)
self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside) self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside)
} }
deinit { deinit {
self.codeDisposable.dispose() self.codeDisposable.dispose()
self.camera.stopCapture(invalidate: true)
}
fileprivate func updateInForeground(_ inForeground: Bool) {
if !inForeground {
self.camera.stopCapture(invalidate: false)
} else {
self.camera.startCapture()
}
} }
override func didLoad() { override func didLoad() {
@ -272,14 +292,19 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0)) transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0))
let torchButtonSize = CGSize(width: 72.0, height: 72.0) let buttonSize = CGSize(width: 72.0, height: 72.0)
transition.updateFrame(node: self.torchButtonNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - torchButtonSize.width) / 2.0), y: dimHeight + frameSide + 50.0), size: torchButtonSize)) transition.updateFrame(node: self.galleryButtonNode, frame: CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0) - buttonSize.width - 28.0, y: dimHeight + frameSide + 50.0), size: buttonSize))
transition.updateFrame(node: self.torchButtonNode, frame: CGRect(origin: CGPoint(x: floor(layout.size.width / 2.0) + 28.0, y: dimHeight + frameSide + 50.0), size: buttonSize))
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height)) let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: dimHeight - titleSize.height - titleSpacing), size: titleSize) let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: dimHeight - titleSize.height - titleSpacing), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame) transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
} }
@objc private func galleryPressed() {
}
@objc private func torchPressed() { @objc private func torchPressed() {
self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected
self.camera.setTorchActive(self.torchButtonNode.isSelected) self.camera.setTorchActive(self.torchButtonNode.isSelected)

View File

@ -0,0 +1,146 @@
import Foundation
import UIKit
import SwiftSignalKit
import AppBundle
import AccountContext
import TelegramPresentationData
import AsyncDisplayKit
import Display
import Postbox
import QrCode
import ShareController
func shareInvoiceQrCode(context: AccountContext, invoice: String) {
let _ = (qrCode(string: invoice, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")), ecl: "Q")
|> map { generator -> UIImage? in
let imageSize = CGSize(width: 768.0, height: 768.0)
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), scale: 1.0))
return context?.generateImage()
}
|> deliverOnMainQueue).start(next: { image in
guard let image = image else {
return
}
let activityController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
context.sharedContext.applicationBindings.presentNativeController(activityController)
})
}
public final class WalletQrViewScreen: ViewController {
private let context: AccountContext
private let invoice: String
private var presentationData: PresentationData
private var previousScreenBrightness: CGFloat?
private var displayLinkAnimator: DisplayLinkAnimator?
private let idleTimerExtensionDisposable: Disposable
public init(context: AccountContext, invoice: String) {
self.context = context
self.invoice = invoice
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
let defaultNavigationPresentationData = NavigationBarPresentationData(presentationTheme: self.presentationData.theme, presentationStrings: self.presentationData.strings)
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor)
self.idleTimerExtensionDisposable = context.sharedContext.applicationBindings.pushIdleTimerExtension()
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings))
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
self.title = "QR Code"
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationShareIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.shareButtonPressed))
}
deinit {
self.idleTimerExtensionDisposable.dispose()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func loadDisplayNode() {
self.displayNode = WalletQrViewScreenNode(context: self.context, presentationData: self.presentationData, message: self.invoice)
self.displayNodeDidLoad()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let screenBrightness = UIScreen.main.brightness
if screenBrightness < 0.85 {
self.previousScreenBrightness = screenBrightness
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.5, from: screenBrightness, to: 0.85, update: { value in
UIScreen.main.brightness = value
}, completion: {
self.displayLinkAnimator = nil
})
}
}
public override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
let screenBrightness = UIScreen.main.brightness
if let previousScreenBrightness = self.previousScreenBrightness, screenBrightness > previousScreenBrightness {
self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2, from: screenBrightness, to: previousScreenBrightness, update: { value in
UIScreen.main.brightness = value
}, completion: {
self.displayLinkAnimator = nil
})
}
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! WalletQrViewScreenNode).containerLayoutUpdated(layout: layout, navigationHeight: self.navigationHeight, transition: transition)
}
@objc private func shareButtonPressed() {
shareInvoiceQrCode(context: self.context, invoice: self.invoice)
}
}
private final class WalletQrViewScreenNode: ViewControllerTracingNode {
private var presentationData: PresentationData
private let invoice: String
private let imageNode: TransformImageNode
init(context: AccountContext, presentationData: PresentationData, message: String) {
self.presentationData = presentationData
self.invoice = message
self.imageNode = TransformImageNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.imageNode)
self.imageNode.setSignal(qrCode(string: self.invoice, color: .black, backgroundColor: .white, icon: .custom(UIImage(bundleImageName: "Wallet/QrGem")), ecl: "Q"), attemptSynchronously: true)
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let makeImageLayout = self.imageNode.asyncLayout()
let imageSide = layout.size.width - 48.0 * 2.0
var imageSize = CGSize(width: imageSide, height: imageSide)
let imageApply = makeImageLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: nil))
let _ = imageApply()
transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: floor((layout.size.height - imageSize.height - layout.intrinsicInsets.bottom) / 2.0)), size: imageSize))
}
}

View File

@ -12,48 +12,79 @@ import SwiftSignalKit
import OverlayStatusController import OverlayStatusController
import ShareController import ShareController
private func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
private final class WalletReceiveScreenArguments { private final class WalletReceiveScreenArguments {
let context: AccountContext let context: AccountContext
let updateState: ((WalletReceiveScreenState) -> WalletReceiveScreenState) -> Void
let updateText: (WalletReceiveScreenEntryTag, String) -> Void
let selectNextInputItem: (WalletReceiveScreenEntryTag) -> Void
let dismissInput: () -> Void
let copyAddress: () -> Void let copyAddress: () -> Void
let shareAddressLink: () -> Void let shareAddressLink: () -> Void
let openQrCode: () -> Void
let displayQrCodeContextMenu: () -> Void
let scrollToBottom: () -> Void
init(context: AccountContext, copyAddress: @escaping () -> Void, shareAddressLink: @escaping () -> Void) { init(context: AccountContext, updateState: @escaping ((WalletReceiveScreenState) -> WalletReceiveScreenState) -> Void, updateText: @escaping (WalletReceiveScreenEntryTag, String) -> Void, selectNextInputItem: @escaping (WalletReceiveScreenEntryTag) -> Void, dismissInput: @escaping () -> Void, copyAddress: @escaping () -> Void, shareAddressLink: @escaping () -> Void, openQrCode: @escaping () -> Void, displayQrCodeContextMenu: @escaping () -> Void, scrollToBottom: @escaping () -> Void) {
self.context = context self.context = context
self.updateState = updateState
self.updateText = updateText
self.selectNextInputItem = selectNextInputItem
self.dismissInput = dismissInput
self.copyAddress = copyAddress self.copyAddress = copyAddress
self.shareAddressLink = shareAddressLink self.shareAddressLink = shareAddressLink
self.openQrCode = openQrCode
self.displayQrCodeContextMenu = displayQrCodeContextMenu
self.scrollToBottom = scrollToBottom
} }
} }
private enum WalletReceiveScreenSection: Int32 { private enum WalletReceiveScreenSection: Int32 {
case address case address
case amount
case comment
}
private enum WalletReceiveScreenEntryTag: ItemListItemTag {
case amount
case comment
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? WalletReceiveScreenEntryTag {
return self == other
} else {
return false
}
}
} }
private enum WalletReceiveScreenEntry: ItemListNodeEntry { private enum WalletReceiveScreenEntry: ItemListNodeEntry {
case addressHeader(PresentationTheme, String)
case addressCode(PresentationTheme, String) case addressCode(PresentationTheme, String)
case address(PresentationTheme, String) case addressHeader(PresentationTheme, String)
case address(PresentationTheme, String, Bool)
case copyAddress(PresentationTheme, String) case copyAddress(PresentationTheme, String)
case shareAddressLink(PresentationTheme, String) case shareAddressLink(PresentationTheme, String)
case addressInfo(PresentationTheme, String) case addressInfo(PresentationTheme, String)
case amountHeader(PresentationTheme, String)
case amount(PresentationTheme, PresentationStrings, String, String)
case commentHeader(PresentationTheme, String)
case comment(PresentationTheme, String, String)
var section: ItemListSectionId { var section: ItemListSectionId {
switch self { switch self {
case .addressHeader, .addressCode, .address, .copyAddress, .shareAddressLink, .addressInfo: case .addressCode, .addressHeader, .address, .copyAddress, .shareAddressLink, .addressInfo:
return WalletReceiveScreenSection.address.rawValue return WalletReceiveScreenSection.address.rawValue
case .amountHeader, .amount:
return WalletReceiveScreenSection.amount.rawValue
case .commentHeader, .comment:
return WalletReceiveScreenSection.comment.rawValue
} }
} }
var stableId: Int32 { var stableId: Int32 {
switch self { switch self {
case .addressHeader:
return 0
case .addressCode: case .addressCode:
return 0
case .addressHeader:
return 1 return 1
case .address: case .address:
return 2 return 2
@ -63,25 +94,33 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
return 4 return 4
case .addressInfo: case .addressInfo:
return 5 return 5
case .amountHeader:
return 6
case .amount:
return 7
case .commentHeader:
return 8
case .comment:
return 9
} }
} }
static func ==(lhs: WalletReceiveScreenEntry, rhs: WalletReceiveScreenEntry) -> Bool { static func ==(lhs: WalletReceiveScreenEntry, rhs: WalletReceiveScreenEntry) -> Bool {
switch lhs { switch lhs {
case let .addressHeader(lhsTheme, lhsText):
if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .addressCode(lhsTheme, lhsAddress): case let .addressCode(lhsTheme, lhsAddress):
if case let .addressCode(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress { if case let .addressCode(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress {
return true return true
} else { } else {
return false return false
} }
case let .address(lhsTheme, lhsAddress): case let .addressHeader(lhsTheme, lhsText):
if case let .address(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress { if case let .addressHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .address(lhsTheme, lhsAddress, lhsMonospace):
if case let .address(rhsTheme, rhsAddress, rhsMonospace) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress, lhsMonospace == rhsMonospace {
return true return true
} else { } else {
return false return false
@ -104,6 +143,30 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .amountHeader(lhsTheme, lhsText):
if case let .amountHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsBalance):
if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsBalance) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsBalance == rhsBalance {
return true
} else {
return false
}
case let .commentHeader(lhsTheme, lhsText):
if case let .commentHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .comment(lhsTheme, lhsPlaceholder, lhsText):
if case let .comment(rhsTheme, rhsPlaceholder, rhsText) = rhs, lhsTheme === rhsTheme, lhsPlaceholder == rhsPlaceholder, lhsText == rhsText {
return true
} else {
return false
}
} }
} }
@ -113,12 +176,16 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
func item(_ arguments: WalletReceiveScreenArguments) -> ListViewItem { func item(_ arguments: WalletReceiveScreenArguments) -> ListViewItem {
switch self { switch self {
case let .addressCode(theme, text):
return WalletQrCodeItem(theme: theme, address: text, sectionId: self.section, style: .blocks, action: {
arguments.openQrCode()
}, longTapAction: {
arguments.displayQrCodeContextMenu()
})
case let .addressHeader(theme, text): case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .addressCode(theme, address): case let .address(theme, text, monospace):
return WalletQrCodeItem(theme: theme, address: "ton://\(address)", sectionId: self.section, style: .blocks) return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], font: monospace ? .monospace : .default, sectionId: self.section, style: .blocks)
case let .address(theme, address):
return ItemListMultilineTextItem(theme: theme, text: address, enabledEntityTypes: [], font: .monospace, sectionId: self.section, style: .blocks)
case let .copyAddress(theme, text): case let .copyAddress(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyAddress() arguments.copyAddress()
@ -129,18 +196,97 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
}) })
case let .addressInfo(theme, text): case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section) return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .amountHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .amount(theme, strings, placeholder, text):
return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, tag: WalletReceiveScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in
let text = formatAmountText(text, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletReceiveScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
return isValidAmount(text)
}, processPaste: { pastedText in
if isValidAmount(pastedText) {
return normalizedStringForGramsString(pastedText)
} else {
return text
}
}, updatedFocus: { focus in
arguments.updateState { state in
var state = state
state.focusItemTag = focus ? WalletReceiveScreenEntryTag.amount : nil
return state
}
if focus {
arguments.scrollToBottom()
} else {
let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 }
arguments.updateState { state in
var state = state
if !state.amount.isEmpty {
state.amount = normalizedStringForGramsString(state.amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
return state
}
}
}, action: {
arguments.selectNextInputItem(WalletReceiveScreenEntryTag.amount)
})
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, placeholder, value):
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 128, display: true), sectionId: self.section, style: .blocks, returnKeyType: .done, textUpdated: { text in
arguments.updateText(WalletReceiveScreenEntryTag.comment, text)
}, updatedFocus: { focus in
arguments.updateState { state in
var state = state
state.focusItemTag = focus ? WalletReceiveScreenEntryTag.comment : nil
return state
}
if focus {
arguments.scrollToBottom()
}
}, tag: WalletReceiveScreenEntryTag.comment, action: {
arguments.dismissInput()
})
} }
} }
} }
private func walletReceiveScreenEntries(presentationData: PresentationData, address: String) -> [WalletReceiveScreenEntry] { private struct WalletReceiveScreenState: Equatable {
var amount: String
var comment: String
var focusItemTag: WalletReceiveScreenEntryTag?
var isEmpty: Bool {
return self.amount.isEmpty && self.comment.isEmpty
}
}
private func walletReceiveScreenEntries(presentationData: PresentationData, address: String, state: WalletReceiveScreenState) -> [WalletReceiveScreenEntry] {
var entries: [WalletReceiveScreenEntry] = [] var entries: [WalletReceiveScreenEntry] = []
entries.append(.addressHeader(presentationData.theme, "YOUR WALLET ADDRESS")) entries.append(.addressCode(presentationData.theme, invoiceUrl(address: address, state: state, escapeComment: true)))
entries.append(.addressCode(presentationData.theme, address)) entries.append(.addressHeader(presentationData.theme, state.isEmpty ? "YOUR WALLET ADDRESS" : "INVOICE URL"))
entries.append(.address(presentationData.theme, formatAddress(address)))
entries.append(.copyAddress(presentationData.theme, "Copy Wallet Address")) let addressText: String
entries.append(.shareAddressLink(presentationData.theme, "Share Wallet Address")) var addressMonospace = false
if state.isEmpty {
addressText = formatAddress(address)
addressMonospace = true
} else {
addressText = invoiceUrl(address: address, state: state, escapeComment: false)
}
entries.append(.address(presentationData.theme, addressText, addressMonospace))
entries.append(.copyAddress(presentationData.theme, state.isEmpty ? "Copy Wallet Address" : "Copy Invoice URL"))
entries.append(.shareAddressLink(presentationData.theme, state.isEmpty ? "Share Wallet Address" : "Share Invoice URL"))
entries.append(.addressInfo(presentationData.theme, "Share this link with other Gram wallet owners to receive Grams from them.")) entries.append(.addressInfo(presentationData.theme, "Share this link with other Gram wallet owners to receive Grams from them."))
let amount = amountValue(state.amount)
entries.append(.amountHeader(presentationData.theme, "AMOUNT"))
entries.append(.amount(presentationData.theme, presentationData.strings, "Grams to receive", state.amount ?? ""))
entries.append(.commentHeader(presentationData.theme, "COMMENT (OPTIONAL)"))
entries.append(.comment(presentationData.theme, "Description of the payment", state.comment))
return entries return entries
} }
@ -152,42 +298,176 @@ private final class WalletReceiveScreenImpl: ItemListController<WalletReceiveScr
} }
func walletReceiveScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) -> ViewController { private func invoiceUrl(address: String, state: WalletReceiveScreenState, escapeComment: Bool = true) -> String {
var presentControllerImpl: ((ViewController, Any?) -> Void)? let escapedAddress = address.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
var dismissImpl: (() -> Void)? var arguments = ""
if !state.amount.isEmpty {
let arguments = WalletReceiveScreenArguments(context: context, copyAddress: { arguments += arguments.isEmpty ? "/?" : "&"
let presentationData = context.sharedContext.currentPresentationData.with { $0 } arguments += "amount=\(amountValue(state.amount))"
}
UIPasteboard.general.string = address if !state.comment.isEmpty, let escapedComment = state.comment.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) {
arguments += arguments.isEmpty ? "/?" : "&"
if escapeComment {
arguments += "text=\(escapedComment)"
} else {
arguments += "text=\(state.comment)"
}
}
return "ton://\(escapedAddress)\(arguments)"
}
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .genericSuccess("Address copied to clipboard.", false)), nil) func walletReceiveScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) -> ViewController {
let initialState = WalletReceiveScreenState(amount: "", comment: "", focusItemTag: nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
let updateState: ((WalletReceiveScreenState) -> WalletReceiveScreenState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var dismissImpl: (() -> Void)?
var selectNextInputItemImpl: ((WalletReceiveScreenEntryTag) -> Void)?
var dismissInputImpl: (() -> Void)?
var ensureItemVisibleImpl: ((WalletReceiveScreenEntryTag, Bool) -> Void)?
var displayQrCodeContextMenuImpl: (() -> Void)?
weak var currentStatusController: ViewController?
let arguments = WalletReceiveScreenArguments(context: context, updateState: { f in
updateState(f)
}, updateText: { tag, value in
updateState { state in
var state = state
switch tag {
case .amount:
state.amount = value
case .comment:
state.comment = value
}
return state
}
ensureItemVisibleImpl?(tag, false)
}, selectNextInputItem: { tag in
selectNextInputItemImpl?(tag)
}, dismissInput: {
dismissInputImpl?()
}, copyAddress: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let state = stateValue.with { $0 }
let successText: String
if state.isEmpty {
UIPasteboard.general.string = address
successText = "Address copied to clipboard."
} else {
UIPasteboard.general.string = invoiceUrl(address: address, state: state)
successText = "Invoice URL copied to clipboard."
}
if currentStatusController == nil {
let statusController = OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .genericSuccess(successText, false))
presentControllerImpl?(statusController, nil)
currentStatusController = statusController
}
}, shareAddressLink: { }, shareAddressLink: {
let controller = ShareController(context: context, subject: .url("ton://\(address)"), preferredAction: .default) dismissInputImpl?()
let state = stateValue.with { $0 }
let url = invoiceUrl(address: address, state: state)
let controller = ShareController(context: context, subject: .url(url), preferredAction: .default)
presentControllerImpl?(controller, nil) presentControllerImpl?(controller, nil)
}, openQrCode: {
dismissInputImpl?()
let state = stateValue.with { $0 }
let url = invoiceUrl(address: address, state: state)
pushImpl?(WalletQrViewScreen(context: context, invoice: url))
}, displayQrCodeContextMenu: {
dismissInputImpl?()
displayQrCodeContextMenuImpl?()
}, scrollToBottom: {
ensureItemVisibleImpl?(WalletReceiveScreenEntryTag.comment, true)
}) })
let address: Signal<String, NoError> = .single(address) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState<WalletReceiveScreenEntry>, WalletReceiveScreenEntry.ItemGenerationArguments)) in
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, address)
|> map { presentationData, address -> (ItemListControllerState, (ItemListNodeState<WalletReceiveScreenEntry>, WalletReceiveScreenEntry.ItemGenerationArguments)) in
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .regular, enabled: true, action: { let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .regular, enabled: true, action: {
dismissImpl?() dismissImpl?()
}) })
var ensureVisibleItemTag: ItemListItemTag?
if let focusItemTag = state.focusItemTag {
ensureVisibleItemTag = focusItemTag
}
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Receive Grams"), leftNavigationButton: rightNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false) let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Receive Grams"), leftNavigationButton: ItemListNavigationButton(content: .none, style: .regular, enabled: false, action: {}), rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletReceiveScreenEntries(presentationData: presentationData, address: address), style: .blocks, animateChanges: false) let listState = ItemListNodeState(entries: walletReceiveScreenEntries(presentationData: presentationData, address: address, state: state), style: .blocks, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: false)
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} }
let controller = WalletReceiveScreenImpl(context: context, state: signal) let controller = WalletReceiveScreenImpl(context: context, state: signal)
controller.navigationPresentation = .modal controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.experimentalSnapScrollToItem = true
presentControllerImpl = { [weak controller] c, a in presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a) controller?.present(c, in: .window(.root), with: a)
} }
pushImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss() let _ = controller?.dismiss()
} }
selectNextInputItemImpl = { [weak controller] currentTag in
guard let controller = controller else {
return
}
var resultItemNode: ItemListItemFocusableNode?
var focusOnNext = false
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode, let tag = itemNode.tag, let focusableItemNode = itemNode as? ItemListItemFocusableNode {
if focusOnNext && resultItemNode == nil {
resultItemNode = focusableItemNode
return true
} else if currentTag.isEqual(to: tag) {
focusOnNext = true
}
}
return false
})
if let resultItemNode = resultItemNode {
resultItemNode.focus()
}
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
ensureItemVisibleImpl = { [weak controller] targetTag, animated in
controller?.afterLayout({
guard let controller = controller else {
return
}
var resultItemNode: ListViewItemNode?
let state = stateValue.with({ $0 })
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode as? ListViewItemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
controller.ensureItemNodeVisible(resultItemNode, animated: animated)
}
})
}
displayQrCodeContextMenuImpl = { [weak controller] in
let state = stateValue.with { $0 }
let url = invoiceUrl(address: address, state: state)
shareInvoiceQrCode(context: context, invoice: url)
}
return controller return controller
} }

View File

@ -11,21 +11,27 @@ import ItemListUI
import SwiftSignalKit import SwiftSignalKit
import AlertUI import AlertUI
import TextFormat import TextFormat
import DeviceAccess
import TelegramStringFormatting
import UrlHandling
private let walletAddressLength: Int = 48
private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed() private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()
private final class WalletSendScreenArguments { private final class WalletSendScreenArguments {
let context: AccountContext let context: AccountContext
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void
let updateText: (WalletSendScreenEntryTag, String) -> Void
let selectNextInputItem: (WalletSendScreenEntryTag) -> Void let selectNextInputItem: (WalletSendScreenEntryTag) -> Void
let dismissInput: () -> Void
let openQrScanner: () -> Void let openQrScanner: () -> Void
let proceed: () -> Void let proceed: () -> Void
init(context: AccountContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) { init(context: AccountContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, updateText: @escaping (WalletSendScreenEntryTag, String) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, dismissInput: @escaping () -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) {
self.context = context self.context = context
self.updateState = updateState self.updateState = updateState
self.updateText = updateText
self.selectNextInputItem = selectNextInputItem self.selectNextInputItem = selectNextInputItem
self.dismissInput = dismissInput
self.openQrScanner = openQrScanner self.openQrScanner = openQrScanner
self.proceed = proceed self.proceed = proceed
} }
@ -51,76 +57,12 @@ private enum WalletSendScreenEntryTag: ItemListItemTag {
} }
} }
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=").inverted
private func isValidAddress(_ address: String, exactLength: Bool = false) -> Bool {
if address.count > walletAddressLength || address.rangeOfCharacter(from: invalidAddressCharacters) != nil {
return false
}
if exactLength && address.count != walletAddressLength {
return false
}
return true
}
private let invalidAmountCharacters = CharacterSet(charactersIn: "01234567890.,").inverted
private func isValidAmount(_ amount: String) -> Bool {
if amount.rangeOfCharacter(from: invalidAmountCharacters) != nil {
return false
}
var hasDecimalSeparator = false
var hasLeadingZero = false
var index = 0
for c in amount {
if c == "." || c == "," {
if !hasDecimalSeparator {
hasDecimalSeparator = true
} else {
return false
}
}
index += 1
}
var decimalIndex: String.Index?
if let index = amount.firstIndex(of: ".") {
decimalIndex = index
} else if let index = amount.firstIndex(of: ",") {
decimalIndex = index
}
if let decimalIndex = decimalIndex, amount.distance(from: decimalIndex, to: amount.endIndex) > 10 {
return false
}
return true
}
private func formatAmountText(_ amount: Int64, decimalSeparator: String = ".") -> String {
if amount < 1000000000 {
return "0\(decimalSeparator)\(String(amount).rightJustified(width: 9, pad: "0"))"
} else {
var string = String(amount)
string.insert(contentsOf: decimalSeparator, at: string.index(string.endIndex, offsetBy: -9))
return string
}
}
private func amountValue(_ string: String) -> Int64 {
return Int64((Double(string.replacingOccurrences(of: ",", with: ".")) ?? 0.0) * 1000000000.0)
}
private func normalizedStringForGramsString(_ string: String, decimalSeparator: String = ".") -> String {
return formatAmountText(amountValue(string), decimalSeparator: decimalSeparator)
}
private enum WalletSendScreenEntry: ItemListNodeEntry { private enum WalletSendScreenEntry: ItemListNodeEntry {
case addressHeader(PresentationTheme, String) case addressHeader(PresentationTheme, String)
case address(PresentationTheme, String, String) case address(PresentationTheme, String, String)
case addressInfo(PresentationTheme, String) case addressInfo(PresentationTheme, String)
case amountHeader(PresentationTheme, String, String?, Bool) case amountHeader(PresentationTheme, String, String?, Bool)
case amount(PresentationTheme, PresentationStrings, String, String) case amount(PresentationTheme, PresentationStrings, String, String)
case commentHeader(PresentationTheme, String) case commentHeader(PresentationTheme, String)
case comment(PresentationTheme, String, String) case comment(PresentationTheme, String, String)
@ -180,8 +122,8 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
} else { } else {
return false return false
} }
case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsBalance): case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsAmount):
if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsBalance) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsBalance == rhsBalance { if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsAmount == rhsAmount {
return true return true
} else { } else {
return false return false
@ -210,14 +152,44 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
case let .addressHeader(theme, text): case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .address(theme, placeholder, address): case let .address(theme, placeholder, address):
return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { address in return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { text in
arguments.updateState { state in arguments.updateText(WalletSendScreenEntryTag.address, text.replacingOccurrences(of: "\n", with: ""))
var state = state
state.address = address.replacingOccurrences(of: "\n", with: "")
return state
}
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
return isValidAddress(text) return isValidAddress(text)
}, processPaste: { text in
if let url = URL(string: text), let parsedUrl = parseWalletUrl(url) {
var focusItemTag: WalletSendScreenEntryTag?
arguments.updateState { state in
var state = state
state.address = parsedUrl.address
if let amount = parsedUrl.amount {
state.amount = formatBalanceText(amount, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator)
} else if state.amount.isEmpty {
focusItemTag = WalletSendScreenEntryTag.address
}
if let comment = parsedUrl.comment {
state.comment = comment
} else if state.comment.isEmpty && focusItemTag == nil {
focusItemTag = WalletSendScreenEntryTag.amount
}
return state
}
if let focusItemTag = focusItemTag {
arguments.selectNextInputItem(focusItemTag)
} else {
arguments.dismissInput()
}
} else if isValidAddress(text) {
arguments.updateText(WalletSendScreenEntryTag.address, text)
if isValidAddress(text, exactLength: true, url: false) {
arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
}
} else if isValidAddress(text, url: true) {
arguments.updateText(WalletSendScreenEntryTag.address, convertedAddress(text, url: false))
if isValidAddress(text, exactLength: true, url: true) {
arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
}
}
}, tag: WalletSendScreenEntryTag.address, action: { }, tag: WalletSendScreenEntryTag.address, action: {
arguments.selectNextInputItem(WalletSendScreenEntryTag.address) arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
}, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: { }, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: {
@ -229,16 +201,14 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section)
case let .amount(theme, strings, placeholder, text): case let .amount(theme, strings, placeholder, text):
return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, tag: WalletSendScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in return ItemListSingleLineInputItem(theme: theme, strings: strings, title: NSAttributedString(string: ""), text: text, placeholder: placeholder, type: .decimal, returnKeyType: .next, tag: WalletSendScreenEntryTag.amount, sectionId: self.section, textUpdated: { text in
arguments.updateState { state in let text = formatAmountText(text, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator)
var state = state arguments.updateText(WalletSendScreenEntryTag.amount, text)
state.amount = text
return state
}
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
return isValidAmount(text) return isValidAmount(text)
}, processPaste: { pastedText in }, processPaste: { pastedText in
if isValidAmount(pastedText) { if isValidAmount(pastedText) {
return normalizedStringForGramsString(pastedText) let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 }
return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else { } else {
return text return text
} }
@ -259,12 +229,8 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
case let .commentHeader(theme, text): case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, placeholder, value): case let .comment(theme, placeholder, value):
return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 128, display: true), sectionId: self.section, style: .blocks, returnKeyType: .send, textUpdated: { comment in return ItemListMultilineInputItem(theme: theme, text: value, placeholder: placeholder, maxLength: ItemListMultilineInputItemTextLimit(value: 128, display: true), sectionId: self.section, style: .blocks, returnKeyType: .send, textUpdated: { text in
arguments.updateState { state in arguments.updateText(WalletSendScreenEntryTag.comment, text)
var state = state
state.text = comment
return state
}
}, tag: WalletSendScreenEntryTag.comment, action: { }, tag: WalletSendScreenEntryTag.comment, action: {
arguments.proceed() arguments.proceed()
}) })
@ -275,7 +241,7 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
private struct WalletSendScreenState: Equatable { private struct WalletSendScreenState: Equatable {
var address: String var address: String
var amount: String var amount: String
var text: String var comment: String
} }
private func walletSendScreenEntries(presentationData: PresentationData, balance: Int64?, state: WalletSendScreenState) -> [WalletSendScreenEntry] { private func walletSendScreenEntries(presentationData: PresentationData, balance: Int64?, state: WalletSendScreenState) -> [WalletSendScreenEntry] {
@ -289,8 +255,8 @@ private func walletSendScreenEntries(presentationData: PresentationData, balance
entries.append(.amountHeader(presentationData.theme, "AMOUNT", balance.flatMap { "BALANCE: \(formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" }, amount > 0 && (balance ?? 0) < amount)) entries.append(.amountHeader(presentationData.theme, "AMOUNT", balance.flatMap { "BALANCE: \(formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" }, amount > 0 && (balance ?? 0) < amount))
entries.append(.amount(presentationData.theme, presentationData.strings, "Grams to send", state.amount ?? "")) entries.append(.amount(presentationData.theme, presentationData.strings, "Grams to send", state.amount ?? ""))
entries.append(.commentHeader(presentationData.theme, "COMMENT")) entries.append(.commentHeader(presentationData.theme, "COMMENT (OPTIONAL)"))
entries.append(.comment(presentationData.theme, "Optional description of the payment", state.text)) entries.append(.comment(presentationData.theme, "Description of the payment", state.comment))
return entries return entries
} }
@ -302,10 +268,10 @@ private final class WalletSendScreenImpl: ItemListController<WalletSendScreenEnt
} }
func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil, text: String? = nil) -> ViewController { public func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil, comment: String? = nil) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatAmountText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", text: text ?? "")
let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: comment ?? "")
let statePromise = ValuePromise(initialState, ignoreRepeated: true) let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState) let stateValue = Atomic(value: initialState)
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in
@ -319,36 +285,63 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)? var dismissInputImpl: (() -> Void)?
var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)? var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)?
var ensureItemVisibleImpl: ((WalletSendScreenEntryTag) -> Void)?
let arguments = WalletSendScreenArguments(context: context, updateState: { f in let arguments = WalletSendScreenArguments(context: context, updateState: { f in
updateState(f) updateState(f)
}, updateText: { tag, value in
updateState { state in
var state = state
switch tag {
case .address:
state.address = value
case .amount:
state.amount = value
case .comment:
state.comment = value
}
return state
}
ensureItemVisibleImpl?(tag)
}, selectNextInputItem: { tag in }, selectNextInputItem: { tag in
selectNextInputItemImpl?(tag) selectNextInputItemImpl?(tag)
}, dismissInput: {
dismissInputImpl?()
}, openQrScanner: { }, openQrScanner: {
dismissInputImpl?() dismissInputImpl?()
pushImpl?(WalletQrScanScreen(context: context, completion: { address, amount, comment in
var updatedState: WalletSendScreenState? DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
updateState { state in presentControllerImpl?(c, a)
var state = state }, openSettings: {
state.address = address context.sharedContext.applicationBindings.openSettings()
if let amount = amount { }, { granted in
state.amount = formatAmountText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) guard granted else {
} return
if let comment = comment {
state.text = comment
}
updatedState = state
return state
} }
popImpl?() pushImpl?(WalletQrScanScreen(context: context, completion: { parsedUrl in
if let updatedState = updatedState { var updatedState: WalletSendScreenState?
if updatedState.amount.isEmpty { updateState { state in
selectNextInputItemImpl?(WalletSendScreenEntryTag.address) var state = state
} else if updatedState.text.isEmpty { state.address = parsedUrl.address
selectNextInputItemImpl?(WalletSendScreenEntryTag.amount) if let amount = parsedUrl.amount {
state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
if let comment = parsedUrl.comment {
state.comment = comment
}
updatedState = state
return state
} }
} popImpl?()
})) if let updatedState = updatedState {
if updatedState.amount.isEmpty {
selectNextInputItemImpl?(WalletSendScreenEntryTag.address)
} else if updatedState.comment.isEmpty {
selectNextInputItemImpl?(WalletSendScreenEntryTag.amount)
}
}
}))
})
}, proceed: { }, proceed: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let state = stateValue.with { $0 } let state = stateValue.with { $0 }
@ -356,7 +349,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
updateState { state in updateState { state in
var state = state var state = state
state.amount = formatAmountText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
return state return state
} }
@ -375,7 +368,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
dismissAlertImpl?(true) dismissAlertImpl?(true)
}), TextAlertAction(type: .defaultAction, title: "Confirm", action: { }), TextAlertAction(type: .defaultAction, title: "Confirm", action: {
dismissAlertImpl?(false) dismissAlertImpl?(false)
pushImpl?(WalletSplashScreen(context: context, tonContext: tonContext, mode: .sending(walletInfo, state.address, amount, state.text))) pushImpl?(WalletSplashScreen(context: context, tonContext: tonContext, mode: .sending(walletInfo, state.address, amount, state.comment)))
})], dismissAutomatically: false) })], dismissAutomatically: false)
presentInGlobalOverlayImpl?(controller, nil) presentInGlobalOverlayImpl?(controller, nil)
@ -419,7 +412,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
let amount = amountValue(state.amount) let amount = amountValue(state.amount)
var sendEnabled = false var sendEnabled = false
if let balance = balance { if let balance = balance {
sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= balance.balance sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= balance.balance && state.comment.count <= 128
} }
let rightNavigationButton = ItemListNavigationButton(content: .text("Send"), style: .bold, enabled: sendEnabled, action: { let rightNavigationButton = ItemListNavigationButton(content: .text("Send"), style: .bold, enabled: sendEnabled, action: {
arguments.proceed() arguments.proceed()
@ -432,7 +425,8 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
} }
let controller = WalletSendScreenImpl(context: context, state: signal) let controller = WalletSendScreenImpl(context: context, state: signal)
controller.navigationPresentation = .modalInLargeLayout controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
presentControllerImpl = { [weak controller] c, a in presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a) controller?.present(c, in: .window(.root), with: a)
} }
@ -473,5 +467,28 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
resultItemNode.focus() resultItemNode.focus()
} }
} }
ensureItemVisibleImpl = { [weak controller] targetTag in
controller?.afterLayout({
guard let controller = controller else {
return
}
var resultItemNode: ListViewItemNode?
let state = stateValue.with({ $0 })
let _ = controller.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListItemNode {
if let tag = itemNode.tag, tag.isEqual(to: targetTag) {
resultItemNode = itemNode as? ListViewItemNode
return true
}
}
return false
})
if let resultItemNode = resultItemNode {
controller.ensureItemNodeVisible(resultItemNode)
}
})
}
return controller return controller
} }

View File

@ -17,10 +17,12 @@ import TelegramStringFormatting
private final class WalletTransactionInfoControllerArguments { private final class WalletTransactionInfoControllerArguments {
let copyWalletAddress: () -> Void let copyWalletAddress: () -> Void
let sendGrams: () -> Void let sendGrams: () -> Void
let displayContextMenu: (WalletTransactionInfoEntryTag, String) -> Void
init(copyWalletAddress: @escaping () -> Void, sendGrams: @escaping () -> Void) { init(copyWalletAddress: @escaping () -> Void, sendGrams: @escaping () -> Void, displayContextMenu: @escaping (WalletTransactionInfoEntryTag, String) -> Void) {
self.copyWalletAddress = copyWalletAddress self.copyWalletAddress = copyWalletAddress
self.sendGrams = sendGrams self.sendGrams = sendGrams
self.displayContextMenu = displayContextMenu
} }
} }
@ -30,6 +32,18 @@ private enum WalletTransactionInfoSection: Int32 {
case comment case comment
} }
private enum WalletTransactionInfoEntryTag: ItemListItemTag {
case comment
func isEqual(to other: ItemListItemTag) -> Bool {
if let other = other as? WalletTransactionInfoEntryTag {
return self == other
} else {
return false
}
}
}
private enum WalletTransactionInfoEntry: ItemListNodeEntry { private enum WalletTransactionInfoEntry: ItemListNodeEntry {
case amount(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, WalletTransaction) case amount(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, WalletTransaction)
case infoHeader(PresentationTheme, String) case infoHeader(PresentationTheme, String)
@ -92,7 +106,9 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
case let .commentHeader(theme, text): case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section) return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .comment(theme, text): case let .comment(theme, text):
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks) return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], sectionId: self.section, style: .blocks, longTapAction: {
arguments.displayContextMenu(WalletTransactionInfoEntryTag.comment, text)
}, tag: WalletTransactionInfoEntryTag.comment)
} }
} }
} }
@ -157,12 +173,6 @@ private func extractDescription(_ walletTransaction: WalletTransaction) -> Strin
return text return text
} }
private func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] { private func walletTransactionInfoControllerEntries(presentationData: PresentationData, walletTransaction: WalletTransaction, state: WalletTransactionInfoControllerState) -> [WalletTransactionInfoEntry] {
var entries: [WalletTransactionInfoEntry] = [] var entries: [WalletTransactionInfoEntry] = []
@ -203,6 +213,7 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
var dismissImpl: (() -> Void)? var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)? var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)? var pushImpl: ((ViewController) -> Void)?
var displayContextMenuImpl: ((WalletTransactionInfoEntryTag, String) -> Void)?
let arguments = WalletTransactionInfoControllerArguments(copyWalletAddress: { let arguments = WalletTransactionInfoControllerArguments(copyWalletAddress: {
let address = extractAddress(walletTransaction) let address = extractAddress(walletTransaction)
@ -217,6 +228,8 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
dismissImpl?() dismissImpl?()
pushImpl?(walletSendScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address)) pushImpl?(walletSendScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
} }
}, displayContextMenu: { tag, text in
displayContextMenuImpl?(tag, text)
}) })
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get()) let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get())
@ -242,6 +255,35 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
pushImpl = { [weak controller] c in pushImpl = { [weak controller] c in
controller?.push(c) controller?.push(c)
} }
displayContextMenuImpl = { [weak controller] tag, value in
if let strongController = controller {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
var resultItemNode: ListViewItemNode?
let _ = strongController.frameForItemNode({ itemNode in
if let itemNode = itemNode as? ItemListMultilineTextItemNode {
if let itemTag = itemNode.tag as? WalletTransactionInfoEntryTag {
if itemTag == tag {
resultItemNode = itemNode
return true
}
}
}
return false
})
if let resultItemNode = resultItemNode {
let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: {
UIPasteboard.general.string = value
})])
strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in
if let strongController = controller, let resultItemNode = resultItemNode {
return (resultItemNode, resultItemNode.contentBounds.insetBy(dx: 0.0, dy: -2.0), strongController.displayNode, strongController.view.bounds)
} else {
return nil
}
}))
}
}
}
return controller return controller
} }

View File

@ -0,0 +1,112 @@
import Foundation
import TelegramStringFormatting
let walletAddressLength: Int = 48
func formatAddress(_ address: String) -> String {
var address = address
address.insert("\n", at: address.index(address.startIndex, offsetBy: address.count / 2))
return address
}
func formatBalanceText(_ value: Int64, decimalSeparator: String) -> String {
var balanceText = "\(abs(value))"
while balanceText.count < 10 {
balanceText.insert("0", at: balanceText.startIndex)
}
balanceText.insert(contentsOf: decimalSeparator, at: balanceText.index(balanceText.endIndex, offsetBy: -9))
while true {
if balanceText.hasSuffix("0") {
if balanceText.hasSuffix("\(decimalSeparator)0") {
break
} else {
balanceText.removeLast()
}
} else {
break
}
}
if value < 0 {
balanceText.insert("-", at: balanceText.startIndex)
}
return balanceText
}
private let invalidAmountCharacters = CharacterSet(charactersIn: "01234567890.,").inverted
func isValidAmount(_ amount: String) -> Bool {
let amount = normalizeArabicNumeralString(amount, type: .western)
if amount.rangeOfCharacter(from: invalidAmountCharacters) != nil {
return false
}
var hasDecimalSeparator = false
var hasLeadingZero = false
var index = 0
for c in amount {
if c == "." || c == "," {
if !hasDecimalSeparator {
hasDecimalSeparator = true
} else {
return false
}
}
index += 1
}
var decimalIndex: String.Index?
if let index = amount.firstIndex(of: ".") {
decimalIndex = index
} else if let index = amount.firstIndex(of: ",") {
decimalIndex = index
}
if let decimalIndex = decimalIndex, amount.distance(from: decimalIndex, to: amount.endIndex) > 10 {
return false
}
return true
}
func amountValue(_ string: String) -> Int64 {
return Int64((Double(string.replacingOccurrences(of: ",", with: ".")) ?? 0.0) * 1000000000.0)
}
func normalizedStringForGramsString(_ string: String, decimalSeparator: String = ".") -> String {
return formatBalanceText(amountValue(string), decimalSeparator: decimalSeparator)
}
func formatAmountText(_ text: String, decimalSeparator: String) -> String {
var text = normalizeArabicNumeralString(text, type: .western)
if text == "." || text == "," {
text = "0\(decimalSeparator)"
} else if text == "0" {
text = "0\(decimalSeparator)"
} else if text.hasPrefix("0") && text.firstIndex(of: ".") == nil && text.firstIndex(of: ",") == nil {
var trimmedText = text
while trimmedText.first == "0" {
trimmedText.removeFirst()
}
text = trimmedText
}
return text
}
private let invalidAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=").inverted
private let invalidUrlAddressCharacters = CharacterSet(charactersIn: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=").inverted
func isValidAddress(_ address: String, exactLength: Bool = false, url: Bool = false) -> Bool {
if address.count > walletAddressLength || address.rangeOfCharacter(from: url ? invalidUrlAddressCharacters : invalidAddressCharacters) != nil {
return false
}
if exactLength && address.count != walletAddressLength {
return false
}
return true
}
func convertedAddress(_ address: String, url: Bool) -> String {
if url {
return address.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
} else {
return address.replacingOccurrences(of: "-", with: "+").replacingOccurrences(of: "_", with: "/")
}
}

View File

@ -2250,6 +2250,8 @@ private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
private let inputNode: TextFieldNode private let inputNode: TextFieldNode
private let clearButtonNode: HighlightableButtonNode private let clearButtonNode: HighlightableButtonNode
public private(set) var isLast: Bool
var text: String { var text: String {
get { get {
return self.inputNode.textField.text ?? "" return self.inputNode.textField.text ?? ""
@ -2263,6 +2265,7 @@ private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
self.next = next self.next = next
self.focused = focused self.focused = focused
self.pasteWords = pasteWords self.pasteWords = pasteWords
self.isLast = isLast
self.backgroundNode = ASImageNode() self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false self.backgroundNode.displaysAsynchronously = false
@ -2710,7 +2713,13 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
guard let strongSelf = self else { guard let strongSelf = self else {
return return
} }
strongSelf.scrollNode.view.scrollRectToVisible(node.frame.insetBy(dx: 0.0, dy: -10.0), animated: true) if node.isLast {
UIView.animate(withDuration: 0.3, animations: {
strongSelf.scrollNode.view.scrollRectToVisible(strongSelf.buttonNode.frame.insetBy(dx: 0.0, dy: -10.0), animated: false)
})
} else {
strongSelf.scrollNode.view.scrollRectToVisible(node.frame.insetBy(dx: 0.0, dy: -10.0), animated: true)
}
} }
pasteWords = { [weak self] wordList in pasteWords = { [weak self] wordList in
guard let strongSelf = self else { guard let strongSelf = self else {

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit import UIKit
import AppBundle import AppBundle
import AccountContext import AccountContext
import SwiftSignalKit
import TelegramPresentationData import TelegramPresentationData
import AsyncDisplayKit import AsyncDisplayKit
import Display import Display
@ -19,6 +20,7 @@ public final class WalletWordDisplayScreen: ViewController {
private let wordList: [String] private let wordList: [String]
private let startTime: Double private let startTime: Double
private let idleTimerExtensionDisposable: Disposable
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, wordList: [String]) { public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, wordList: [String]) {
self.context = context self.context = context
@ -32,6 +34,7 @@ public final class WalletWordDisplayScreen: ViewController {
let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor) let navigationBarTheme = NavigationBarTheme(buttonColor: defaultNavigationPresentationData.theme.buttonColor, disabledButtonColor: defaultNavigationPresentationData.theme.disabledButtonColor, primaryTextColor: defaultNavigationPresentationData.theme.primaryTextColor, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: defaultNavigationPresentationData.theme.badgeBackgroundColor, badgeStrokeColor: defaultNavigationPresentationData.theme.badgeStrokeColor, badgeTextColor: defaultNavigationPresentationData.theme.badgeTextColor)
self.startTime = Date().timeIntervalSince1970 self.startTime = Date().timeIntervalSince1970
self.idleTimerExtensionDisposable = context.sharedContext.applicationBindings.pushIdleTimerExtension()
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings)) super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings))
@ -46,6 +49,10 @@ public final class WalletWordDisplayScreen: ViewController {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
deinit {
self.idleTimerExtensionDisposable.dispose()
}
@objc private func backPressed() { @objc private func backPressed() {
self.dismiss() self.dismiss()
} }

View File

@ -741,6 +741,19 @@
<key>explicitFileType</key> <key>explicitFileType</key>
<string>archive.ar</string> <string>archive.ar</string>
</dict> </dict>
<key>1DD70E29D81471E200000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>libUrlHandling.a</string>
<key>path</key>
<string>libUrlHandling.a</string>
<key>sourceTree</key>
<string>BUILT_PRODUCTS_DIR</string>
<key>explicitFileType</key>
<string>archive.ar</string>
</dict>
<key>B401C97968022A5500000000</key> <key>B401C97968022A5500000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -802,6 +815,7 @@
<string>1DD70E29AE67341000000000</string> <string>1DD70E29AE67341000000000</string>
<string>1DD70E2951398CF200000000</string> <string>1DD70E2951398CF200000000</string>
<string>1DD70E29597BAFBB00000000</string> <string>1DD70E29597BAFBB00000000</string>
<string>1DD70E29D81471E200000000</string>
</array> </array>
</dict> </dict>
<key>1DD70E29D097476500000000</key> <key>1DD70E29D097476500000000</key>
@ -920,6 +934,17 @@
<key>sourceTree</key> <key>sourceTree</key>
<string>SOURCE_ROOT</string> <string>SOURCE_ROOT</string>
</dict> </dict>
<key>1DD70E298815219000000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletQrViewScreen.swift</string>
<key>path</key>
<string>Sources/WalletQrViewScreen.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2979DDEBBB00000000</key> <key>1DD70E2979DDEBBB00000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -975,6 +1000,17 @@
<key>sourceTree</key> <key>sourceTree</key>
<string>SOURCE_ROOT</string> <string>SOURCE_ROOT</string>
</dict> </dict>
<key>1DD70E298710C0BD00000000</key>
<dict>
<key>isa</key>
<string>PBXFileReference</string>
<key>name</key>
<string>WalletUtils.swift</string>
<key>path</key>
<string>Sources/WalletUtils.swift</string>
<key>sourceTree</key>
<string>SOURCE_ROOT</string>
</dict>
<key>1DD70E2936794EB600000000</key> <key>1DD70E2936794EB600000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -1014,11 +1050,13 @@
<string>1DD70E29E336006800000000</string> <string>1DD70E29E336006800000000</string>
<string>1DD70E296D49CFFF00000000</string> <string>1DD70E296D49CFFF00000000</string>
<string>1DD70E29DE28A96800000000</string> <string>1DD70E29DE28A96800000000</string>
<string>1DD70E298815219000000000</string>
<string>1DD70E2979DDEBBB00000000</string> <string>1DD70E2979DDEBBB00000000</string>
<string>1DD70E2948FA33F200000000</string> <string>1DD70E2948FA33F200000000</string>
<string>1DD70E2986544B8D00000000</string> <string>1DD70E2986544B8D00000000</string>
<string>1DD70E2964068E1100000000</string> <string>1DD70E2964068E1100000000</string>
<string>1DD70E290467090400000000</string> <string>1DD70E290467090400000000</string>
<string>1DD70E298710C0BD00000000</string>
<string>1DD70E2936794EB600000000</string> <string>1DD70E2936794EB600000000</string>
<string>1DD70E290678D03000000000</string> <string>1DD70E290678D03000000000</string>
</array> </array>
@ -1102,6 +1140,13 @@
<key>fileRef</key> <key>fileRef</key>
<string>1DD70E29DE28A96800000000</string> <string>1DD70E29DE28A96800000000</string>
</dict> </dict>
<key>E7A30F048815219000000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E298815219000000000</string>
</dict>
<key>E7A30F0479DDEBBB00000000</key> <key>E7A30F0479DDEBBB00000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -1137,6 +1182,13 @@
<key>fileRef</key> <key>fileRef</key>
<string>1DD70E290467090400000000</string> <string>1DD70E290467090400000000</string>
</dict> </dict>
<key>E7A30F048710C0BD00000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E298710C0BD00000000</string>
</dict>
<key>E7A30F0436794EB600000000</key> <key>E7A30F0436794EB600000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -1164,11 +1216,13 @@
<string>E7A30F04E336006800000000</string> <string>E7A30F04E336006800000000</string>
<string>E7A30F046D49CFFF00000000</string> <string>E7A30F046D49CFFF00000000</string>
<string>E7A30F04DE28A96800000000</string> <string>E7A30F04DE28A96800000000</string>
<string>E7A30F048815219000000000</string>
<string>E7A30F0479DDEBBB00000000</string> <string>E7A30F0479DDEBBB00000000</string>
<string>E7A30F0448FA33F200000000</string> <string>E7A30F0448FA33F200000000</string>
<string>E7A30F0486544B8D00000000</string> <string>E7A30F0486544B8D00000000</string>
<string>E7A30F0464068E1100000000</string> <string>E7A30F0464068E1100000000</string>
<string>E7A30F040467090400000000</string> <string>E7A30F040467090400000000</string>
<string>E7A30F048710C0BD00000000</string>
<string>E7A30F0436794EB600000000</string> <string>E7A30F0436794EB600000000</string>
<string>E7A30F040678D03000000000</string> <string>E7A30F040678D03000000000</string>
</array> </array>
@ -1530,6 +1584,13 @@
<key>fileRef</key> <key>fileRef</key>
<string>1DD70E29AE67341000000000</string> <string>1DD70E29AE67341000000000</string>
</dict> </dict>
<key>E7A30F04D81471E200000000</key>
<dict>
<key>isa</key>
<string>PBXBuildFile</string>
<key>fileRef</key>
<string>1DD70E29D81471E200000000</string>
</dict>
<key>FAF5FAC90000000000000000</key> <key>FAF5FAC90000000000000000</key>
<dict> <dict>
<key>isa</key> <key>isa</key>
@ -1587,6 +1648,7 @@
<string>E7A30F0481AE180900000000</string> <string>E7A30F0481AE180900000000</string>
<string>E7A30F04524F478E00000000</string> <string>E7A30F04524F478E00000000</string>
<string>E7A30F04AE67341000000000</string> <string>E7A30F04AE67341000000000</string>
<string>E7A30F04D81471E200000000</string>
</array> </array>
<key>name</key> <key>name</key>
<string>Fake Swift Dependencies (Copy Files Phase)</string> <string>Fake Swift Dependencies (Copy Files Phase)</string>