Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1207,7 +1207,7 @@
repositoryURL = "https://github.com/synonymdev/bitkit-core";
requirement = {
kind = exactVersion;
version = 0.4.2;
version = 0.5.3;
};
};
96E20CD22CB6D91A00C24149 /* XCRemoteSwiftPackageReference "CodeScanner" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,10 @@ struct AppScene: View {
// Start watching pending orders after wallet is ready
await blocktank.startWatchingPendingOrders(transferViewModel: transfer)

// Open the swap updates stream so any pending LN -> onchain swaps resume
// and auto-claim once their lockup confirms. Retries until the stream starts.
wallet.ensureSwapUpdatesRunning()

// Schedule full backup after wallet create/restore to prevent epoch dates in backup status
await BackupService.shared.scheduleFullBackup()
} catch {
Expand Down
94 changes: 94 additions & 0 deletions Bitkit/Components/AmountSlider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import SwiftUI

/// Continuous slider over a `minValue`...`maxValue` range, styled to match `CustomSlider`
/// (same track and thumb) but without discrete steps. Used to pick a transfer amount
/// within its allowed limits.
struct AmountSlider: View {
@Binding var value: UInt64
let minValue: UInt64
let maxValue: UInt64

@State private var sliderWidth: CGFloat = 0

private var fraction: CGFloat {
guard maxValue > minValue else { return 0 }
let clamped = min(max(value, minValue), maxValue)
return CGFloat(clamped - minValue) / CGFloat(maxValue - minValue)
}

private func value(at position: CGFloat) -> UInt64 {
guard sliderWidth > 0, maxValue > minValue else { return minValue }
let normalized = min(max(position / sliderWidth, 0), 1)
return minValue + UInt64((Double(maxValue - minValue) * Double(normalized)).rounded())
}

var body: some View {
Rectangle()
.fill(Color.clear)
.frame(height: 32)
.overlay(
ZStack {
// Track background
Rectangle()
.fill(Color.green32)
.frame(height: 8)
.cornerRadius(8)

// Active track (from start to current position)
Rectangle()
.fill(Color.greenAccent)
.frame(width: fraction * sliderWidth, height: 8)
.cornerRadius(8)
.frame(maxWidth: .infinity, alignment: .leading)

// Slider thumb
Circle()
.fill(Color.greenAccent)
.frame(width: 32, height: 32)
.overlay(
Circle()
.fill(Color.white)
.frame(width: 16, height: 16)
)
.position(x: fraction * sliderWidth, y: 16)
.allowsHitTesting(false)
}
)
.contentShape(Rectangle())
.background(
GeometryReader { geometry in
Color.clear
.onAppear {
sliderWidth = geometry.size.width
}
.onChange(of: geometry.size.width) { _, width in
sliderWidth = width
}
}
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { gesture in
guard sliderWidth > 0 else { return }
value = value(at: gesture.location.x)
}
)
}
}

#Preview {
struct PreviewWrapper: View {
@State private var value: UInt64 = 72000

var body: some View {
VStack {
AmountSlider(value: $value, minValue: 50000, maxValue: 100_000)
Text("\(value) sats")
}
.padding(32)
}
}

return PreviewWrapper()
.preferredColorScheme(.dark)
}
16 changes: 10 additions & 6 deletions Bitkit/Components/SwipeButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ import SwiftUI
struct SwipeButton: View {
let title: String
let accentColor: Color
/// Blocks the swipe and shows the knob spinner while a prerequisite is still loading.
var isLoading = false
/// Optional binding for swipe progress (0...1), e.g. to drive animations in the parent.
var swipeProgress: Binding<CGFloat>?
let onComplete: () async throws -> Void

@State private var offset: CGFloat = 0
@State private var isLoading = false
@State private var isSubmitting = false

private var isBusy: Bool { isLoading || isSubmitting }

private let buttonHeight: CGFloat = 76
private let innerPadding: CGFloat = 16
Expand Down Expand Up @@ -49,7 +53,7 @@ struct SwipeButton: View {
.frame(width: buttonHeight - innerPadding, height: buttonHeight - innerPadding)
.overlay(
ZStack {
if isLoading {
if isBusy {
ActivityIndicator(theme: .dark)
} else {
Image("arrow-right")
Expand All @@ -72,21 +76,21 @@ struct SwipeButton: View {
.gesture(
DragGesture()
.onChanged { value in
guard !isLoading else { return }
guard !isBusy else { return }
withAnimation(.interactiveSpring()) {
offset = value.translation.width
swipeProgress?.wrappedValue = max(0, min(1, offset / maxOffset))
}
}
.onEnded { _ in
guard !isLoading else { return }
guard !isBusy else { return }
withAnimation(.spring()) {
let threshold = geometry.size.width * 0.7
if offset > threshold {
Haptics.play(.medium)
offset = geometry.size.width - buttonHeight
swipeProgress?.wrappedValue = 1
isLoading = true
isSubmitting = true
Task { @MainActor in
do {
try await onComplete()
Expand All @@ -99,7 +103,7 @@ struct SwipeButton: View {

// Adjust the delay to match animation duration
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
isLoading = false
isSubmitting = false
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions Bitkit/Constants/Env.swift
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ enum Env {
return .bitcoin
}()

/// Whether LN -> onchain swaps can reach a Boltz backend. Boltz only serves a public API on
/// mainnet: its testnet deployment is deprecated and regtest resolves to a local backend that
/// no build of ours can reach. Elsewhere the transfer to savings closes a channel instead.
static var isSwapSupported: Bool { network == .bitcoin }

static let ldkLogLevel = LDKNode.LogLevel.trace

static let walletSyncIntervalSecs: UInt64 = 10 // TODO: play around with this
Expand Down
21 changes: 21 additions & 0 deletions Bitkit/Extensions/PaymentFailureReason+UserMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import LDKNode

extension PaymentFailureReason {
/// User-facing message for a failed payment, mirroring Android's
/// `PaymentFailureReason.toUserMessage`; reasons without a dedicated string fall back to
/// the generic payment-failed description.
static func userMessage(for reason: PaymentFailureReason?) -> String {
switch reason {
case .recipientRejected:
return t("wallet__toast_payment_failed_recipient_rejected")
case .retriesExhausted:
return t("wallet__toast_payment_failed_retries_exhausted")
case .routeNotFound:
return t("wallet__toast_payment_failed_route_not_found")
case .paymentExpired:
return t("wallet__toast_payment_failed_timeout")
default:
return t("wallet__toast_payment_failed_description")
}
}
}
2 changes: 2 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ struct MainNavView: View {
case .probingTool: ProbingToolScreen()
case .legacyRnRecovery: LegacyRnRecoveryScreen()
case .orders: ChannelOrders()
case .swaps: SwapsListView()
case let .swapDetail(id): SwapDetailView(swapId: id)
case .logs: LogView()
case .trezor: TrezorRootView()
}
Expand Down
11 changes: 11 additions & 0 deletions Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -221,11 +221,19 @@
"lightning__availability__text" = "Funds transfer to savings is usually instant, but settlement may take up to <accent>14 days</accent> under certain network conditions.";
"lightning__savings_confirm__label" = "Transfer to savings";
"lightning__savings_confirm__transfer_all" = "Transfer all";
"lightning__savings_confirm__amount" = "To savings";
"lightning__savings_confirm__amount_too_low" = "Amount is too low to transfer to savings.";
"lightning__savings_confirm__close_instead" = "Close channel instead";
"lightning__savings_confirm__network_fee" = "Network fee";
"lightning__savings_confirm__receive" = "You'll receive";
"lightning__savings_confirm__service_fee" = "Service fee";
"lightning__savings_advanced__title" = "Select funds\n<accent>to transfer</accent>";
"lightning__savings_advanced__text" = "You can transfer part of your spending balance to savings, because you have multiple active Lightning Connections.";
"lightning__savings_advanced__total" = "Total selected";
"lightning__savings_progress__title" = "Funds\n<accent>in transfer</accent>";
"lightning__savings_progress__text" = "Please wait, your funds transfer is in progress. This should take <accent>±10 seconds.</accent>";
"lightning__savings_settling__title" = "Transfer\n<accent>on the way</accent>";
"lightning__savings_settling__text" = "Your transfer has been initiated and is <accent>settling on-chain</accent>. Your savings balance will update automatically once it completes.";
"lightning__savings_interrupted__nav_title" = "Transfer\n<accent>interrupted</accent>";
"lightning__savings_interrupted__title" = "Keep Bitkit\n<accent>open</accent>";
"lightning__savings_interrupted__text" = "Funds were not transferred yet. Bitkit will try to initiate the transfer during the next <accent>30 minutes</accent>. Please keep your app open.";
Expand Down Expand Up @@ -1299,6 +1307,9 @@
"wallet__toast_payment_failed_title" = "Payment Failed";
"wallet__toast_payment_failed_description" = "Your instant payment failed. Please try again.";
"wallet__toast_payment_failed_timeout" = "Payment timed out. Please try again.";
"wallet__toast_payment_failed_recipient_rejected" = "The recipient rejected this payment. Try a different amount.";
"wallet__toast_payment_failed_retries_exhausted" = "Could not find a route with sufficient liquidity. Try a smaller amount or wait and try again.";
"wallet__toast_payment_failed_route_not_found" = "Could not find a payment path to the recipient.";
"wallet__toast_received_transaction_replaced_title" = "Received Transaction Replaced";
"wallet__toast_received_transaction_replaced_description" = "Your received transaction was replaced by a fee bump";
"wallet__toast_transaction_replaced_title" = "Transaction Replaced";
Expand Down
Loading
Loading