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">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@ -65,6 +67,8 @@
ReferencedContainer = "container:Project.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -167,6 +167,7 @@ public enum ResolvedUrl {
case share(url: String?, text: String?, to: String?)
case wallpaper(WallpaperUrlParameter)
case theme(String)
case wallet(address: String, amount: Int64?, comment: String?)
}
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 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 openAddPersonContact(context: AccountContext, peerId: PeerId, pushController: @escaping (ViewController) -> Void, present: @escaping (ViewController, Any?) -> Void)
func presentContactsWarningSuppression(context: AccountContext, present: (ViewController, Any?) -> Void)
func openWallet(context: AccountContext, walletContext: OpenWalletContext, present: @escaping (ViewController) -> Void)
func navigateToCurrentCall()
var hasOngoingCall: ValuePromise<Bool> { get }

View File

@ -60,11 +60,13 @@ private final class CameraContext {
self.session.startRunning()
}
func stopCapture() {
self.session.beginConfiguration()
self.input.invalidate(for: self.session)
self.output.invalidate(for: self.session)
self.session.commitConfiguration()
func stopCapture(invalidate: Bool = false) {
if invalidate {
self.session.beginConfiguration()
self.input.invalidate(for: self.session)
self.output.invalidate(for: self.session)
self.session.commitConfiguration()
}
self.session.stopRunning()
}
@ -143,10 +145,10 @@ public final class Camera {
}
}
public func stopCapture() {
public func stopCapture(invalidate: Bool = false) {
self.queue.async {
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, *) {
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
}, completion: {
completedEffect = true
intermediateCompletion()
}, completion: {
completedEffect = true
intermediateCompletion()
})
}
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) {
(self.displayNode as! ItemListControllerNode<Entry>).listNode.ensureItemNodeVisible(itemNode)
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true) {
(self.displayNode as! ItemListControllerNode<Entry>).listNode.ensureItemNodeVisible(itemNode, animated: animated)
}
public func afterLayout(_ f: @escaping () -> Void) {

View File

@ -37,12 +37,14 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
let action: (() -> Void)?
let textUpdated: (String) -> Void
let shouldUpdateText: (String) -> Bool
let processPaste: ((String) -> Void)?
let updatedFocus: ((Bool) -> Void)?
let maxLength: ItemListMultilineInputItemTextLimit?
let minimalHeight: CGFloat?
let inlineAction: ItemListMultilineInputInlineAction?
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.text = text
self.placeholder = placeholder
@ -55,6 +57,8 @@ public class ItemListMultilineInputItem: ListViewItem, ItemListItem {
self.minimalHeight = minimalHeight
self.textUpdated = textUpdated
self.shouldUpdateText = shouldUpdateText
self.processPaste = processPaste
self.updatedFocus = updatedFocus
self.tag = tag
self.action = action
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)))
}
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 {
if let item = self.item {
if text.count > 1, let processPaste = item.processPaste {
processPaste(text)
return false
}
if let action = item.action, text == "\n" {
action()
return false
}
var newText: String = editableTextNode.textView.text
newText.replaceSubrange(newText.index(newText.startIndex, offsetBy: range.lowerBound) ..< newText.index(newText.startIndex, offsetBy: range.upperBound), with: text)
let newText = (editableTextNode.textView.text as NSString).replacingCharacters(in: range, with: text)
if !item.shouldUpdateText(newText) {
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 {
if let item = self.item {
var newText = textField.text ?? ""
newText.replaceSubrange(newText.index(newText.startIndex, offsetBy: range.lowerBound) ..< newText.index(newText.startIndex, offsetBy: range.upperBound), with: string)
let newText = ((textField.text ?? "") as NSString).replacingCharacters(in: range, with: string)
if !item.shouldUpdateText(newText) {
return false
}

View File

@ -66,7 +66,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
_backgroundView.image = backgroundImage;
[_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.font = [TGFont roundedFontOfSize:17];
_countLabel.text = [TGStringUtils stringWithLocalizedNumber:0];
@ -292,7 +292,7 @@ const CGFloat TGPhotoCounterButtonMaskFade = 18;
if (sizeToFit)
[_countLabel sizeToFit];
CGFloat labelWidth = CGRound(_countLabel.frame.size.width);
CGFloat labelWidth = ceilf(_countLabel.frame.size.width);
CGFloat labelOrigin = 0.0f;
if (![self _useRtlLayout])

View File

@ -10,7 +10,7 @@ public enum QrCodeIcon {
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
if let data = string.data(using: .isoLatin1, allowLossyConversion: false), let filter = CIFilter(name: "CIQRCodeGenerator") {
filter.setValue(data, forKey: "inputMessage")
@ -25,7 +25,7 @@ public func qrCode(string: String, color: UIColor, backgroundColor: UIColor? = n
}
|> map { inputImage 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 fittedSize = arguments.imageSize.aspectFilled(arguments.boundingSize).fitted(arguments.imageSize)

View File

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

View File

@ -38,10 +38,10 @@ public final class ShareProxyServerActionSheetController: ActionSheetController
}))
items.append(ActionSheetButtonItem(title: strings.SocksProxySetup_ShareQRCode, action: { [weak self] in
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
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()
}
|> deliverOnMainQueue).start(next: { image in

View File

@ -12,6 +12,7 @@ import AccountContext
import ShareController
import SearchBarNode
import SearchUI
import ActivityIndicator
private enum LanguageListSection: ItemListSectionId {
case official
@ -194,12 +195,12 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController
if self.hasValidLayout {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
self.dequeueTransitions()
}
}
}
private func dequeueTransition() {
private func dequeueTransitions() {
if let transition = self.enqueuedTransitions.first {
self.enqueuedTransitions.remove(at: 0)
@ -248,7 +249,7 @@ private final class LocalizationListSearchContainerNode: SearchDisplayController
if !self.hasValidLayout {
self.hasValidLayout = true
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
self.dequeueTransitions()
}
}
}
@ -265,17 +266,18 @@ private struct LanguageListNodeTransition {
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let firstTime: Bool
let isLoading: 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 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 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 {
@ -292,7 +294,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
private var containerLayout: (ContainerViewLayout, CGFloat)?
let listNode: ListView
private var queuedTransitions: [LanguageListNodeTransition] = []
private var activityIndicator: ActivityIndicator?
private var searchDisplayController: SearchDisplayController?
private let presentationDataValue = Promise<(PresentationTheme, PresentationStrings)>()
@ -408,7 +410,7 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
}
}
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)
})
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 })
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 {
self.dequeueTransitions()
}
@ -483,26 +490,38 @@ final class LocalizationListControllerNode: ViewControllerTracingNode {
}
private func dequeueTransitions() {
if self.containerLayout != nil {
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
var options = ListViewDeleteAndInsertOptions()
if transition.firstTime {
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.animated {
options.insert(.AnimateInsertion)
}
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)
}
}
})
guard let (layout, navigationBarHeight) = self.containerLayout else {
return
}
while !self.queuedTransitions.isEmpty {
let transition = self.queuedTransitions.removeFirst()
var options = ListViewDeleteAndInsertOptions()
if transition.firstTime {
options.insert(.Synchronous)
options.insert(.LowLatency)
} else if transition.animated {
options.insert(.AnimateInsertion)
}
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
case .passport:
return PresentationResourcesSettings.passport
case .wallet:
return PresentationResourcesSettings.wallet
case .support:
return PresentationResourcesSettings.support
case .faq:

View File

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

View File

@ -574,12 +574,12 @@ private func settingsEntries(account: Account, presentationData: PresentationDat
let languageName = presentationData.strings.primaryComponent.localizedName
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 {
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 {
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()
|> deliverOnMainQueue
|> take(1)).start(next: { context in
openWallet(context: context, push: { c in
context.sharedContext.openWallet(context: context, walletContext: .generic, present: { c in
pushControllerImpl?(c)
})
})
@ -1098,8 +1098,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
)
)
let hasWallet = .single(false)
|> then(contextValue.get()
let hasWallet = contextValue.get()
|> mapToSignal { context in
return context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|> map { view -> Bool in
@ -1107,7 +1106,7 @@ public func settingsController(context: AccountContext, accountManager: AccountM
let configuration = WalletConfiguration.with(appConfiguration: appConfiguration)
return configuration.enabled
}
})
}
let hasPassport = ValuePromise<Bool>(false)
let updatePassport: () -> Void = {
@ -1618,36 +1617,3 @@ private func accountContextMenuItems(context: AccountContext, logout: @escaping
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 _ = (updatePresentationThemeSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager, { current in
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
var themeSpecificChatWallpapers = current.themeSpecificChatWallpapers
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)
}) |> 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 appearance = UIImage(bundleImageName: "Settings/MenuIcons/Appearance")?.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 watch = UIImage(bundleImageName: "Settings/MenuIcons/Watch")?.precomposed()

View File

@ -20,6 +20,7 @@ public func normalizeArabicNumeralString(_ string: String, type: ArabicNumeralSt
("7", "٧", "۷"),
("8", "٨", "۸"),
("9", "٩", "۹"),
(",", "٫", "٫")
]
for (western, arabic, persian) in numerals {
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 SettingsUI
import AppBundle
import WalletUI
import UrlHandling
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: {})
} else if let confirmationCode = parseConfirmationCodeUrl(url) {
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 MessageReactionListUI
import AppBundle
import WalletUI
public enum ChatControllerPeekActions {
case standard
@ -5078,8 +5079,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
}, recognizedQRCode: { [weak self] code in
if let strongSelf = self, let (host, port, username, password, secret) = parseProxyUrl(code) {
strongSelf.openResolved(ResolvedUrl.proxy(host: host, port: port, username: username, password: password, secret: secret))
if let strongSelf = self {
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
guard let strongSelf = self else {

View File

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

View File

@ -324,5 +324,9 @@ func openResolvedUrlImpl(_ resolvedUrl: ResolvedUrl, context: AccountContext, ur
controller?.dismiss()
}))
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 PassportUI
import UrlHandling
import WalletUI
public struct ParsedSecureIdUrl {
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) {
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
}

View File

@ -14,6 +14,7 @@ import PeersNearbyUI
import PeerInfoUI
import SettingsUI
import UrlHandling
import WalletUI
private enum CallStatusText: Equatable {
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 {
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

View File

@ -372,6 +372,11 @@ public func parseWallpaperUrl(_ url: String) -> WallpaperUrlParameter? {
}
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 baseTelegramMePaths = ["telegram.me", "t.me"]
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/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/GlassButtonNode:GlassButtonNode",
"//submodules/UrlHandling:UrlHandling",
],
frameworks = [
"$SDKROOT/System/Library/Frameworks/Foundation.framework",

View File

@ -115,13 +115,15 @@ final class WalletInfoEmptyItemNode: ListViewItemNode {
let title = "Wallet Created"
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
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

View File

@ -513,7 +513,7 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.headerNode = WalletInfoHeaderNode(account: account, theme: presentationData.theme, sendAction: sendAction, receiveAction: receiveAction)
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.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 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 {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode

View File

@ -12,12 +12,17 @@ class WalletQrCodeItem: ListViewItem, ItemListItem {
let address: String
let sectionId: ItemListSectionId
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.address = address
self.sectionId = sectionId
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) {
@ -55,10 +60,6 @@ class WalletQrCodeItem: ListViewItem, ItemListItem {
}
class WalletQrCodeItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let imageNode: TransformImageNode
private var item: WalletQrCodeItem?
@ -68,16 +69,6 @@ class WalletQrCodeItemNode: ListViewItemNode {
}
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()
super.init(layerBacked: false, dynamicBounce: false)
@ -85,6 +76,37 @@ class WalletQrCodeItemNode: ListViewItemNode {
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) {
let makeImageLayout = self.imageNode.asyncLayout()
@ -98,7 +120,7 @@ class WalletQrCodeItemNode: ListViewItemNode {
updatedTheme = item.theme
}
if currentItem?.address != item.address {
if currentItem?.address != item.address || updatedTheme != nil {
updatedAddress = item.address
}
@ -106,22 +128,16 @@ class WalletQrCodeItemNode: ListViewItemNode {
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
let inset: CGFloat = 12.0
var imageSize = CGSize(width: 256.0, height: 256.0)
let inset: CGFloat = 0.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 itemBackgroundColor: UIColor
let itemSeparatorColor: UIColor
switch item.style {
case .plain:
itemBackgroundColor = item.theme.list.plainBackgroundColor
itemSeparatorColor = item.theme.list.itemPlainSeparatorColor
contentSize = CGSize(width: params.width, height: imageSize.height + inset * 2.0)
contentSize = CGSize(width: params.width, height: imageSize.height + 30.0)
insets = itemListNeighborsPlainInsets(neighbors)
case .blocks:
itemBackgroundColor = item.theme.list.itemBlocksBackgroundColor
itemSeparatorColor = item.theme.list.itemBlocksSeparatorColor
contentSize = CGSize(width: params.width, height: imageSize.height + inset * 2.0)
contentSize = CGSize(width: params.width, height: imageSize.height + 30.0)
insets = itemListNeighborsGroupedInsets(neighbors)
}
@ -131,69 +147,13 @@ class WalletQrCodeItemNode: ListViewItemNode {
if let strongSelf = self {
strongSelf.item = item
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor
strongSelf.backgroundNode.backgroundColor = itemBackgroundColor
}
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 leftInset: CGFloat
switch item.style {
case .plain:
leftInset = 35.0 + params.leftInset
if strongSelf.backgroundNode.supernode != nil {
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)
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: (params.width - imageSize.width) / 2.0, y: 0.0), size: imageSize)
}
})
}

View File

@ -9,6 +9,7 @@ import SwiftSignalKit
import TelegramCore
import Camera
import GlassButtonNode
import UrlHandling
private func generateFrameImage() -> UIImage? {
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 {
private let context: AccountContext
private let completion: (String, Int64?, String?) -> Void
private let completion: (ParsedWalletUrl) -> Void
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.completion = completion
@ -70,6 +72,14 @@ public final class WalletQrScanScreen: ViewController {
self.navigationBar?.intrinsicCanTransitionInline = false
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) {
@ -77,7 +87,8 @@ public final class WalletQrScanScreen: ViewController {
}
deinit {
self.disposable?.dispose()
self.codeDisposable?.dispose()
self.inForegroundDisposable?.dispose()
}
@objc private func backPressed() {
@ -89,12 +100,7 @@ public final class WalletQrScanScreen: ViewController {
self.displayNodeDidLoad()
// (self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
// |> map { code -> String? in
// return code?.message
// } |> distinctUntilChanged
self.disposable = (((self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
self.codeDisposable = (((self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
|> map { code -> String? in
return code?.message
}
@ -106,8 +112,9 @@ public final class WalletQrScanScreen: ViewController {
guard let strongSelf = self, let code = code else {
return
}
let cleanString = code.replacingOccurrences(of: "ton://", with: "")
strongSelf.completion(cleanString, nil, nil)
if let url = URL(string: code), let parsedWalletUrl = parseWalletUrl(url) {
strongSelf.completion(parsedWalletUrl)
}
})
}
@ -128,6 +135,7 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
private let leftDimNode: ASDisplayNode
private let rightDimNode: ASDisplayNode
private let frameNode: ASImageNode
private let galleryButtonNode: GlassButtonNode
private let torchButtonNode: GlassButtonNode
private let titleNode: ImmediateTextNode
@ -168,6 +176,7 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
self.frameNode = ASImageNode()
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.titleNode = ImmediateTextNode()
@ -189,14 +198,25 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
self.addSubnode(self.leftDimNode)
self.addSubnode(self.rightDimNode)
self.addSubnode(self.frameNode)
self.addSubnode(self.galleryButtonNode)
self.addSubnode(self.torchButtonNode)
self.addSubnode(self.titleNode)
self.galleryButtonNode.addTarget(self, action: #selector(self.galleryPressed), forControlEvents: .touchUpInside)
self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside)
}
deinit {
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() {
@ -272,14 +292,19 @@ private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollV
transition.updateFrame(node: self.frameNode, frame: dimRect.insetBy(dx: -2.0, dy: -2.0))
let torchButtonSize = 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))
let buttonSize = CGSize(width: 72.0, height: 72.0)
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 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)
}
@objc private func galleryPressed() {
}
@objc private func torchPressed() {
self.torchButtonNode.isSelected = !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 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 {
let context: AccountContext
let updateState: ((WalletReceiveScreenState) -> WalletReceiveScreenState) -> Void
let updateText: (WalletReceiveScreenEntryTag, String) -> Void
let selectNextInputItem: (WalletReceiveScreenEntryTag) -> Void
let dismissInput: () -> Void
let copyAddress: () -> 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.updateState = updateState
self.updateText = updateText
self.selectNextInputItem = selectNextInputItem
self.dismissInput = dismissInput
self.copyAddress = copyAddress
self.shareAddressLink = shareAddressLink
self.openQrCode = openQrCode
self.displayQrCodeContextMenu = displayQrCodeContextMenu
self.scrollToBottom = scrollToBottom
}
}
private enum WalletReceiveScreenSection: Int32 {
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 {
case addressHeader(PresentationTheme, String)
case addressCode(PresentationTheme, String)
case address(PresentationTheme, String)
case addressHeader(PresentationTheme, String)
case address(PresentationTheme, String, Bool)
case copyAddress(PresentationTheme, String)
case shareAddressLink(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 {
switch self {
case .addressHeader, .addressCode, .address, .copyAddress, .shareAddressLink, .addressInfo:
case .addressCode, .addressHeader, .address, .copyAddress, .shareAddressLink, .addressInfo:
return WalletReceiveScreenSection.address.rawValue
case .amountHeader, .amount:
return WalletReceiveScreenSection.amount.rawValue
case .commentHeader, .comment:
return WalletReceiveScreenSection.comment.rawValue
}
}
var stableId: Int32 {
switch self {
case .addressHeader:
return 0
case .addressCode:
return 0
case .addressHeader:
return 1
case .address:
return 2
@ -63,25 +94,33 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
return 4
case .addressInfo:
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 {
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):
if case let .addressCode(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress {
return true
} else {
return false
}
case let .address(lhsTheme, lhsAddress):
if case let .address(rhsTheme, rhsAddress) = rhs, lhsTheme === rhsTheme, lhsAddress == rhsAddress {
case let .addressHeader(lhsTheme, lhsText):
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
} else {
return false
@ -104,6 +143,30 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
} else {
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 {
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):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .addressCode(theme, address):
return WalletQrCodeItem(theme: theme, address: "ton://\(address)", 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 .address(theme, text, monospace):
return ItemListMultilineTextItem(theme: theme, text: text, enabledEntityTypes: [], font: monospace ? .monospace : .default, sectionId: self.section, style: .blocks)
case let .copyAddress(theme, text):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.copyAddress()
@ -129,18 +196,97 @@ private enum WalletReceiveScreenEntry: ItemListNodeEntry {
})
case let .addressInfo(theme, text):
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] = []
entries.append(.addressHeader(presentationData.theme, "YOUR WALLET ADDRESS"))
entries.append(.addressCode(presentationData.theme, address))
entries.append(.address(presentationData.theme, formatAddress(address)))
entries.append(.copyAddress(presentationData.theme, "Copy Wallet Address"))
entries.append(.shareAddressLink(presentationData.theme, "Share Wallet Address"))
entries.append(.addressCode(presentationData.theme, invoiceUrl(address: address, state: state, escapeComment: true)))
entries.append(.addressHeader(presentationData.theme, state.isEmpty ? "YOUR WALLET ADDRESS" : "INVOICE URL"))
let addressText: String
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."))
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
}
@ -152,42 +298,176 @@ private final class WalletReceiveScreenImpl: ItemListController<WalletReceiveScr
}
func walletReceiveScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String) -> ViewController {
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var dismissImpl: (() -> Void)?
let arguments = WalletReceiveScreenArguments(context: context, copyAddress: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
UIPasteboard.general.string = address
private func invoiceUrl(address: String, state: WalletReceiveScreenState, escapeComment: Bool = true) -> String {
let escapedAddress = address.replacingOccurrences(of: "+", with: "-").replacingOccurrences(of: "/", with: "_")
var arguments = ""
if !state.amount.isEmpty {
arguments += arguments.isEmpty ? "/?" : "&"
arguments += "amount=\(amountValue(state.amount))"
}
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: {
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)
}, 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, address)
|> map { presentationData, address -> (ItemListControllerState, (ItemListNodeState<WalletReceiveScreenEntry>, WalletReceiveScreenEntry.ItemGenerationArguments)) in
let signal = combineLatest(queue: .mainQueue(), context.sharedContext.presentationData, statePromise.get())
|> map { presentationData, state -> (ItemListControllerState, (ItemListNodeState<WalletReceiveScreenEntry>, WalletReceiveScreenEntry.ItemGenerationArguments)) in
let rightNavigationButton = ItemListNavigationButton(content: .text(presentationData.strings.Common_Done), style: .regular, enabled: true, action: {
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 listState = ItemListNodeState(entries: walletReceiveScreenEntries(presentationData: presentationData, address: address), style: .blocks, 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, state: state), style: .blocks, ensureVisibleItemTag: ensureVisibleItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}
let controller = WalletReceiveScreenImpl(context: context, state: signal)
controller.navigationPresentation = .modal
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
controller.experimentalSnapScrollToItem = true
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}
pushImpl = { [weak controller] c in
controller?.push(c)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
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
}

View File

@ -11,21 +11,27 @@ import ItemListUI
import SwiftSignalKit
import AlertUI
import TextFormat
import DeviceAccess
import TelegramStringFormatting
import UrlHandling
private let walletAddressLength: Int = 48
private let balanceIcon = UIImage(bundleImageName: "Wallet/TransactionGem")?.precomposed()
private final class WalletSendScreenArguments {
let context: AccountContext
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void
let updateText: (WalletSendScreenEntryTag, String) -> Void
let selectNextInputItem: (WalletSendScreenEntryTag) -> Void
let dismissInput: () -> Void
let openQrScanner: () -> 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.updateState = updateState
self.updateText = updateText
self.selectNextInputItem = selectNextInputItem
self.dismissInput = dismissInput
self.openQrScanner = openQrScanner
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 {
case addressHeader(PresentationTheme, String)
case address(PresentationTheme, String, String)
case addressInfo(PresentationTheme, String)
case amountHeader(PresentationTheme, String, String?, Bool)
case amount(PresentationTheme, PresentationStrings, String, String)
case commentHeader(PresentationTheme, String)
case comment(PresentationTheme, String, String)
@ -180,8 +122,8 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
} 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 {
case let .amount(lhsTheme, lhsStrings, lhsPlaceholder, lhsAmount):
if case let .amount(rhsTheme, rhsStrings, rhsPlaceholder, rhsAmount) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsPlaceholder == rhsPlaceholder, lhsAmount == rhsAmount {
return true
} else {
return false
@ -210,14 +152,44 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
case let .addressHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
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
arguments.updateState { state in
var state = state
state.address = address.replacingOccurrences(of: "\n", with: "")
return state
}
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.updateText(WalletSendScreenEntryTag.address, text.replacingOccurrences(of: "\n", with: ""))
}, shouldUpdateText: { text in
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: {
arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
}, 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)
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
arguments.updateState { state in
var state = state
state.amount = text
return state
}
let text = formatAmountText(text, decimalSeparator: arguments.context.sharedContext.currentPresentationData.with { $0 }.dateTimeFormat.decimalSeparator)
arguments.updateText(WalletSendScreenEntryTag.amount, text)
}, shouldUpdateText: { text in
return isValidAmount(text)
}, processPaste: { pastedText in
if isValidAmount(pastedText) {
return normalizedStringForGramsString(pastedText)
let presentationData = arguments.context.sharedContext.currentPresentationData.with { $0 }
return normalizedStringForGramsString(pastedText, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
} else {
return text
}
@ -259,12 +229,8 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
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: .send, textUpdated: { comment in
arguments.updateState { state in
var state = state
state.text = comment
return state
}
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.updateText(WalletSendScreenEntryTag.comment, text)
}, tag: WalletSendScreenEntryTag.comment, action: {
arguments.proceed()
})
@ -275,7 +241,7 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
private struct WalletSendScreenState: Equatable {
var address: String
var amount: String
var text: String
var comment: String
}
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(.amount(presentationData.theme, presentationData.strings, "Grams to send", state.amount ?? ""))
entries.append(.commentHeader(presentationData.theme, "COMMENT"))
entries.append(.comment(presentationData.theme, "Optional description of the payment", state.text))
entries.append(.commentHeader(presentationData.theme, "COMMENT (OPTIONAL)"))
entries.append(.comment(presentationData.theme, "Description of the payment", state.comment))
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 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 stateValue = Atomic(value: initialState)
let updateState: ((WalletSendScreenState) -> WalletSendScreenState) -> Void = { f in
@ -319,36 +285,63 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)?
var ensureItemVisibleImpl: ((WalletSendScreenEntryTag) -> Void)?
let arguments = WalletSendScreenArguments(context: context, updateState: { f in
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
selectNextInputItemImpl?(tag)
}, dismissInput: {
dismissInputImpl?()
}, openQrScanner: {
dismissInputImpl?()
pushImpl?(WalletQrScanScreen(context: context, completion: { address, amount, comment in
var updatedState: WalletSendScreenState?
updateState { state in
var state = state
state.address = address
if let amount = amount {
state.amount = formatAmountText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
}
if let comment = comment {
state.text = comment
}
updatedState = state
return state
DeviceAccess.authorizeAccess(to: .camera, presentationData: presentationData, present: { c, a in
presentControllerImpl?(c, a)
}, openSettings: {
context.sharedContext.applicationBindings.openSettings()
}, { granted in
guard granted else {
return
}
popImpl?()
if let updatedState = updatedState {
if updatedState.amount.isEmpty {
selectNextInputItemImpl?(WalletSendScreenEntryTag.address)
} else if updatedState.text.isEmpty {
selectNextInputItemImpl?(WalletSendScreenEntryTag.amount)
pushImpl?(WalletQrScanScreen(context: context, completion: { parsedUrl in
var updatedState: WalletSendScreenState?
updateState { state in
var state = state
state.address = parsedUrl.address
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: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let state = stateValue.with { $0 }
@ -356,7 +349,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
updateState { state in
var state = state
state.amount = formatAmountText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
state.amount = formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
return state
}
@ -375,7 +368,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
dismissAlertImpl?(true)
}), TextAlertAction(type: .defaultAction, title: "Confirm", action: {
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)
presentInGlobalOverlayImpl?(controller, nil)
@ -419,7 +412,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
let amount = amountValue(state.amount)
var sendEnabled = false
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: {
arguments.proceed()
@ -432,7 +425,8 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
}
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
controller?.present(c, in: .window(.root), with: a)
}
@ -473,5 +467,28 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
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
}

View File

@ -17,10 +17,12 @@ import TelegramStringFormatting
private final class WalletTransactionInfoControllerArguments {
let copyWalletAddress: () -> 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.sendGrams = sendGrams
self.displayContextMenu = displayContextMenu
}
}
@ -30,6 +32,18 @@ private enum WalletTransactionInfoSection: Int32 {
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 {
case amount(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, WalletTransaction)
case infoHeader(PresentationTheme, String)
@ -92,7 +106,9 @@ private enum WalletTransactionInfoEntry: ItemListNodeEntry {
case let .commentHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
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
}
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] {
var entries: [WalletTransactionInfoEntry] = []
@ -203,6 +213,7 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
var dismissImpl: (() -> Void)?
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var displayContextMenuImpl: ((WalletTransactionInfoEntryTag, String) -> Void)?
let arguments = WalletTransactionInfoControllerArguments(copyWalletAddress: {
let address = extractAddress(walletTransaction)
@ -217,6 +228,8 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
dismissImpl?()
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())
@ -242,6 +255,35 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
pushImpl = { [weak controller] c in
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
}

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 clearButtonNode: HighlightableButtonNode
public private(set) var isLast: Bool
var text: String {
get {
return self.inputNode.textField.text ?? ""
@ -2263,6 +2265,7 @@ private final class WordCheckInputNode: ASDisplayNode, UITextFieldDelegate {
self.next = next
self.focused = focused
self.pasteWords = pasteWords
self.isLast = isLast
self.backgroundNode = ASImageNode()
self.backgroundNode.displaysAsynchronously = false
@ -2710,7 +2713,13 @@ private final class WalletWordCheckScreenNode: ViewControllerTracingNode, UIScro
guard let strongSelf = self else {
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
guard let strongSelf = self else {

View File

@ -2,6 +2,7 @@ import Foundation
import UIKit
import AppBundle
import AccountContext
import SwiftSignalKit
import TelegramPresentationData
import AsyncDisplayKit
import Display
@ -19,6 +20,7 @@ public final class WalletWordDisplayScreen: ViewController {
private let wordList: [String]
private let startTime: Double
private let idleTimerExtensionDisposable: Disposable
public init(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, wordList: [String]) {
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)
self.startTime = Date().timeIntervalSince1970
self.idleTimerExtensionDisposable = context.sharedContext.applicationBindings.pushIdleTimerExtension()
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")
}
deinit {
self.idleTimerExtensionDisposable.dispose()
}
@objc private func backPressed() {
self.dismiss()
}

View File

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