Wallet QR code scanner and other improvements

This commit is contained in:
Ilya Laktyushin
2019-09-25 00:00:49 +03:00
78 changed files with 2714 additions and 429 deletions

View File

@@ -0,0 +1,22 @@
import Foundation
import TelegramCore
public struct WalletConfiguration {
static var defaultValue: WalletConfiguration {
return WalletConfiguration(enabled: false)
}
public let enabled: Bool
fileprivate init(enabled: Bool) {
self.enabled = enabled
}
public static func with(appConfiguration: AppConfiguration) -> WalletConfiguration {
if let data = appConfiguration.data, let enabled = data["wallet_enabled"] as? Bool {
return WalletConfiguration(enabled: enabled)
} else {
return .defaultValue
}
}
}

View File

@@ -17,6 +17,7 @@ public final class WalletInfoScreen: ViewController {
private let tonContext: TonContext
private let walletInfo: WalletInfo
private let address: String
private let walletState = Promise<WalletState?>()
private var presentationData: PresentationData
@@ -44,6 +45,8 @@ public final class WalletInfoScreen: ViewController {
self.scrollToTop = { [weak self] in
(self?.displayNode as? WalletInfoScreenNode)?.scrollToTop()
}
self.walletState.set(Signal.single(nil) |> then(getWalletState(address: address, tonInstance: tonContext.instance) |> map(Optional.init)))
}
required init(coder aDecoder: NSCoder) {
@@ -59,11 +62,18 @@ public final class WalletInfoScreen: ViewController {
}
override public func loadDisplayNode() {
self.displayNode = WalletInfoScreenNode(account: self.context.account, tonContext: self.tonContext, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, sendAction: { [weak self] in
self.displayNode = WalletInfoScreenNode(account: self.context.account, tonContext: self.tonContext, presentationData: self.presentationData, walletInfo: self.walletInfo, address: self.address, walletState: self.walletState.get(), sendAction: { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.push(walletSendScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: strongSelf.walletInfo))
let _ = (strongSelf.walletState.get()
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] walletState in
guard let strongSelf = self else {
return
}
strongSelf.push(walletSendScreen(context: strongSelf.context, tonContext: strongSelf.tonContext, walletInfo: strongSelf.walletInfo, walletState: walletState))
})
}, receiveAction: { [weak self] in
guard let strongSelf = self else {
return
@@ -87,8 +97,8 @@ public final class WalletInfoScreen: ViewController {
}
private final class WalletInfoBalanceNode: ASDisplayNode {
private let balanceTextNode: ImmediateTextNode
private let balanceIconNode: ASImageNode
let balanceTextNode: ImmediateTextNode
let balanceIconNode: ASImageNode
var balance: String = " " {
didSet {
@@ -96,6 +106,8 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
}
}
var isLoading: Bool = true
init(theme: PresentationTheme) {
self.balanceTextNode = ImmediateTextNode()
self.balanceTextNode.displaysAsynchronously = false
@@ -123,9 +135,14 @@ private final class WalletInfoBalanceNode: ASDisplayNode {
let balanceOrigin = CGPoint(x: floor((width - balanceTextSize.width - balanceIconSpacing - balanceIconSize.width / 2.0) / 2.0), y: 0.0)
let balanceTextFrame = CGRect(origin: balanceOrigin, size: balanceTextSize)
let balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
let balanceIconFrame: CGRect
if self.isLoading {
balanceIconFrame = CGRect(origin: CGPoint(x: floor((width - balanceIconSize.width) / 2.0), y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
} else {
balanceIconFrame = CGRect(origin: CGPoint(x: balanceTextFrame.maxX + balanceIconSpacing, y: balanceTextFrame.minY + floor((balanceTextFrame.height - balanceIconSize.height) / 2.0)), size: balanceIconSize)
}
transition.updateFrameAdditive(node: self.balanceTextNode, frame: balanceTextFrame)
transition.updateFrameAdditive(node: self.balanceIconNode, frame: balanceIconFrame)
transition.updateFrame(node: self.balanceIconNode, frame: balanceIconFrame)
return balanceTextSize.height
}
@@ -245,10 +262,10 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
}
}
if self.balance == nil {
self.balanceNode.isHidden = true
self.balanceNode.balanceTextNode.isHidden = true
self.balanceSubtitleNode.isHidden = true
} else {
self.balanceNode.isHidden = false
self.balanceNode.balanceTextNode.isHidden = false
self.balanceSubtitleNode.isHidden = false
}
transition.updateFrame(node: self.receiveButtonNode, frame: receiveButtonFrame)
@@ -272,7 +289,8 @@ private final class WalletInfoHeaderNode: ASDisplayNode {
func animateIn() {
self.sendButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.receiveButtonNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceNode.isLoading = false
self.balanceNode.balanceTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.balanceSubtitleNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
@@ -348,6 +366,7 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
private var presentationData: PresentationData
private let walletInfo: WalletInfo
private let address: String
private let openTransaction: (WalletTransaction) -> Void
private let headerNode: WalletInfoHeaderNode
@@ -366,7 +385,9 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
private var currentEntries: [WalletInfoListEntry]?
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
private var isReady: Bool = false
init(account: Account, tonContext: TonContext, presentationData: PresentationData, walletInfo: WalletInfo, address: String, walletState: Signal<WalletState?, NoError>, sendAction: @escaping () -> Void, receiveAction: @escaping () -> Void, openTransaction: @escaping (WalletTransaction) -> Void) {
self.account = account
self.tonContext = tonContext
self.presentationData = presentationData
@@ -379,24 +400,29 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.listNode = ListView()
self.listNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor
self.listNode.verticalScrollIndicatorFollowsOverscroll = true
self.listNode.isHidden = true
super.init()
self.backgroundColor = .white
self.balanceDisposable.set((currentWalletBalance(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
self.balanceDisposable.set((walletState
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
guard let strongSelf = self, let value = value else {
return
}
let firstTime = strongSelf.headerNode.balance == nil
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.rawValue), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
strongSelf.headerNode.balance = max(0, value.rawValue)
strongSelf.headerNode.balanceNode.balance = formatBalanceText(max(0, value.balance), decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)
strongSelf.headerNode.balance = max(0, value.balance)
if let (layout, navigationHeight) = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate)
}
if firstTime {
strongSelf.headerNode.animateIn()
let wasReady = strongSelf.isReady
strongSelf.isReady = strongSelf.headerNode.balance != nil && strongSelf.currentEntries != nil
if strongSelf.isReady && !wasReady {
strongSelf.animateReadyIn()
}
}))
@@ -412,7 +438,9 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
strongSelf.listOffset = offset
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
if strongSelf.isReady {
strongSelf.headerNode.update(size: strongSelf.headerNode.bounds.size, navigationHeight: navigationHeight, offset: offset, transition: listTransition)
}
}
self.listNode.visibleBottomContentOffsetChanged = { [weak self] offset in
@@ -448,6 +476,11 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.refreshTransactions()
}
deinit {
self.balanceDisposable.dispose()
self.transactionListDisposable.dispose()
}
func scrollToHideHeader() {
guard let (_, navigationHeight) = self.validLayout else {
return
@@ -466,11 +499,22 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
let headerHeight: CGFloat = navigationHeight + 260.0
let topInset: CGFloat = headerHeight
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: headerHeight))
transition.updateFrame(node: self.headerNode, frame: headerFrame)
self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: self.listOffset ?? 0.0, transition: transition)
let visualHeaderHeight: CGFloat
let visualHeaderOffset: CGFloat
if !self.isReady {
visualHeaderHeight = layout.size.height
visualHeaderOffset = visualHeaderHeight
} else {
visualHeaderHeight = headerHeight
visualHeaderOffset = self.listOffset ?? 0.0
}
let visualListOffset = visualHeaderHeight - headerHeight
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: layout.size))
let headerFrame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: visualHeaderHeight))
transition.updateFrame(node: self.headerNode, frame: headerFrame)
self.headerNode.update(size: headerFrame.size, navigationHeight: navigationHeight, offset: visualHeaderOffset, transition: transition)
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(x: 0.0, y: visualListOffset), size: layout.size))
var duration: Double = 0.0
var curve: UInt = 0
@@ -601,8 +645,11 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.enqueuedTransactions.append(transaction)
self.dequeueTransaction()
if isFirst {
self.listNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let wasReady = self.isReady
self.isReady = self.headerNode.balance != nil && self.currentEntries != nil
if self.isReady && !wasReady {
self.animateReadyIn()
}
}
@@ -621,6 +668,14 @@ private final class WalletInfoScreenNode: ViewControllerTracingNode {
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
private func animateReadyIn() {
self.listNode.isHidden = false
self.headerNode.animateIn()
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.5, curve: .spring))
}
}
}
func formatBalanceText(_ value: Int64, decimalSeparator: String) -> String {

View File

@@ -5,26 +5,66 @@ import AccountContext
import TelegramPresentationData
import AsyncDisplayKit
import Display
import Postbox
import SwiftSignalKit
import TelegramCore
import AlertUI
import Camera
import GlassButtonNode
private func generateFrameImage() -> UIImage? {
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(4.0)
context.setLineCap(.round)
var path = CGMutablePath();
path.move(to: CGPoint(x: 2.0, y: 2.0 + 26.0))
path.addArc(tangent1End: CGPoint(x: 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0)
path.addLine(to: CGPoint(x: 2.0 + 26.0, y: 2.0))
context.addPath(path)
context.strokePath()
path.move(to: CGPoint(x: size.width - 2.0, y: 2.0 + 26.0))
path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: 2.0), radius: 6.0)
path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: 2.0))
context.addPath(path)
context.strokePath()
path.move(to: CGPoint(x: 2.0, y: size.height - 2.0 - 26.0))
path.addArc(tangent1End: CGPoint(x: 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0)
path.addLine(to: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0))
context.addPath(path)
context.strokePath()
path.move(to: CGPoint(x: size.width - 2.0, y: size.height - 2.0 - 26.0))
path.addArc(tangent1End: CGPoint(x: size.width - 2.0, y: size.height - 2.0), tangent2End: CGPoint(x: 2.0 + 26.0, y: size.height - 2.0), radius: 6.0)
path.addLine(to: CGPoint(x: size.width - 2.0 - 26.0, y: size.height - 2.0))
context.addPath(path)
context.strokePath()
})?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32)
}
public final class WalletQrScanScreen: ViewController {
private let context: AccountContext
private let tonContext: TonContext
private let completion: (String, Int64?, String?) -> Void
private var presentationData: PresentationData
public init(context: AccountContext, tonContext: TonContext) {
private var disposable: Disposable?
public init(context: AccountContext, completion: @escaping (String, Int64?, String?) -> Void) {
self.context = context
self.tonContext = tonContext
self.completion = completion
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)
let navigationBarTheme = NavigationBarTheme(buttonColor: .white, disabledButtonColor: .white, primaryTextColor: .white, backgroundColor: .clear, separatorColor: .clear, badgeBackgroundColor: .clear, badgeStrokeColor: .clear, badgeTextColor: .clear)
super.init(navigationBarPresentationData: NavigationBarPresentationData(theme: navigationBarTheme, strings: defaultNavigationPresentationData.strings))
self.statusBar.statusBarStyle = .White
self.navigationPresentation = .modalInLargeLayout
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.navigationBar?.intrinsicCanTransitionInline = false
@@ -36,6 +76,10 @@ public final class WalletQrScanScreen: ViewController {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.disposable?.dispose()
}
@objc private func backPressed() {
self.dismiss()
}
@@ -44,6 +88,27 @@ public final class WalletQrScanScreen: ViewController {
self.displayNode = WalletQrScanScreenNode(presentationData: self.presentationData)
self.displayNodeDidLoad()
// (self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
// |> map { code -> String? in
// return code?.message
// } |> distinctUntilChanged
self.disposable = (((self.displayNode as! WalletQrScanScreenNode).focusedCode.get()
|> map { code -> String? in
return code?.message
}
|> distinctUntilChanged
|> delay(2.5, queue: Queue.mainQueue()))
|> mapToSignal { code -> Signal<String?, NoError> in
return .single(code)
}).start(next: { [weak self] code in
guard let strongSelf = self, let code = code else {
return
}
let cleanString = code.replacingOccurrences(of: "ton://", with: "")
strongSelf.completion(cleanString, nil, nil)
})
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@@ -55,59 +120,168 @@ public final class WalletQrScanScreen: ViewController {
private final class WalletQrScanScreenNode: ViewControllerTracingNode, UIScrollViewDelegate {
private var presentationData: PresentationData
private let previewNode: CameraPreviewNode
private let fadeNode: ASDisplayNode
private let topDimNode: ASDisplayNode
private let bottomDimNode: ASDisplayNode
private let leftDimNode: ASDisplayNode
private let rightDimNode: ASDisplayNode
private let frameNode: ASImageNode
private let torchButtonNode: GlassButtonNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
private var navigationHeight: CGFloat?
private let camera: Camera
private let codeDisposable = MetaDisposable()
fileprivate let focusedCode = ValuePromise<CameraCode?>(ignoreRepeated: true)
private var focusedRect: CGRect?
private var validLayout: (ContainerViewLayout, CGFloat)?
init(presentationData: PresentationData) {
self.presentationData = presentationData
let title: String = ""
let text: String = ""
self.previewNode = CameraPreviewNode()
self.previewNode.backgroundColor = .black
self.fadeNode = ASDisplayNode()
self.fadeNode.alpha = 0.0
self.fadeNode.backgroundColor = .black
self.topDimNode = ASDisplayNode()
self.topDimNode.alpha = 0.625
self.topDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
self.bottomDimNode = ASDisplayNode()
self.bottomDimNode.alpha = 0.625
self.bottomDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
self.leftDimNode = ASDisplayNode()
self.leftDimNode.alpha = 0.625
self.leftDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
self.rightDimNode = ASDisplayNode()
self.rightDimNode.alpha = 0.625
self.rightDimNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.8)
self.frameNode = ASImageNode()
self.frameNode.image = generateFrameImage()
self.torchButtonNode = GlassButtonNode(icon: UIImage(bundleImageName: "Wallet/CameraFlashIcon")!, label: nil)
self.titleNode = ImmediateTextNode()
self.titleNode.displaysAsynchronously = false
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.bold(32.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.titleNode.attributedText = NSAttributedString(string: "Scan QR Code", font: Font.bold(32.0), textColor: .white)
self.titleNode.maximumNumberOfLines = 0
self.titleNode.textAlignment = .center
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center
self.camera = Camera(configuration: .init(preset: .hd1920x1080, position: .back, audio: false))
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.previewNode)
self.addSubnode(self.fadeNode)
self.addSubnode(self.topDimNode)
self.addSubnode(self.bottomDimNode)
self.addSubnode(self.leftDimNode)
self.addSubnode(self.rightDimNode)
self.addSubnode(self.frameNode)
self.addSubnode(self.torchButtonNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.torchButtonNode.addTarget(self, action: #selector(self.torchPressed), forControlEvents: .touchUpInside)
}
deinit {
self.codeDisposable.dispose()
}
override func didLoad() {
super.didLoad()
self.camera.attachPreviewNode(self.previewNode)
self.camera.startCapture()
let throttledSignal = self.camera.detectedCodes
|> mapToThrottled { next -> Signal<[CameraCode], NoError> in
return .single(next) |> then(.complete() |> delay(0.3, queue: Queue.concurrentDefaultQueue()))
}
self.codeDisposable.set((throttledSignal
|> deliverOnMainQueue).start(next: { [weak self] codes in
guard let strongSelf = self else {
return
}
let filteredCodes = codes.filter { $0.message.hasPrefix("ton://") }
if let code = filteredCodes.first, CGRect(x: 0.3, y: 0.3, width: 0.4, height: 0.4).contains(code.boundingBox.center) {
strongSelf.focusedCode.set(code)
strongSelf.updateFocusedRect(code.boundingBox)
} else {
strongSelf.focusedCode.set(nil)
strongSelf.updateFocusedRect(nil)
}
}))
}
private func updateFocusedRect(_ rect: CGRect?) {
self.focusedRect = rect
if let (layout, navigationHeight) = self.validLayout {
self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .animated(duration: 0.4, curve: .spring))
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.navigationHeight = navigationHeight
self.validLayout = (layout, navigationHeight)
let sideInset: CGFloat = 32.0
let titleSpacing: CGFloat = 19.0
let textSpacing: CGFloat = 37.0
let sideInset: CGFloat = 66.0
let titleSpacing: CGFloat = 48.0
let bounds = CGRect(origin: CGPoint(), size: layout.size)
transition.updateFrame(node: self.previewNode, frame: bounds)
transition.updateFrame(node: self.fadeNode, frame: bounds)
let frameSide = max(240.0, layout.size.width - sideInset * 2.0)
let dimHeight = ceil((layout.size.height - frameSide) / 2.0)
let dimInset = (layout.size.width - frameSide) / 2.0
let dimAlpha: CGFloat
let dimRect: CGRect
if let focusedRect = self.focusedRect {
dimAlpha = 1.0
let side = max(bounds.width * focusedRect.width, bounds.height * focusedRect.height) * 0.6
let center = CGPoint(x: (1.0 - focusedRect.center.y) * bounds.width, y: focusedRect.center.x * bounds.height)
dimRect = CGRect(x: center.x - side / 2.0, y: center.y - side / 2.0, width: side, height: side)
} else {
dimAlpha = 0.625
dimRect = CGRect(x: dimInset, y: dimHeight, width: layout.size.width - dimInset * 2.0, height: layout.size.height - dimHeight * 2.0)
}
transition.updateFrame(node: self.topDimNode, frame: CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: dimRect.minY))
transition.updateFrame(node: self.bottomDimNode, frame: CGRect(x: 0.0, y: dimRect.maxY, width: layout.size.width, height: max(0.0, layout.size.height - dimRect.maxY)))
transition.updateFrame(node: self.leftDimNode, frame: CGRect(x: 0.0, y: dimRect.minY, width: max(0.0, dimRect.minX), height: dimRect.height))
transition.updateFrame(node: self.rightDimNode, frame: CGRect(x: dimRect.maxX, y: dimRect.minY, width: max(0.0, layout.size.width - dimRect.maxX), height: dimRect.height))
transition.updateAlpha(node: self.topDimNode, alpha: dimAlpha)
transition.updateAlpha(node: self.bottomDimNode, alpha: dimAlpha)
transition.updateAlpha(node: self.leftDimNode, alpha: dimAlpha)
transition.updateAlpha(node: self.rightDimNode, alpha: dimAlpha)
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 titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - sideInset * 2.0, height: layout.size.height))
var contentHeight: CGFloat = 0.0
let contentVerticalOrigin = navigationHeight + 10.0
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: contentVerticalOrigin), size: titleSize)
let titleFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width) / 2.0), y: dimHeight - titleSize.height - titleSpacing), size: titleSize)
transition.updateFrameAdditive(node: self.titleNode, frame: titleFrame)
let textFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width) / 2.0), y: titleFrame.maxY + titleSpacing), size: textSize)
transition.updateFrameAdditive(node: self.textNode, frame: textFrame)
contentHeight = textFrame.maxY + textSpacing
}
@objc private func torchPressed() {
self.torchButtonNode.isSelected = !self.torchButtonNode.isSelected
self.camera.setTorchActive(self.torchButtonNode.isSelected)
}
}

View File

@@ -131,9 +131,9 @@ private func walletReceiveScreenEntries(presentationData: PresentationData, addr
var entries: [WalletReceiveScreenEntry] = []
entries.append(.addressHeader(presentationData.theme, "YOUR WALLET ADDRESS"))
let address = String(address[address.startIndex..<address.index(address.startIndex, offsetBy: 24)] + "\n" + address[address.index(address.startIndex, offsetBy: 24)..<address.endIndex])
let formattedAddress = String(address[address.startIndex..<address.index(address.startIndex, offsetBy: 24)] + "\n" + address[address.index(address.startIndex, offsetBy: 24)..<address.endIndex])
entries.append(.addressCode(presentationData.theme, address))
entries.append(.address(presentationData.theme, address))
entries.append(.address(presentationData.theme, formattedAddress))
entries.append(.copyAddress(presentationData.theme, "Copy Wallet Address"))
entries.append(.shareAddressLink(presentationData.theme, "Share Wallet Address"))
entries.append(.addressInfo(presentationData.theme, "Share this link with other Gram wallet owners to receive Grams from them."))
@@ -159,9 +159,6 @@ func walletReceiveScreen(context: AccountContext, tonContext: TonContext, wallet
presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .genericSuccess("Address copied to clipboard.", false)), nil)
}, shareAddressLink: {
guard let address = address.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) else {
return
}
let controller = ShareController(context: context, subject: .url("ton://\(address)"), preferredAction: .default)
presentControllerImpl?(controller, nil)
})

View File

@@ -13,17 +13,20 @@ import AlertUI
import TextFormat
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 selectNextInputItem: (WalletSendScreenEntryTag) -> Void
let openQrScanner: () -> Void
let proceed: () -> Void
init(context: AccountContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, proceed: @escaping () -> Void) {
init(context: AccountContext, updateState: @escaping ((WalletSendScreenState) -> WalletSendScreenState) -> Void, selectNextInputItem: @escaping (WalletSendScreenEntryTag) -> Void, openQrScanner: @escaping () -> Void, proceed: @escaping () -> Void) {
self.context = context
self.updateState = updateState
self.selectNextInputItem = selectNextInputItem
self.openQrScanner = openQrScanner
self.proceed = proceed
}
}
@@ -93,8 +96,8 @@ private func isValidAmount(_ amount: String) -> Bool {
}
private func formatAmountText(_ amount: Int64, decimalSeparator: String = ".") -> String {
if amount < 10000000000 {
return "0\(decimalSeparator)\(String(amount).rightJustified(width: 10, pad: "0"))"
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))
@@ -103,7 +106,7 @@ private func formatAmountText(_ amount: Int64, decimalSeparator: String = ".") -
}
private func amountValue(_ string: String) -> Int64 {
return Int64((Double(string) ?? 0.0) * 1000.0)
return Int64((Double(string.replacingOccurrences(of: ",", with: ".")) ?? 0.0) * 1000000000.0)
}
private func normalizedStringForGramsString(_ string: String, decimalSeparator: String = ".") -> String {
@@ -115,7 +118,7 @@ private enum WalletSendScreenEntry: ItemListNodeEntry {
case address(PresentationTheme, String, String)
case addressInfo(PresentationTheme, String)
case amountHeader(PresentationTheme, String, String, Bool)
case amountHeader(PresentationTheme, String, String?, Bool)
case amount(PresentationTheme, PresentationStrings, String, String)
case commentHeader(PresentationTheme, String)
@@ -207,21 +210,23 @@ 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: "Enter wallet address...", maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { address in
return ItemListMultilineInputItem(theme: theme, text: address, placeholder: placeholder, maxLength: .init(value: walletAddressLength, display: false), sectionId: self.section, style: .blocks, capitalization: false, autocorrection: false, returnKeyType: .next, minimalHeight: 68.0, textUpdated: { address in
arguments.updateState { state in
var state = state
state.address = address
state.address = address.replacingOccurrences(of: "\n", with: "")
return state
}
}, shouldUpdateText: { text in
return isValidAddress(text)
}, tag: WalletSendScreenEntryTag.address, action: {
arguments.selectNextInputItem(WalletSendScreenEntryTag.address)
})
}, inlineAction: ItemListMultilineInputInlineAction(icon: UIImage(bundleImageName: "Wallet/QrIcon")!, action: {
arguments.openQrScanner()
}))
case let .addressInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .amountHeader(theme, text, balance, insufficient):
return ItemListSectionHeaderItem(theme: theme, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: balance, color: insufficient ? .destructive : .generic), sectionId: self.section)
return ItemListSectionHeaderItem(theme: theme, text: text, activityIndicator: balance == nil ? .right : .none, accessoryText: balance.flatMap { ItemListSectionHeaderAccessoryText(value: $0, color: insufficient ? .destructive : .generic, icon: balanceIcon) }, sectionId: self.section)
case let .amount(theme, strings, placeholder, text):
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
@@ -271,16 +276,18 @@ private struct WalletSendScreenState: Equatable {
var address: String
var amount: String
var comment: String
var qrScanAvailable: Bool
}
private func walletSendScreenEntries(presentationData: PresentationData, balance: Int64?, state: WalletSendScreenState) -> [WalletSendScreenEntry] {
var entries: [WalletSendScreenEntry] = []
entries.append(.addressHeader(presentationData.theme, "RECIPIENT WALLET ADDRESS"))
entries.append(.address(presentationData.theme, "Enter wallet address...", state.address))
entries.append(.addressInfo(presentationData.theme, "Copy the 48-letter address of the recipient here or ask them to send you a ton:// link."))
let amount = amountValue(state.amount)
entries.append(.amountHeader(presentationData.theme, "AMOUNT", "BALANCE: \(formatBalanceText(balance ?? 0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))💎", amount > 0 && (balance ?? 0) < amount))
entries.append(.amountHeader(presentationData.theme, "AMOUNT", balance.flatMap { "BALANCE: \(formatBalanceText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))" }, amount > 0 && (balance ?? 0) < amount))
entries.append(.amount(presentationData.theme, presentationData.strings, "Grams to send", state.amount ?? ""))
entries.append(.commentHeader(presentationData.theme, "COMMENT"))
@@ -296,9 +303,9 @@ private final class WalletSendScreenImpl: ItemListController<WalletSendScreenEnt
}
func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, address: String? = nil, amount: Int64? = nil) -> ViewController {
func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInfo: WalletInfo, walletState: WalletState? = nil, address: String? = nil, amount: Int64? = nil) -> ViewController {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatAmountText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: "")
let initialState = WalletSendScreenState(address: address ?? "", amount: amount.flatMap { formatAmountText($0, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) } ?? "", comment: "", qrScanAvailable: address == nil)
let statePromise = ValuePromise(initialState, ignoreRepeated: true)
let stateValue = Atomic(value: initialState)
@@ -309,13 +316,41 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
var presentControllerImpl: ((ViewController, Any?) -> Void)?
var presentInGlobalOverlayImpl: ((ViewController, Any?) -> Void)?
var pushImpl: ((ViewController) -> Void)?
var popImpl: (() -> Void)?
var dismissImpl: (() -> Void)?
var dismissInputImpl: (() -> Void)?
var selectNextInputItemImpl: ((WalletSendScreenEntryTag) -> Void)?
let arguments = WalletSendScreenArguments(context: context, updateState: { f in
updateState(f)
}, selectNextInputItem: { tag in
selectNextInputItemImpl?(tag)
}, 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.comment = comment
}
state.qrScanAvailable = false
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 }
@@ -331,7 +366,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
let address = state.address[state.address.startIndex..<state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)] + " \n " + state.address[state.address.index(state.address.startIndex, offsetBy: walletAddressLength / 2)..<state.address.endIndex]
let text = "Do you want to send **\(formatAmountText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))** Grams to\n\(address)?"
let text = "Do you want to send **\(formatBalanceText(amount, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))** Grams to\n\(address)?"
let bodyAttributes = MarkdownAttributeSet(font: Font.regular(13.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: presentationData.theme.actionSheet.primaryTextColor)
let attributedText = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil }), textAlignment: .center))
@@ -342,7 +377,7 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
dismissAlertImpl?(true)
}), TextAlertAction(type: .defaultAction, title: "Confirm", action: {
dismissAlertImpl?(false)
pushImpl?(WalletPasscodeScreen(context: context, tonContext: tonContext, mode: .authorizeTransfer(walletInfo, state.address, amount, state.comment)))
pushImpl?(WalletSplashScreen(context: context, tonContext: tonContext, mode: .sending(walletInfo, state.address, amount, state.comment)))
})], dismissAutomatically: false)
presentInGlobalOverlayImpl?(controller, nil)
@@ -355,8 +390,13 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
}
})
let balance: Signal<WalletBalance?, NoError> = Signal.single(WalletBalance(rawValue: 2500))
let balance: Signal<WalletState?, NoError> = .single(walletState)
|> then(walletAddress(publicKey: walletInfo.publicKey, tonInstance: tonContext.instance)
|> mapToSignal { address in
return getWalletState(address: address, tonInstance: tonContext.instance)
|> map(Optional.init)
})
var focusItemTag: ItemListItemTag?
if address == nil {
focusItemTag = WalletSendScreenEntryTag.address
@@ -373,14 +413,14 @@ 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.rawValue
sendEnabled = isValidAddress(state.address, exactLength: true) && amount > 0 && amount <= balance.balance
}
let rightNavigationButton = ItemListNavigationButton(content: .text("Send"), style: .bold, enabled: sendEnabled, action: {
arguments.proceed()
})
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text("Send Grams"), leftNavigationButton: leftNavigationButton, rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: walletSendScreenEntries(presentationData: presentationData, balance: balance?.rawValue, state: state), style: .blocks, focusItemTag: focusItemTag, animateChanges: false)
let listState = ItemListNodeState(entries: walletSendScreenEntries(presentationData: presentationData, balance: balance?.balance, state: state), style: .blocks, focusItemTag: focusItemTag, animateChanges: false)
return (controllerState, (listState, arguments))
}
@@ -396,10 +436,16 @@ func walletSendScreen(context: AccountContext, tonContext: TonContext, walletInf
pushImpl = { [weak controller] c in
controller?.push(c)
}
popImpl = { [weak controller] in
(controller?.navigationController as? NavigationController)?.popViewController(animated: true)
}
dismissImpl = { [weak controller] in
controller?.view.endEditing(true)
let _ = controller?.dismiss()
}
dismissInputImpl = { [weak controller] in
controller?.view.endEditing(true)
}
selectNextInputItemImpl = { [weak controller] currentTag in
guard let controller = controller else {
return

View File

@@ -13,6 +13,7 @@ import SwiftSignalKit
import OverlayStatusController
import ItemListUI
import AlertUI
import TextFormat
public enum WalletSplashMode {
case intro
@@ -49,10 +50,10 @@ public final class WalletSplashScreen: ViewController {
case .intro:
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "Not Now", style: .plain, target: self, action: #selector(self.backPressed)), animated: false)
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "Import existing wallet", style: .plain, target: self, action: #selector(self.importPressed)), animated: false)
case let .sending(walletInfo, address, amount, comment):
case let .sending(walletInfo, address, amount, textMessage):
self.navigationItem.setLeftBarButton(UIBarButtonItem(customDisplayNode: ASDisplayNode())!, animated: false)
let _ = (sendGramsFromWallet(network: self.context.account.network, tonInstance: self.tonContext.instance, keychain: self.tonContext.keychain, walletInfo: walletInfo, toAddress: address, amount: amount)
let _ = (sendGramsFromWallet(network: self.context.account.network, tonInstance: self.tonContext.instance, keychain: self.tonContext.keychain, walletInfo: walletInfo, toAddress: address, amount: amount, textMessage: textMessage)
|> deliverOnMainQueue).start(error: { [weak self] error in
guard let strongSelf = self else {
return
@@ -223,29 +224,32 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
self.animationNode = AnimatedStickerNode()
let title: String
let text: String
let text: NSAttributedString
let buttonText: String
let termsText: String
let secondaryActionText: String
let textFont = Font.regular(16.0)
let textColor = self.presentationData.theme.list.itemPrimaryTextColor
switch mode {
case .intro:
title = "Gram Wallet"
text = "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries."
text = NSAttributedString(string: "Gram wallet allows you to make fast and secure blockchain-based payments without intermediaries.", font: textFont, textColor: textColor)
buttonText = "Create My Wallet"
termsText = "By creating the wallet you accept\nTerms of Conditions."
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/IntroIcon")
secondaryActionText = ""
case .created:
title = "Congratulations"
text = "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down secret words and\nset up a secure passcode."
text = NSAttributedString(string: "Your Gram wallet has just been created. Only you control it.\n\nTo be able to always have access to it, please write down secret words and\nset up a secure passcode.", font: textFont, textColor: textColor)
buttonText = "Proceed"
termsText = ""
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/CreatedIcon")
secondaryActionText = ""
case .success:
title = "Ready to go!"
text = "Youre all set. Now you have a wallet that only you control - directly, without middlemen or bankers. "
text = NSAttributedString(string: "Youre all set. Now you have a wallet that only you control - directly, without middlemen or bankers. ", font: textFont, textColor: textColor)
buttonText = "View My Wallet"
termsText = ""
self.iconNode.image = nil
@@ -256,7 +260,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
secondaryActionText = ""
case .restoreFailed:
title = "Too Bad"
text = "Without the secret words, you can't'nrestore access to the wallet."
text = NSAttributedString(string: "Without the secret words, you can't'nrestore access to the wallet.", font: textFont, textColor: textColor)
buttonText = "Create a New Wallet"
termsText = ""
self.iconNode.image = nil
@@ -267,14 +271,16 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
secondaryActionText = "Enter 24 words"
case .sending:
title = "Sending Grams"
text = "Please wait a few seconds for your transaction to be processed..."
text = NSAttributedString(string: "Please wait a few seconds for your transaction to be processed...", font: textFont, textColor: textColor)
buttonText = ""
termsText = ""
self.iconNode.image = UIImage(bundleImageName: "Settings/Wallet/SendingIcon")
secondaryActionText = ""
case let .sent(_, amount):
title = "Done!"
text = "\(amount) Grams have been sent."
let bodyAttributes = MarkdownAttributeSet(font: textFont, textColor: textColor)
let boldAttributes = MarkdownAttributeSet(font: Font.semibold(16.0), textColor: textColor)
text = parseMarkdownIntoAttributedString("**\(formatBalanceText(amount, decimalSeparator: self.presentationData.dateTimeFormat.decimalSeparator)) Grams** have been sent.", attributes: MarkdownAttributes(body: bodyAttributes, bold: boldAttributes, link: bodyAttributes, linkAttribute: { _ in return nil }), textAlignment: .center)
buttonText = "View My Wallet"
termsText = ""
self.iconNode.image = nil
@@ -293,7 +299,7 @@ private final class WalletSplashScreenNode: ViewControllerTracingNode {
self.textNode = ImmediateTextNode()
self.textNode.displaysAsynchronously = false
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(16.0), textColor: self.presentationData.theme.list.itemPrimaryTextColor)
self.textNode.attributedText = text
self.textNode.maximumNumberOfLines = 0
self.textNode.lineSpacing = 0.1
self.textNode.textAlignment = .center

View File

@@ -142,7 +142,7 @@ private func walletTransactionInfoControllerEntries(presentationData: Presentati
}
entries.append(.infoAddress(presentationData.theme, text))
if case .list = address {
entries.append(.infoCopyAddress(presentationData.theme, "Copy Address"))
entries.append(.infoCopyAddress(presentationData.theme, "Copy Wallet Address"))
entries.append(.infoSendGrams(presentationData.theme, "Send Grams"))
}
@@ -170,6 +170,7 @@ func walletTransactionInfoController(context: AccountContext, tonContext: TonCon
}, sendGrams: {
let address = extractAddress(walletTransaction)
if case let .list(addresses) = address, let address = addresses.first {
dismissImpl?()
pushImpl?(walletSendScreen(context: context, tonContext: tonContext, walletInfo: walletInfo, address: address))
}
})