651 lines
24 KiB
Swift
651 lines
24 KiB
Swift
|
|
import SwiftUI
|
|||
|
|
|
|||
|
|
enum SendMethod: String, CaseIterable {
|
|||
|
|
case montanaID = "montanaID"
|
|||
|
|
case nickname = "nickname"
|
|||
|
|
case fullAddress = "fullAddress"
|
|||
|
|
|
|||
|
|
var label: String {
|
|||
|
|
switch self {
|
|||
|
|
case .montanaID: return "Ɉ-ID"
|
|||
|
|
case .nickname: return "@ Ник"
|
|||
|
|
case .fullAddress: return "mt... Адрес"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var placeholder: String {
|
|||
|
|
switch self {
|
|||
|
|
case .montanaID: return "1"
|
|||
|
|
case .nickname: return "@junomoneta"
|
|||
|
|
case .fullAddress: return "mt..."
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var icon: String {
|
|||
|
|
switch self {
|
|||
|
|
case .montanaID: return "person.text.rectangle"
|
|||
|
|
case .nickname: return "at"
|
|||
|
|
case .fullAddress: return "key.horizontal"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Состояния экрана отправки
|
|||
|
|
enum SendStep {
|
|||
|
|
case form // ввод данных
|
|||
|
|
case preConfirm // подтверждение перед отправкой
|
|||
|
|
case result // результат
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
struct SendView: View {
|
|||
|
|
@EnvironmentObject var engine: PresenceEngine
|
|||
|
|
var onShowHistory: (() -> Void)? = nil
|
|||
|
|
|
|||
|
|
@State private var step: SendStep = .form
|
|||
|
|
@State private var sendMethod: SendMethod = .montanaID
|
|||
|
|
@State private var recipient = ""
|
|||
|
|
@State private var amount = ""
|
|||
|
|
@State private var statusText = ""
|
|||
|
|
@State private var statusColor: Color = .secondary
|
|||
|
|
@State private var resolvedAddress = ""
|
|||
|
|
@State private var resolvedAlias = ""
|
|||
|
|
@State private var isSending = false
|
|||
|
|
@State private var sentAmount = 0
|
|||
|
|
@State private var sentRecipient = ""
|
|||
|
|
@State private var sentTimestamp = ""
|
|||
|
|
@FocusState private var recipientFocused: Bool
|
|||
|
|
@Environment(\.dismiss) private var dismiss
|
|||
|
|
|
|||
|
|
private let cyan = Color(red: 0, green: 0.83, blue: 1)
|
|||
|
|
private let purple = Color(red: 0.48, green: 0.18, blue: 1)
|
|||
|
|
private let cardBg = Color(red: 0.09, green: 0.09, blue: 0.12)
|
|||
|
|
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
|
|||
|
|
|
|||
|
|
var body: some View {
|
|||
|
|
Group {
|
|||
|
|
switch step {
|
|||
|
|
case .form:
|
|||
|
|
sendFormView
|
|||
|
|
case .preConfirm:
|
|||
|
|
preConfirmView
|
|||
|
|
case .result:
|
|||
|
|
resultView
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.frame(width: 320, height: 440)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
// STEP 2: Подтверждение перед отправкой
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
private var preConfirmView: some View {
|
|||
|
|
VStack(spacing: 0) {
|
|||
|
|
// Header
|
|||
|
|
HStack {
|
|||
|
|
Image(systemName: "exclamationmark.shield.fill")
|
|||
|
|
.foregroundColor(gold)
|
|||
|
|
Text("Подтверждение")
|
|||
|
|
.font(.system(size: 16, weight: .bold))
|
|||
|
|
Spacer()
|
|||
|
|
Button(action: { step = .form }) {
|
|||
|
|
Image(systemName: "xmark.circle.fill")
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.top, 14)
|
|||
|
|
.padding(.bottom, 16)
|
|||
|
|
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
Text("Вы отправляете")
|
|||
|
|
.font(.system(size: 12))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
.padding(.bottom, 6)
|
|||
|
|
|
|||
|
|
Text(formatAmount(Int(amount) ?? 0))
|
|||
|
|
.font(.system(size: 28, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(cyan)
|
|||
|
|
.padding(.bottom, 16)
|
|||
|
|
|
|||
|
|
VStack(spacing: 10) {
|
|||
|
|
confirmRow(label: "Получатель", value: resolvedAlias.isEmpty ? displayAddr(resolvedAddress) : resolvedAlias)
|
|||
|
|
confirmRow(label: "Адрес", value: displayAddr(resolvedAddress))
|
|||
|
|
}
|
|||
|
|
.padding(14)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(10)
|
|||
|
|
.padding(.horizontal, 20)
|
|||
|
|
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
if !statusText.isEmpty {
|
|||
|
|
Text(statusText)
|
|||
|
|
.font(.system(size: 11))
|
|||
|
|
.foregroundColor(statusColor)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 6)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Подтвердить
|
|||
|
|
Button(action: { executeTransfer() }) {
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
if isSending {
|
|||
|
|
ProgressView()
|
|||
|
|
.controlSize(.small)
|
|||
|
|
} else {
|
|||
|
|
Image(systemName: "checkmark.shield.fill")
|
|||
|
|
}
|
|||
|
|
Text("Подтвердить")
|
|||
|
|
.font(.system(size: 14, weight: .bold))
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 4)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.borderedProminent)
|
|||
|
|
.tint(gold)
|
|||
|
|
.controlSize(.large)
|
|||
|
|
.disabled(isSending)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 8)
|
|||
|
|
|
|||
|
|
// Назад
|
|||
|
|
Button(action: { step = .form }) {
|
|||
|
|
Text("Назад")
|
|||
|
|
.font(.system(size: 12))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
.padding(.bottom, 14)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
// STEP 3: Результат — Готово + История
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
private var resultView: some View {
|
|||
|
|
VStack(spacing: 0) {
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
Image(systemName: "checkmark.circle.fill")
|
|||
|
|
.font(.system(size: 48))
|
|||
|
|
.foregroundColor(.green)
|
|||
|
|
.padding(.bottom, 16)
|
|||
|
|
|
|||
|
|
Text("Отправлено")
|
|||
|
|
.font(.system(size: 18, weight: .bold))
|
|||
|
|
.foregroundColor(.green)
|
|||
|
|
.padding(.bottom, 20)
|
|||
|
|
|
|||
|
|
VStack(spacing: 12) {
|
|||
|
|
confirmRow(label: "Сумма", value: formatAmount(sentAmount))
|
|||
|
|
confirmRow(label: "Получатель", value: sentRecipient)
|
|||
|
|
confirmRow(label: "Время", value: sentTimestamp)
|
|||
|
|
}
|
|||
|
|
.padding(16)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(10)
|
|||
|
|
.padding(.horizontal, 24)
|
|||
|
|
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
// История
|
|||
|
|
Button(action: {
|
|||
|
|
dismiss()
|
|||
|
|
onShowHistory?()
|
|||
|
|
}) {
|
|||
|
|
HStack(spacing: 6) {
|
|||
|
|
Image(systemName: "clock.arrow.circlepath")
|
|||
|
|
Text("Посмотреть историю")
|
|||
|
|
.font(.system(size: 13, weight: .medium))
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 4)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.borderedProminent)
|
|||
|
|
.tint(gold)
|
|||
|
|
.controlSize(.large)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 8)
|
|||
|
|
|
|||
|
|
// Готово
|
|||
|
|
Button(action: { dismiss() }) {
|
|||
|
|
Text("Готово")
|
|||
|
|
.font(.system(size: 13, weight: .medium))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
.padding(.bottom, 14)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func confirmRow(label: String, value: String) -> some View {
|
|||
|
|
HStack {
|
|||
|
|
Text(label)
|
|||
|
|
.font(.system(size: 11))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
Spacer()
|
|||
|
|
Text(value)
|
|||
|
|
.font(.system(size: 13, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(.white)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
// STEP 1: Форма ввода
|
|||
|
|
// ═══════════════════════════════════════════════════════════════════
|
|||
|
|
|
|||
|
|
private var sendFormView: some View {
|
|||
|
|
VStack(spacing: 0) {
|
|||
|
|
// Header
|
|||
|
|
HStack {
|
|||
|
|
Image(systemName: "paperplane.fill")
|
|||
|
|
.foregroundColor(cyan)
|
|||
|
|
Text("Ɉ Отправить")
|
|||
|
|
.font(.system(size: 16, weight: .bold))
|
|||
|
|
Spacer()
|
|||
|
|
Button(action: { dismiss() }) {
|
|||
|
|
Image(systemName: "xmark.circle.fill")
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.top, 14)
|
|||
|
|
.padding(.bottom, 10)
|
|||
|
|
|
|||
|
|
// Balance card
|
|||
|
|
HStack {
|
|||
|
|
VStack(alignment: .leading, spacing: 2) {
|
|||
|
|
Text("Доступно")
|
|||
|
|
.font(.system(size: 10))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
Text("\(formatAmount(engine.availableBalance))")
|
|||
|
|
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(cyan)
|
|||
|
|
}
|
|||
|
|
Spacer()
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 14)
|
|||
|
|
.padding(.vertical, 10)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 12)
|
|||
|
|
|
|||
|
|
// Send method picker
|
|||
|
|
VStack(alignment: .leading, spacing: 6) {
|
|||
|
|
Text("Как отправить")
|
|||
|
|
.font(.system(size: 10, weight: .medium))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
ForEach(SendMethod.allCases, id: \.self) { method in
|
|||
|
|
methodButton(method)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
}
|
|||
|
|
.padding(.bottom, 10)
|
|||
|
|
|
|||
|
|
// Recipient input
|
|||
|
|
VStack(spacing: 6) {
|
|||
|
|
HStack(spacing: 6) {
|
|||
|
|
Image(systemName: sendMethod.icon)
|
|||
|
|
.foregroundColor(cyan.opacity(0.6))
|
|||
|
|
.font(.system(size: 12))
|
|||
|
|
.frame(width: 20)
|
|||
|
|
|
|||
|
|
TextField(sendMethod.placeholder, text: $recipient)
|
|||
|
|
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
|||
|
|
.textFieldStyle(.plain)
|
|||
|
|
.focused($recipientFocused)
|
|||
|
|
.onSubmit { resolveRecipient() }
|
|||
|
|
.onChange(of: recipient) { _ in
|
|||
|
|
resolvedAddress = ""
|
|||
|
|
resolvedAlias = ""
|
|||
|
|
statusText = ""
|
|||
|
|
}
|
|||
|
|
.onChange(of: recipientFocused) { focused in
|
|||
|
|
if !focused && !recipient.isEmpty && resolvedAddress.isEmpty {
|
|||
|
|
resolveRecipient()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Button(action: {
|
|||
|
|
if let str = NSPasteboard.general.string(forType: .string) {
|
|||
|
|
recipient = String(str.trimmingCharacters(in: .whitespacesAndNewlines).prefix(100))
|
|||
|
|
resolveRecipient()
|
|||
|
|
}
|
|||
|
|
}) {
|
|||
|
|
Image(systemName: "doc.on.clipboard")
|
|||
|
|
.font(.system(size: 11))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
.help("Вставить")
|
|||
|
|
}
|
|||
|
|
.padding(10)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 8)
|
|||
|
|
.stroke(recipientFocused ? cyan.opacity(0.5) : Color.secondary.opacity(0.2), lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
|
|||
|
|
if !resolvedAlias.isEmpty {
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Image(systemName: "checkmark.circle.fill")
|
|||
|
|
.foregroundColor(.green)
|
|||
|
|
.font(.system(size: 10))
|
|||
|
|
Text(resolvedAlias)
|
|||
|
|
.font(.system(size: 11, weight: .medium))
|
|||
|
|
.foregroundColor(.green)
|
|||
|
|
Spacer()
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 20)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
.padding(.bottom, 10)
|
|||
|
|
|
|||
|
|
// Amount input
|
|||
|
|
VStack(spacing: 8) {
|
|||
|
|
HStack(spacing: 6) {
|
|||
|
|
Text("Ɉ")
|
|||
|
|
.font(.system(size: 18, weight: .bold, design: .monospaced))
|
|||
|
|
.foregroundColor(gold.opacity(0.5))
|
|||
|
|
|
|||
|
|
TextField("0", text: $amount)
|
|||
|
|
.font(.system(size: 22, weight: .bold, design: .monospaced))
|
|||
|
|
.textFieldStyle(.plain)
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
}
|
|||
|
|
.padding(10)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(8)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 8)
|
|||
|
|
.stroke(Color.secondary.opacity(0.2), lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
|
|||
|
|
// Quick amount buttons
|
|||
|
|
HStack(spacing: 6) {
|
|||
|
|
quickBtn(100)
|
|||
|
|
quickBtn(1000)
|
|||
|
|
quickBtn(10000)
|
|||
|
|
Button(action: { amount = "\(engine.availableBalance)" }) {
|
|||
|
|
Text("MAX")
|
|||
|
|
.font(.system(size: 10, weight: .bold))
|
|||
|
|
.foregroundColor(cyan)
|
|||
|
|
.padding(.horizontal, 10)
|
|||
|
|
.padding(.vertical, 5)
|
|||
|
|
.background(cyan.opacity(0.1))
|
|||
|
|
.cornerRadius(6)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Warnings
|
|||
|
|
if let amt = Int(amount), amt > engine.availableBalance, engine.availableBalance > 0 {
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|||
|
|
.font(.system(size: 9))
|
|||
|
|
Text("Недостаточно средств")
|
|||
|
|
}
|
|||
|
|
.font(.system(size: 10))
|
|||
|
|
.foregroundColor(.orange)
|
|||
|
|
.padding(.top, 6)
|
|||
|
|
}
|
|||
|
|
if engine.availableBalance == 0 {
|
|||
|
|
HStack(spacing: 4) {
|
|||
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|||
|
|
.font(.system(size: 9))
|
|||
|
|
Text("Нет доступных средств")
|
|||
|
|
}
|
|||
|
|
.font(.system(size: 10))
|
|||
|
|
.foregroundColor(.orange)
|
|||
|
|
.padding(.top, 6)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Spacer()
|
|||
|
|
|
|||
|
|
// Status
|
|||
|
|
if !statusText.isEmpty {
|
|||
|
|
Text(statusText)
|
|||
|
|
.font(.system(size: 11))
|
|||
|
|
.foregroundColor(statusColor)
|
|||
|
|
.lineLimit(2)
|
|||
|
|
.multilineTextAlignment(.center)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 6)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Send button → переход к подтверждению
|
|||
|
|
Button(action: { prepareConfirmation() }) {
|
|||
|
|
HStack(spacing: 8) {
|
|||
|
|
Image(systemName: "paperplane.fill")
|
|||
|
|
Text("Отправить")
|
|||
|
|
.font(.system(size: 14, weight: .bold))
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 4)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.borderedProminent)
|
|||
|
|
.tint(canSend ? cyan : Color.secondary.opacity(0.3))
|
|||
|
|
.controlSize(.large)
|
|||
|
|
.disabled(!canSend)
|
|||
|
|
.padding(.horizontal, 16)
|
|||
|
|
.padding(.bottom, 14)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Subviews
|
|||
|
|
|
|||
|
|
private func methodButton(_ method: SendMethod) -> some View {
|
|||
|
|
let isSelected = sendMethod == method
|
|||
|
|
return Button(action: {
|
|||
|
|
sendMethod = method
|
|||
|
|
recipient = ""
|
|||
|
|
resolvedAddress = ""
|
|||
|
|
resolvedAlias = ""
|
|||
|
|
statusText = ""
|
|||
|
|
}) {
|
|||
|
|
VStack(spacing: 3) {
|
|||
|
|
Image(systemName: method.icon)
|
|||
|
|
.font(.system(size: 11))
|
|||
|
|
Text(method.label)
|
|||
|
|
.font(.system(size: 8, weight: .medium))
|
|||
|
|
.lineLimit(1)
|
|||
|
|
}
|
|||
|
|
.frame(maxWidth: .infinity)
|
|||
|
|
.padding(.vertical, 6)
|
|||
|
|
.background(isSelected ? cyan.opacity(0.15) : cardBg)
|
|||
|
|
.foregroundColor(isSelected ? cyan : .secondary)
|
|||
|
|
.cornerRadius(6)
|
|||
|
|
.overlay(
|
|||
|
|
RoundedRectangle(cornerRadius: 6)
|
|||
|
|
.stroke(isSelected ? cyan.opacity(0.4) : Color.clear, lineWidth: 1)
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func quickBtn(_ value: Int) -> some View {
|
|||
|
|
Button(action: { amount = "\(value)" }) {
|
|||
|
|
Text("\(value)")
|
|||
|
|
.font(.system(size: 10, weight: .medium, design: .monospaced))
|
|||
|
|
.foregroundColor(.secondary)
|
|||
|
|
.padding(.horizontal, 10)
|
|||
|
|
.padding(.vertical, 5)
|
|||
|
|
.background(cardBg)
|
|||
|
|
.cornerRadius(6)
|
|||
|
|
}
|
|||
|
|
.buttonStyle(.plain)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func formatAmount(_ amount: Int) -> String {
|
|||
|
|
let formatter = NumberFormatter()
|
|||
|
|
formatter.numberStyle = .decimal
|
|||
|
|
formatter.groupingSeparator = " "
|
|||
|
|
let formatted = formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
|
|||
|
|
return "\(formatted) Ɉ"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Helpers
|
|||
|
|
|
|||
|
|
private func displayAddr(_ addr: String) -> String {
|
|||
|
|
guard addr.count > 10 else { return addr }
|
|||
|
|
return String(addr.prefix(6)) + "..." + String(addr.suffix(4))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// MARK: - Logic
|
|||
|
|
|
|||
|
|
private var canSend: Bool {
|
|||
|
|
guard let amt = Int(amount), amt > 0, amt <= engine.availableBalance else { return false }
|
|||
|
|
return !recipient.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && !isSending
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func extractLookupID(from input: String) -> String? {
|
|||
|
|
switch sendMethod {
|
|||
|
|
case .montanaID:
|
|||
|
|
// Символ Ɉ зашит — вводится только номер цифрами
|
|||
|
|
if input.hasPrefix("Ɉ-") { return String(input.dropFirst(2)) }
|
|||
|
|
else if input.hasPrefix("J-") || input.hasPrefix("j-") { return String(input.dropFirst(2)) }
|
|||
|
|
let digits = input.filter { $0.isNumber }
|
|||
|
|
return digits.isEmpty ? nil : digits
|
|||
|
|
case .nickname:
|
|||
|
|
let nick = input.hasPrefix("@") ? String(input.dropFirst()) : input
|
|||
|
|
return nick.isEmpty ? nil : nick
|
|||
|
|
case .fullAddress:
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private func resolveRecipient() {
|
|||
|
|
let input = recipient.trimmingCharacters(in: .whitespacesAndNewlines)
|
|||
|
|
guard !input.isEmpty else { return }
|
|||
|
|
|
|||
|
|
if sendMethod == .fullAddress {
|
|||
|
|
if input.hasPrefix("mt") && input.count == 42 {
|
|||
|
|
resolvedAddress = input
|
|||
|
|
resolvedAlias = String(input.prefix(8)) + "..." + String(input.suffix(4))
|
|||
|
|
} else {
|
|||
|
|
statusText = "Адрес должен начинаться с mt и содержать 42 символа"
|
|||
|
|
statusColor = .orange
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
guard let lookupID = extractLookupID(from: input) else { return }
|
|||
|
|
|
|||
|
|
statusText = "Ищу..."
|
|||
|
|
statusColor = .secondary
|
|||
|
|
Task { @MainActor in
|
|||
|
|
do {
|
|||
|
|
let (addr, alias) = try await engine.api.lookupWallet(identifier: lookupID)
|
|||
|
|
if addr == (engine.address ?? "") {
|
|||
|
|
resolvedAddress = ""
|
|||
|
|
resolvedAlias = ""
|
|||
|
|
statusText = "Нельзя отправить себе"
|
|||
|
|
statusColor = .orange
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
resolvedAddress = addr
|
|||
|
|
resolvedAlias = alias
|
|||
|
|
statusText = ""
|
|||
|
|
} catch {
|
|||
|
|
statusText = "Адрес не найден"
|
|||
|
|
statusColor = .red
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Шаг 1 → 2: Резолвим адрес и показываем экран подтверждения
|
|||
|
|
private func prepareConfirmation() {
|
|||
|
|
guard !isSending else { return }
|
|||
|
|
guard let _ = Int(amount), canSend else { return }
|
|||
|
|
|
|||
|
|
// Если адрес ещё не резолвлен — резолвим
|
|||
|
|
if resolvedAddress.isEmpty {
|
|||
|
|
let input = recipient.trimmingCharacters(in: .whitespacesAndNewlines)
|
|||
|
|
guard !input.isEmpty else {
|
|||
|
|
statusText = "Введите получателя"
|
|||
|
|
statusColor = .orange
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if sendMethod == .fullAddress && input.hasPrefix("mt") && input.count == 42 {
|
|||
|
|
resolvedAddress = input
|
|||
|
|
resolvedAlias = String(input.prefix(8)) + "..." + String(input.suffix(4))
|
|||
|
|
step = .preConfirm
|
|||
|
|
} else if let lookupID = extractLookupID(from: input) {
|
|||
|
|
isSending = true
|
|||
|
|
statusText = "Ищу..."
|
|||
|
|
statusColor = .secondary
|
|||
|
|
Task { @MainActor in
|
|||
|
|
defer { isSending = false }
|
|||
|
|
do {
|
|||
|
|
let (addr, alias) = try await engine.api.lookupWallet(identifier: lookupID)
|
|||
|
|
guard addr != (engine.address ?? "") else {
|
|||
|
|
statusText = "Нельзя отправить себе"
|
|||
|
|
statusColor = .orange
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
resolvedAddress = addr
|
|||
|
|
resolvedAlias = alias
|
|||
|
|
statusText = ""
|
|||
|
|
step = .preConfirm
|
|||
|
|
} catch {
|
|||
|
|
statusText = "Адрес не найден"
|
|||
|
|
statusColor = .red
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
guard resolvedAddress != (engine.address ?? "") else {
|
|||
|
|
statusText = "Нельзя отправить себе"
|
|||
|
|
statusColor = .orange
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
step = .preConfirm
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Шаг 2 → 3: Реальная отправка после подтверждения
|
|||
|
|
private func executeTransfer() {
|
|||
|
|
guard !isSending else { return }
|
|||
|
|
guard let amt = Int(amount), amt > 0 else { return }
|
|||
|
|
guard !resolvedAddress.isEmpty else { return }
|
|||
|
|
|
|||
|
|
isSending = true
|
|||
|
|
statusText = ""
|
|||
|
|
Task { @MainActor in
|
|||
|
|
defer { isSending = false }
|
|||
|
|
do {
|
|||
|
|
try await engine.api.transfer(
|
|||
|
|
from: engine.address ?? "",
|
|||
|
|
to: resolvedAddress,
|
|||
|
|
amount: amt
|
|||
|
|
)
|
|||
|
|
await engine.syncBalance()
|
|||
|
|
let df = DateFormatter()
|
|||
|
|
df.dateFormat = "dd.MM.yyyy HH:mm:ss"
|
|||
|
|
sentAmount = amt
|
|||
|
|
sentRecipient = resolvedAlias.isEmpty ? displayAddr(resolvedAddress) : resolvedAlias
|
|||
|
|
sentTimestamp = df.string(from: Date())
|
|||
|
|
step = .result
|
|||
|
|
} catch {
|
|||
|
|
statusText = "Ошибка отправки. Попробуйте позже"
|
|||
|
|
statusColor = .red
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|