diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift index 4b0268ce6..596e47020 100644 --- a/Bitkit/AppScene.swift +++ b/Bitkit/AppScene.swift @@ -97,6 +97,7 @@ struct AppScene: View { if UserDefaults.standard.bool(forKey: "pinOnLaunch") && settings.pinEnabled { isPinVerified = false } + if migrations.needsPostMigrationSync { app.toast( type: .warning, diff --git a/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/Contents.json new file mode 100644 index 000000000..e84178605 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "arrow-counter-clockwise.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/arrow-counter-clockwise.pdf b/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/arrow-counter-clockwise.pdf new file mode 100644 index 000000000..6c28c8d72 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/arrow-counter-clockwise.imageset/arrow-counter-clockwise.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/barcode.imageset /Contents.json b/Bitkit/Assets.xcassets/icons/barcode.imageset /Contents.json new file mode 100644 index 000000000..6e7a24260 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/barcode.imageset /Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "x-mark.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/barcode.imageset /barcode.pdf b/Bitkit/Assets.xcassets/icons/barcode.imageset /barcode.pdf new file mode 100644 index 000000000..5004953a3 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/barcode.imageset /barcode.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/broadcast.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/broadcast.imageset/Contents.json new file mode 100644 index 000000000..8e5a73516 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/broadcast.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "broadcast.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/broadcast.imageset/broadcast.pdf b/Bitkit/Assets.xcassets/icons/broadcast.imageset/broadcast.pdf new file mode 100644 index 000000000..2817a1137 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/broadcast.imageset/broadcast.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/Contents.json new file mode 100644 index 000000000..528c5fb61 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "caret-double-right.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/caret-double-right.pdf b/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/caret-double-right.pdf new file mode 100644 index 000000000..154d89b67 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/caret-double-right.imageset/caret-double-right.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/cube.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/cube.imageset/Contents.json new file mode 100644 index 000000000..9c36c08b7 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/cube.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "cube.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/cube.imageset/cube.pdf b/Bitkit/Assets.xcassets/icons/cube.imageset/cube.pdf new file mode 100644 index 000000000..75afbf287 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/cube.imageset/cube.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/database.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/database.imageset/Contents.json new file mode 100644 index 000000000..d11044b66 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/database.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "database.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/database.imageset/database.pdf b/Bitkit/Assets.xcassets/icons/database.imageset/database.pdf new file mode 100644 index 000000000..a7325b39e Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/database.imageset/database.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/Contents.json new file mode 100644 index 000000000..1486dd2e4 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "device-mobile-speaker.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/device-mobile-speaker.pdf b/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/device-mobile-speaker.pdf new file mode 100644 index 000000000..94677a2ab Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/device-mobile-speaker.imageset/device-mobile-speaker.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/eye-slash.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/eye-slash.imageset/Contents.json new file mode 100644 index 000000000..969759f92 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/eye-slash.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "eye-slash.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/eye-slash.imageset/eye-slash.pdf b/Bitkit/Assets.xcassets/icons/eye-slash.imageset/eye-slash.pdf new file mode 100644 index 000000000..3690f6ae0 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/eye-slash.imageset/eye-slash.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/file-text.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/file-text.imageset/Contents.json new file mode 100644 index 000000000..5064d8d33 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/file-text.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "file-text.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/file-text.imageset/file-text.pdf b/Bitkit/Assets.xcassets/icons/file-text.imageset/file-text.pdf new file mode 100644 index 000000000..3d68de81a Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/file-text.imageset/file-text.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/git-branch.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/git-branch.imageset/Contents.json new file mode 100644 index 000000000..2941a65ec --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/git-branch.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "git-branch.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/git-branch.imageset/git-branch.pdf b/Bitkit/Assets.xcassets/icons/git-branch.imageset/git-branch.pdf new file mode 100644 index 000000000..05bf87c9f Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/git-branch.imageset/git-branch.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/git-diff.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/git-diff.imageset/Contents.json new file mode 100644 index 000000000..416ad0367 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/git-diff.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "git-diff.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/git-diff.imageset/git-diff.pdf b/Bitkit/Assets.xcassets/icons/git-diff.imageset/git-diff.pdf new file mode 100644 index 000000000..79e7a522a Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/git-diff.imageset/git-diff.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/Contents.json new file mode 100644 index 000000000..798be8db4 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "hand-pointing.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/hand-pointing.pdf b/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/hand-pointing.pdf new file mode 100644 index 000000000..edbe95255 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/hand-pointing.imageset/hand-pointing.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/hard-drives.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/hard-drives.imageset/Contents.json new file mode 100644 index 000000000..b320970d6 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/hard-drives.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "hard-drives.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/hard-drives.imageset/hard-drives.pdf b/Bitkit/Assets.xcassets/icons/hard-drives.imageset/hard-drives.pdf new file mode 100644 index 000000000..0939363b4 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/hard-drives.imageset/hard-drives.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/list-dashes.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/list-dashes.imageset/Contents.json new file mode 100644 index 000000000..7da1884ca --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/list-dashes.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "list-dashes.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/list-dashes.imageset/list-dashes.pdf b/Bitkit/Assets.xcassets/icons/list-dashes.imageset/list-dashes.pdf new file mode 100644 index 000000000..4e7f8e65a Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/list-dashes.imageset/list-dashes.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/lock-key.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/lock-key.imageset/Contents.json new file mode 100644 index 000000000..0100d4a72 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/lock-key.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "lock-key.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/lock-key.imageset/lock-key.pdf b/Bitkit/Assets.xcassets/icons/lock-key.imageset/lock-key.pdf new file mode 100644 index 000000000..ce72eab32 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/lock-key.imageset/lock-key.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/question.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/question.imageset/Contents.json new file mode 100644 index 000000000..fd3293f15 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/question.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "question.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/question.imageset/question.pdf b/Bitkit/Assets.xcassets/icons/question.imageset/question.pdf new file mode 100644 index 000000000..9aac37522 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/question.imageset/question.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/smiley.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/smiley.imageset/Contents.json new file mode 100644 index 000000000..824138d01 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/smiley.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "smiley.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/smiley.imageset/smiley.pdf b/Bitkit/Assets.xcassets/icons/smiley.imageset/smiley.pdf new file mode 100644 index 000000000..19344ec43 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/smiley.imageset/smiley.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/stop-circle.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/stop-circle.imageset/Contents.json new file mode 100644 index 000000000..0d8d7715c --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/stop-circle.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "stop-circle.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/stop-circle.imageset/stop-circle.pdf b/Bitkit/Assets.xcassets/icons/stop-circle.imageset/stop-circle.pdf new file mode 100644 index 000000000..59047f7b6 Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/stop-circle.imageset/stop-circle.pdf differ diff --git a/Bitkit/Assets.xcassets/icons/translate.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/translate.imageset/Contents.json new file mode 100644 index 000000000..e4a49f0d5 --- /dev/null +++ b/Bitkit/Assets.xcassets/icons/translate.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "translate.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Bitkit/Assets.xcassets/icons/translate.imageset/translate.pdf b/Bitkit/Assets.xcassets/icons/translate.imageset/translate.pdf new file mode 100644 index 000000000..d775d9d9e Binary files /dev/null and b/Bitkit/Assets.xcassets/icons/translate.imageset/translate.pdf differ diff --git a/Bitkit/Assets.xcassets/synonym-logo.imageset/Contents.json b/Bitkit/Assets.xcassets/synonym-logo.imageset/Contents.json new file mode 100644 index 000000000..3336f23a9 --- /dev/null +++ b/Bitkit/Assets.xcassets/synonym-logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "synonym-logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Bitkit/Assets.xcassets/synonym-logo.imageset/synonym-logo.png b/Bitkit/Assets.xcassets/synonym-logo.imageset/synonym-logo.png new file mode 100644 index 000000000..1d871d71c Binary files /dev/null and b/Bitkit/Assets.xcassets/synonym-logo.imageset/synonym-logo.png differ diff --git a/Bitkit/Assets.xcassets/tether-logo.imageset/Contents.json b/Bitkit/Assets.xcassets/tether-logo.imageset/Contents.json new file mode 100644 index 000000000..2f6041ed0 --- /dev/null +++ b/Bitkit/Assets.xcassets/tether-logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "tether-logo.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Bitkit/Assets.xcassets/tether-logo.imageset/tether-logo.png b/Bitkit/Assets.xcassets/tether-logo.imageset/tether-logo.png new file mode 100644 index 000000000..efbb0d23b Binary files /dev/null and b/Bitkit/Assets.xcassets/tether-logo.imageset/tether-logo.png differ diff --git a/Bitkit/Components/DrawerView.swift b/Bitkit/Components/DrawerView.swift index 9dd6733a8..bdbb41df2 100644 --- a/Bitkit/Components/DrawerView.swift +++ b/Bitkit/Components/DrawerView.swift @@ -7,6 +7,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable { case profile case widgets case shop + case support case settings case appStatus @@ -22,6 +23,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable { case .profile: return "user-square" case .widgets: return "stack" case .shop: return "storefront" + case .support: return "chat" case .settings: return "gear-six" case .appStatus: return "status-circle" } @@ -35,6 +37,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable { case .profile: return t("wallet__drawer__profile") case .widgets: return t("wallet__drawer__widgets") case .shop: return t("wallet__drawer__shop") + case .support: return t("wallet__drawer__support") case .settings: return t("wallet__drawer__settings") case .appStatus: return t("settings__status__title") } @@ -57,6 +60,7 @@ enum DrawerMenuItem: Int, CaseIterable, Identifiable, Hashable { case .profile: return "DrawerProfile" case .widgets: return "DrawerWidgets" case .shop: return "DrawerShop" + case .support: return "DrawerSupport" case .settings: return "DrawerSettings" case .appStatus: return "DrawerAppStatus" } @@ -99,9 +103,10 @@ struct DrawerView: View { case .activity: return .activityList case .contacts: return .contacts case .profile: return .profile - case .settings: return .settings - case .shop: return app.hasSeenShopIntro ? .shopDiscover : .shopIntro case .widgets: return app.hasSeenWidgetsIntro ? .widgetsList : .widgetsIntro + case .shop: return app.hasSeenShopIntro ? .shopDiscover : .shopIntro + case .support: return .support + case .settings: return .settings case .appStatus: return .appStatus } } @@ -178,7 +183,7 @@ struct DrawerView: View { .transition(.move(edge: .trailing)) } } - .onChange(of: app.showDrawer) { show in + .onChange(of: app.showDrawer) { _, show in if show { currentDragOffset = 0 withAnimation(.easeOut(duration: 0.25)) { diff --git a/Bitkit/Components/SegmentedControl.swift b/Bitkit/Components/SegmentedControl.swift index fcf4c67a9..99260e1d4 100644 --- a/Bitkit/Components/SegmentedControl.swift +++ b/Bitkit/Components/SegmentedControl.swift @@ -16,13 +16,13 @@ struct SegmentedControl: View { private let defaultActiveColor: Color @Namespace private var underlineNamespace - init(selectedTab: Binding, tabs: [T], activeColor: Color = .brandAccent) { + init(selectedTab: Binding, tabs: [T], activeColor: Color = .textPrimary) { _selectedTab = selectedTab tabItems = tabs.map { TabItem($0) } defaultActiveColor = activeColor } - init(selectedTab: Binding, tabItems: [TabItem], defaultActiveColor: Color = .brandAccent) { + init(selectedTab: Binding, tabItems: [TabItem], defaultActiveColor: Color = .textPrimary) { _selectedTab = selectedTab self.tabItems = tabItems self.defaultActiveColor = defaultActiveColor @@ -32,7 +32,7 @@ struct SegmentedControl: View { HStack(spacing: 8) { ForEach(tabItems, id: \.tab) { tabItem in Button(action: { - withAnimation(.spring(response: 0.35, dampingFraction: 0.8)) { + withAnimation(.easeInOut(duration: 0.2)) { selectedTab = tabItem.tab } }) { @@ -43,6 +43,7 @@ struct SegmentedControl: View { Rectangle() .frame(height: 2) .foregroundColor(Color.white64) + if selectedTab == tabItem.tab { Rectangle() .frame(height: 2) @@ -51,7 +52,7 @@ struct SegmentedControl: View { } } } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) diff --git a/Bitkit/Components/SettingsListLabel.swift b/Bitkit/Components/SettingsRow.swift similarity index 69% rename from Bitkit/Components/SettingsListLabel.swift rename to Bitkit/Components/SettingsRow.swift index 38328a2bf..f3a11e3f6 100644 --- a/Bitkit/Components/SettingsListLabel.swift +++ b/Bitkit/Components/SettingsRow.swift @@ -1,15 +1,32 @@ import SwiftUI -enum SettingsListRightIcon { +/// Section header for settings screens +struct SettingsSectionHeader: View { + let title: String + + init(_ title: String) { + self.title = title + } + + var body: some View { + CaptionMText(title) + .frame(height: 50) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + } +} + +enum SettingsRowRightIcon { case chevron case checkmark } -struct SettingsListLabel: View { +struct SettingsRow: View { let title: String let iconName: String? + let iconColor: Color? let rightText: String? - let rightIcon: SettingsListRightIcon? + let rightIcon: SettingsRowRightIcon? let toggle: Binding? let disabled: Bool? let testIdentifier: String? @@ -17,14 +34,16 @@ struct SettingsListLabel: View { init( title: String, iconName: String? = nil, + iconColor: Color? = .brandAccent, rightText: String? = nil, - rightIcon: SettingsListRightIcon? = .chevron, + rightIcon: SettingsRowRightIcon? = .chevron, toggle: Binding? = nil, disabled: Bool? = nil, testIdentifier: String? = nil ) { self.title = title self.iconName = iconName + self.iconColor = iconColor self.rightText = rightText self.rightIcon = rightIcon self.toggle = toggle @@ -36,16 +55,12 @@ struct SettingsListLabel: View { VStack(spacing: 0) { HStack(alignment: .center, spacing: 0) { if let iconName { - Label { - BodyMText(title, textColor: .textPrimary) - } icon: { - CircularIcon(icon: iconName, iconColor: .textPrimary) - .padding(.trailing, 8) - } - } else { - BodyMText(title, textColor: .textPrimary) + CircularIcon(icon: iconName, iconColor: iconColor ?? .brandAccent, backgroundColor: .black) + .padding(.trailing, 8) } + BodyMText(title, textColor: .textPrimary) + Spacer() if let toggle { @@ -57,7 +72,7 @@ struct SettingsListLabel: View { } else { if let rightText { - BodyMText(rightText, textColor: .textPrimary) + BodyMText(rightText, textColor: .textSecondary) .padding(.trailing, 5) .accessibilityIdentifier("Value") } @@ -80,10 +95,7 @@ struct SettingsListLabel: View { } .frame(height: 50) - // Bottom border - Rectangle() - .fill(Color.white10) - .frame(height: 1) + CustomDivider() } } } diff --git a/Bitkit/Components/Social.swift b/Bitkit/Components/Social.swift index 63b873cf8..09f9082ab 100644 --- a/Bitkit/Components/Social.swift +++ b/Bitkit/Components/Social.swift @@ -3,11 +3,7 @@ import SwiftUI struct Social: View { @Environment(\.openURL) private var openURL - let backgroundColor: Color - - init(backgroundColor: Color = .white16) { - self.backgroundColor = backgroundColor - } + let backgroundColor: Color = .clear var body: some View { HStack { diff --git a/Bitkit/Extensions/String+Utilities.swift b/Bitkit/Extensions/String+Utilities.swift index 454f6efdf..2c14c562f 100644 --- a/Bitkit/Extensions/String+Utilities.swift +++ b/Bitkit/Extensions/String+Utilities.swift @@ -1,15 +1,31 @@ import Foundation extension String { - /// Truncates a string to a maximum length and adds ellipsis in the middle - /// - Parameter maxLength: The maximum length of the string - /// - Returns: The truncated string with ellipsis in the middle - func ellipsis(maxLength: Int) -> String { + enum EllipsisStyle { + /// Ellipsis in the middle: "ab...de" + case middle + /// Ellipsis at the end: "abcde..." + case end + } + + /// Truncates a string to a maximum length and adds ellipsis. + /// - Parameters: + /// - maxLength: The maximum length of the string + /// - style: `.middle` (default) keeps start and end with "..." in between; `.end` keeps prefix and "..." at the end + /// - Returns: The truncated string with ellipsis + func ellipsis(maxLength: Int, style: EllipsisStyle = .middle) -> String { if count <= maxLength { return self } - let start = prefix(maxLength / 2) - let end = suffix(maxLength / 2) - return "\(start)...\(end)" + + switch style { + case .middle: + let half = maxLength / 2 + let start = prefix(half) + let end = suffix(half) + return "\(start)...\(end)" + case .end: + return String(prefix(maxLength)) + "..." + } } } diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index f3f5725e6..d029eebf1 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -320,14 +320,8 @@ struct MainNavView: View { case let .widgetEdit(widgetType): WidgetEditView(id: widgetType) // Settings - case .settings: MainSettings() - case .generalSettings: GeneralSettingsView() - case .securitySettings: SecurityPrivacySettingsView() - case .backupSettings: BackupSettings() - case .advancedSettings: AdvancedSettingsView() - case .support: SupportView() - case .about: AboutView() - case .devSettings: DevSettingsView() + case .settings: MainSettingsScreen() + case .support: SupportScreen() // General settings case .languageSettings: LanguageSettingsScreen() @@ -338,16 +332,16 @@ struct MainNavView: View { case .quickpayIntro: QuickpayIntroView() case .customSpeedSettings: CustomSpeedView() case .tagSettings: TagSettingsView() - case .widgetsSettings: WidgetsSettingsView() + case .widgetsSettings: WidgetsSettingsScreen() case .notifications: NotificationsSettings() case .notificationsIntro: NotificationsIntro() // Security settings - case .disablePin: DisablePinView() - case .changePin: PinChangeView() + case .changePin: ChangePinScreen() // Backup settings - case .resetAndRestore: ResetAndRestore() + case .dataBackups: DataBackupsScreen() + case .reset: ResetScreen() // Support settings case .reportIssue: ReportIssue() @@ -363,9 +357,10 @@ struct MainNavView: View { case .electrumSettings: ElectrumSettingsScreen() case .rgsSettings: RgsSettingsScreen() case .addressViewer: AddressViewer() + case .devSettings: DevSettingsView() // Dev settings - case .blocktankRegtest: BlocktankRegtestView() + case .blocktankRegtest: BlocktankRegtestScreen() case .ldkDebug: LdkDebugScreen() case .vssDebug: VssDebugScreen() case .probingTool: ProbingToolScreen() diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index a508858dc..729fda157 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -532,6 +532,7 @@ "security__pin_not_match" = "Try again, this is not the same PIN."; "security__pin_disable_title" = "Disable PIN"; "security__pin_disable_text" = "PIN code is currently enabled. If you want to disable your PIN, you need to enter your current PIN code first."; +"security__pin_enable_button" = "Enable PIN"; "security__pin_disable_button" = "Disable PIN"; "security__pin_enter" = "Please enter your PIN code"; "security__pin_last_attempt" = "Last attempt. Entering the wrong PIN again will reset your wallet."; @@ -612,13 +613,10 @@ "settings__dev_disabled_title" = "Dev Options Disabled"; "settings__dev_disabled_message" = "Developer options are now disabled throughout the app."; "settings__general_title" = "General"; -"settings__security_title" = "Security and Privacy"; -"settings__backup_title" = "Back up or Restore"; +"settings__security_title" = "Security"; "settings__advanced_title" = "Advanced"; -"settings__about_title" = "About"; +"settings__data_backups_nav_title" = "Data Backups"; "settings__support_title" = "Support"; -"settings__about__title" = "About Bitkit"; -"settings__about__text" = "Thank you for being a responsible Bitcoiner.\nChange your wallet, change the world.\n\nBitkit hands you the keys to your money, profile, contacts, and web accounts.\n\nBitkit was crafted by Synonym Software Ltd."; "settings__about__legal" = "Legal"; "settings__about__share" = "Share"; "settings__about__version" = "Version"; @@ -637,7 +635,7 @@ "settings__general__denomination_label" = "Bitcoin denomination"; "settings__general__denomination_modern" = "Modern (₿ 10 000)"; "settings__general__denomination_classic" = "Classic (₿ 0.00010000)"; -"settings__general__speed" = "Transaction speed"; +"settings__general__speed" = "Transaction Speed"; "settings__general__speed_title" = "Transaction Speed"; "settings__general__speed_default" = "Default Transaction Speed"; "settings__general__speed_fee_custom" = "Set Custom Fee"; @@ -648,9 +646,17 @@ "settings__general__language" = "Language"; "settings__general__language_title" = "Language"; "settings__general__language_other" = "Interface language"; -"settings__widgets__nav_title" = "Widgets and Suggestions"; -"settings__widgets__showWidgets" = "Widgets and Suggestions"; +"settings__general__section_interface" = "Interface"; +"settings__general__section_payments" = "Payments"; +"settings__widgets__nav_title" = "Widgets"; +"settings__widgets__section_display" = "Display"; +"settings__widgets__section_reset" = "Reset To Defaults"; +"settings__widgets__showWidgets" = "Show Widgets"; "settings__widgets__showWidgetTitles" = "Show Widget Titles"; +"settings__widgets__reset_widgets" = "Reset Widgets"; +"settings__widgets__reset_widgets_dialog_title" = "Reset Widgets?"; +"settings__widgets__reset_widgets_dialog_description" = "Are you sure you want to reset the widgets? The default widget set with default configurations will be displayed."; +"settings__widgets__reset_suggestions" = "Reset Suggestions Cards"; "settings__notifications__nav_title" = "Background Payments"; "settings__notifications__intro__title" = "Get Paid\nPassively"; "settings__notifications__intro__text" = "Turn on notifications to get paid, even when your Bitkit app is closed."; @@ -682,17 +688,22 @@ "settings__security__warn_100" = "Warn when sending over $100"; "settings__security__pin" = "PIN Code"; "settings__security__pin_change" = "Change PIN Code"; +"settings__security__section_backup" = "Back up or reset"; +"settings__security__section_privacy" = "Privacy"; +"settings__security__section_safety" = "Safety"; +"settings__security__section_pin" = "PIN Code"; "settings__security__pin_launch" = "Require PIN on launch"; "settings__security__pin_idle" = "Require PIN when idle"; "settings__security__pin_payments" = "Require PIN for payments"; "settings__security__pin_enabled" = "Enabled"; "settings__security__pin_disabled" = "Disabled"; -"settings__security__use_bio" = "Use {biometryTypeName} instead"; +"settings__security__use_bio" = "{biometryTypeName} instead of PIN"; "settings__security__footer" = "When enabled, you can use {biometryTypeName} instead of your PIN code to unlock your wallet or send payments."; -"settings__backup__title" = "Back Up Or Restore"; +"settings__reset_nav_title" = "Reset"; "settings__backup__wallet" = "Back up your wallet"; -"settings__backup__export" = "Export wallet data to phone"; -"settings__backup__reset" = "Reset and restore wallet"; +"settings__backup__data" = "Data backups"; +"settings__backup__export" = "Export wallet data"; +"settings__backup__reset" = "Reset wallet"; "settings__backup__failed_title" = "Data Backup Failure"; "settings__backup__failed_message" = "Bitkit failed to back up wallet data. Retrying in {interval, plural, one {# minute} other {# minutes}}."; "settings__backup__latest" = "latest data backups"; @@ -708,7 +719,7 @@ "settings__backup__category_profile" = "Profile"; "settings__backup__category_contacts" = "Contacts"; "settings__support__title" = "Support"; -"settings__support__text" = "Need help? Report your issue from within Bitkit, visit the help center, check the status, or reach out to us on our social channels."; +"settings__support__text" = "Need help? Report your issue from within Bitkit or visit our help center."; "settings__support__report" = "Report Issue"; "settings__support__help" = "Help Center"; "settings__support__status" = "App Status"; @@ -745,9 +756,10 @@ "settings__status__backup__ready" = "Backed up"; "settings__status__backup__pending" = "Backing up..."; "settings__status__backup__error" = "Failed to complete a full backup"; +"settings__adv__section_debug" = "Debug"; "settings__adv__section_payments" = "Payments"; "settings__adv__section_networks" = "Networks"; -"settings__adv__section_other" = "Other"; +"settings__adv__address_type_title" = "Address Type"; "settings__adv__address_type" = "Bitcoin Address Type"; "settings__adv__monitored_address_types" = "Monitored Address Types"; "settings__adv__addr_type_failed_title" = "Failed"; @@ -967,6 +979,7 @@ "wallet__drawer__widgets" = "Widgets"; "wallet__drawer__shop" = "Shop"; "wallet__drawer__settings" = "Settings"; +"wallet__drawer__support" = "Support"; "wallet__drawer__status" = "App Status"; "wallet__send" = "Send"; "wallet__receive" = "Receive"; diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift index b43a79bc0..942182348 100644 --- a/Bitkit/ViewModels/NavigationViewModel.swift +++ b/Bitkit/ViewModels/NavigationViewModel.swift @@ -33,6 +33,7 @@ enum Route: Hashable { case savingsAdvanced case savingsProgress case scanner + case support // Shop case shopIntro @@ -46,37 +47,33 @@ enum Route: Hashable { case widgetDetail(WidgetType) case widgetEdit(WidgetType) - // Main Settings - case settings - case generalSettings - case securitySettings - case backupSettings - case advancedSettings - case support - case about - case devSettings + // Support + case reportIssue + case appStatus - // General settings + // Settings + // General/Interface + case settings case languageSettings case currencySettings case unitSettings - case transactionSpeedSettings - case customSpeedSettings case tagSettings case widgetsSettings + + // General/Payments + case transactionSpeedSettings + case customSpeedSettings case quickpay case quickpayIntro case notifications case notificationsIntro - // Security settings - case disablePin + // Security + case dataBackups + case reset case changePin - /// Backup settings - case resetAndRestore - - // Advanced settings + // Advanced/Payments case coinSelection case addressTypePreference case connections @@ -86,10 +83,7 @@ enum Route: Hashable { case electrumSettings case rgsSettings case addressViewer - - // Support settings - case reportIssue - case appStatus + case devSettings // Dev settings case blocktankRegtest diff --git a/Bitkit/Views/Settings/AboutView.swift b/Bitkit/Views/Settings/AboutView.swift deleted file mode 100644 index a8d5c8766..000000000 --- a/Bitkit/Views/Settings/AboutView.swift +++ /dev/null @@ -1,104 +0,0 @@ -import SwiftUI - -struct DiagonalCut: Shape { - func path(in rect: CGRect) -> Path { - var path = Path() - - let leftCutX = rect.maxX * 0.15 - path.move(to: CGPoint(x: leftCutX, y: rect.maxY)) - - let topCutY = rect.maxY * 0.63 - path.addLine(to: CGPoint(x: rect.maxX, y: topCutY)) - - // Line to the top-right corner - path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) - // Line to the bottom-right corner - path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY)) - // Line to the bottom-left corner - path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY)) - // Close the path back to the starting point - path.closeSubpath() - - return path - } -} - -struct AboutView: View { - @Environment(\.openURL) private var openURL - - private var appVersion: String { - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" - let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" - return "\(version) (\(build))" - } - - private var shareText: String { - return t( - "settings__about__shareText", - variables: ["appStoreUrl": Env.appStoreUrl, "playStoreUrl": Env.playStoreUrl] - ) - } - - var body: some View { - ZStack { - // Orange diagonal background - Color.brandAccent - .clipShape(DiagonalCut()) - .ignoresSafeArea() - - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__about__title")) - .padding(.bottom, 16) - - BodyMText(t("settings__about__text")) - .padding(.vertical, 16) - - VStack(spacing: 0) { - Button(action: { - openURL(URL(string: Env.termsOfServiceUrl)!) - }) { - SettingsListLabel(title: t("settings__about__legal")) - } - - ShareLink(item: shareText, message: Text(shareText)) { - SettingsListLabel(title: t("settings__about__share")) - } - - Button(action: { - openURL(URL(string: Env.githubReleasesUrl)!) - }) { - SettingsListLabel( - title: t("settings__about__version"), - rightText: appVersion, - rightIcon: nil - ) - } - } - - Spacer(minLength: 32) - - VStack(alignment: .center, spacing: 0) { - Image("logo") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 82) - .accessibilityIdentifier("AboutLogo") - } - .frame(maxWidth: .infinity) - .padding(.bottom, 32) - - Social(backgroundColor: .clear) - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - } - } -} - -#Preview { - NavigationView { - AboutView() - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Settings/Advanced/AdvancedSettingsView.swift b/Bitkit/Views/Settings/Advanced/AdvancedSettingsView.swift index c693806d4..5d24873fd 100644 --- a/Bitkit/Views/Settings/Advanced/AdvancedSettingsView.swift +++ b/Bitkit/Views/Settings/Advanced/AdvancedSettingsView.swift @@ -1,113 +1,100 @@ import SwiftUI struct AdvancedSettingsView: View { - @EnvironmentObject var navigation: NavigationViewModel - @EnvironmentObject var suggestionsManager: SuggestionsManager - @EnvironmentObject var settings: SettingsViewModel - @State private var showingResetAlert = false + @EnvironmentObject private var settings: SettingsViewModel + @EnvironmentObject private var wallet: WalletViewModel + + @AppStorage("showDevSettings") private var showDevSettings = Env.isDebug var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__advanced_title")) - .padding(.bottom, 16) - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 16) { - // PAYMENTS Section - VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__adv__section_payments")) - .padding(.bottom, 8) - - NavigationLink(value: Route.addressTypePreference) { - SettingsListLabel( - title: t("settings__adv__address_type"), - rightText: settings.selectedAddressType.localizedTitle + VStack(alignment: .leading, spacing: 0) { + // Debug Section + if showDevSettings { + SettingsSectionHeader(t("settings__adv__section_debug")) + + NavigationLink(value: Route.devSettings) { + SettingsRow( + title: t("settings__dev_title"), + iconName: "game-controller" ) } - .accessibilityIdentifier("AddressTypePreference") - - NavigationLink(value: Route.coinSelection) { - SettingsListLabel(title: t("settings__adv__coin_selection")) - } - .accessibilityIdentifier("CoinSelectPreference") + .padding(.bottom, 16) + .accessibilityIdentifier("DevSettings") + } - // NavigationLink(destination: Text("Coming soon")) { - // SettingsListLabel(title: t("settings__adv__payment_preference")) - // } + // Payments section + SettingsSectionHeader(t("settings__adv__section_payments")) - // NavigationLink(destination: Text("Coming soon")) { - // SettingsListLabel(title: t("settings__adv__gap_limit")) - // } + NavigationLink(value: Route.addressTypePreference) { + SettingsRow( + title: t("settings__adv__address_type_title"), + iconName: "list-dashes", + rightText: settings.selectedAddressType.localizedTitle + ) } + .accessibilityIdentifier("AddressTypePreference") - // NETWORKS Section - VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__adv__section_networks")) - .padding(.top, 24) - .padding(.bottom, 8) - - NavigationLink(value: Route.connections) { - SettingsListLabel(title: t("settings__adv__lightning_connections")) - } - .accessibilityIdentifier("Channels") + NavigationLink(value: Route.coinSelection) { + SettingsRow( + title: t("settings__adv__coin_selection"), + iconName: "coins", + rightText: settings.coinSelectionMethod.localizedTitle + ) + } + .accessibilityIdentifier("CoinSelectPreference") - NavigationLink(value: Route.node) { - SettingsListLabel(title: t("settings__adv__lightning_node")) - } - .accessibilityIdentifier("LightningNodeInfo") + NavigationLink(value: Route.addressViewer) { + SettingsRow( + title: t("settings__adv__address_viewer"), + iconName: "eye" + ) + } + .accessibilityIdentifier("AddressViewer") - NavigationLink(value: Route.electrumSettings) { - SettingsListLabel(title: t("settings__adv__electrum_server")) - } - .accessibilityIdentifier("ElectrumConfig") + // Networks section + SettingsSectionHeader(t("settings__adv__section_networks")) + .padding(.top, 16) - NavigationLink(value: Route.rgsSettings) { - SettingsListLabel(title: t("settings__adv__rgs_server")) - } - .accessibilityIdentifier("RGSServer") + NavigationLink(value: Route.connections) { + SettingsRow( + title: t("settings__adv__lightning_connections"), + iconName: "bolt-hollow", + rightText: String(wallet.channels?.count ?? 0) + ) } + .accessibilityIdentifier("Channels") - // OTHER Section - VStack(alignment: .leading, spacing: 0) { - CaptionMText( - t("settings__adv__section_other") + NavigationLink(value: Route.node) { + SettingsRow( + title: t("settings__adv__lightning_node"), + iconName: "git-branch", + rightText: wallet.nodeId?.ellipsis(maxLength: 5, style: .end) ) - .padding(.top, 24) - .padding(.bottom, 8) - - NavigationLink(value: Route.addressViewer) { - SettingsListLabel(title: t("settings__adv__address_viewer")) - } - .accessibilityIdentifier("AddressViewer") - - // SettingsListLabel(title: t("settings__adv__rescan"), rightIcon: nil) + } + .accessibilityIdentifier("LightningNodeInfo") - Button(action: { - showingResetAlert = true - }) { - SettingsListLabel(title: t("settings__adv__suggestions_reset")) - } - .accessibilityIdentifier("ResetSuggestions") + NavigationLink(value: Route.electrumSettings) { + SettingsRow( + title: t("settings__adv__electrum_server"), + iconName: "hard-drives" + ) + } + .accessibilityIdentifier("ElectrumConfig") - Spacer() + NavigationLink(value: Route.rgsSettings) { + SettingsRow( + title: t("settings__adv__rgs_server"), + iconName: "broadcast" + ) } + .accessibilityIdentifier("RGSServer") } + .padding(.top, 16) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - .alert(t("settings__adv__reset_title"), isPresented: $showingResetAlert) { - Button(t("settings__adv__reset_confirm"), role: .destructive) { - suggestionsManager.resetDismissed() - navigation.reset() - } - .accessibilityIdentifier("DialogConfirm") - - Button(t("common__dialog_cancel"), role: .cancel) {} - .accessibilityIdentifier("DialogCancel") - } message: { - Text(t("settings__adv__reset_desc")) - } } } diff --git a/Bitkit/Views/Settings/Advanced/CoinSelectionSettingsView.swift b/Bitkit/Views/Settings/Advanced/CoinSelectionSettingsView.swift index b2587c31a..7a32a9b59 100644 --- a/Bitkit/Views/Settings/Advanced/CoinSelectionSettingsView.swift +++ b/Bitkit/Views/Settings/Advanced/CoinSelectionSettingsView.swift @@ -117,14 +117,12 @@ struct CoinSelectionSettingsView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: t("settings__adv__coin_selection")) - .padding(.bottom, 16) ScrollView(showsIndicators: false) { - VStack(spacing: 0) { + VStack(spacing: 32) { // COIN SELECTION METHOD Section VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__adv__cs_method")) - .padding(.bottom, 8) + SettingsSectionHeader(t("settings__adv__cs_method")) VStack(spacing: 0) { ForEach(CoinSelectionMethod.allCases, id: \.self) { method in @@ -136,9 +134,7 @@ struct CoinSelectionSettingsView: View { settingsViewModel.coinSelectionMethod = method } - if method != CoinSelectionMethod.allCases.last { - Divider() - } + CustomDivider() } } } @@ -147,9 +143,7 @@ struct CoinSelectionSettingsView: View { // AUTOPILOT MODE Section (only show if Autopilot is selected) if settingsViewModel.coinSelectionMethod == .autopilot { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__adv__cs_auto_mode")) - .padding(.top, 24) - .padding(.bottom, 8) + SettingsSectionHeader(t("settings__adv__cs_auto_mode")) VStack(spacing: 0) { ForEach(CoinSelectionAlgorithm.supportedAlgorithms, id: \.self) { algorithm in @@ -165,16 +159,12 @@ struct CoinSelectionSettingsView: View { } } } - - // Add spacing at the bottom - Spacer() - .frame(height: 32) } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() } } diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionDetailView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionDetailView.swift index 661cce2f3..f2b7aff4c 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionDetailView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionDetailView.swift @@ -66,12 +66,12 @@ struct LightningConnectionDetailView: View { ) .padding(.bottom, 28) - VStack(alignment: .leading, spacing: 32) { + VStack(alignment: .leading, spacing: 16) { // STATUS Section - VStack(alignment: .leading, spacing: 16) { - Divider() + VStack(alignment: .leading, spacing: 0) { + CustomDivider() - CaptionMText(t("lightning__status")) + SettingsSectionHeader(t("lightning__status")) HStack(alignment: .center, spacing: 8) { let status = detailedStatus(for: channel) @@ -84,15 +84,15 @@ struct LightningConnectionDetailView: View { BodyMSBText(status.text, textColor: status.color) } + .padding(.bottom, 16) - Divider() + CustomDivider() } // ORDER DETAILS Section if let order = channelDetails.linkedOrder { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("lightning__order_details")) - .padding(.bottom, 16) + SettingsSectionHeader(t("lightning__order_details")) DetailRow(label: t("lightning__order"), value: order.id) @@ -116,8 +116,7 @@ struct LightningConnectionDetailView: View { // BALANCE Section VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("lightning__balance")) - .padding(.bottom, 16) + SettingsSectionHeader(t("lightning__balance")) DetailRowWithAmount( label: t("lightning__receiving_label"), @@ -140,49 +139,45 @@ struct LightningConnectionDetailView: View { // FEES Section VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("lightning__fees")) - .padding(.bottom, 16) - + SettingsSectionHeader(t("lightning__fees")) DetailRowWithAmount(label: t("lightning__base_fee"), amount: UInt64(channel.forwardingFeeBaseMsat / 1000)) DetailRow(label: t("lightning__fee_rate"), value: "\(channel.forwardingFeeProportionalMillionths) ppm") } // OTHER Section - VStack(alignment: .leading, spacing: 16) { - CaptionMText(t("lightning__other")) - - VStack(spacing: 0) { - DetailRow( - label: t("lightning__is_usable"), - value: channel.isUsable ? t("common__yes") : t("common__no"), - valueTestId: channel.isUsable ? "IsUsableYes" : "IsUsableNo" - ) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader(t("lightning__other")) - // TODO: Add channel opening date - // if let formattedDate = formatDate(channel.fundingTxo) { - // DetailRow(label: t("lightning__opened_on"), value: formattedDate) - // } + DetailRow( + label: t("lightning__is_usable"), + value: channel.isUsable ? t("common__yes") : t("common__no"), + valueTestId: channel.isUsable ? "IsUsableYes" : "IsUsableNo" + ) - if let closedAt = channel.displayedClosedAt { - if let formattedCloseDate = formatDate(closedAt) { - DetailRow(label: t("lightning__closed_on"), value: formattedCloseDate) - } + // TODO: Add channel opening date + // if let formattedDate = formatDate(channel.fundingTxo) { + // DetailRow(label: t("lightning__opened_on"), value: formattedDate) + // } + + if let closedAt = channel.displayedClosedAt { + if let formattedCloseDate = formatDate(closedAt) { + DetailRow(label: t("lightning__closed_on"), value: formattedCloseDate) } + } - DetailRow(label: t("lightning__channel_id"), value: channel.channelIdString) + DetailRow(label: t("lightning__channel_id"), value: channel.channelIdString) - if let txid = channel.displayedFundingTxoTxid, let vout = channel.fundingTxoVout { - DetailRow(label: t("lightning__channel_point"), value: "\(txid):\(vout)") - } + if let txid = channel.displayedFundingTxoTxid, let vout = channel.fundingTxoVout { + DetailRow(label: t("lightning__channel_point"), value: "\(txid):\(vout)") + } - DetailRow( - label: t("lightning__channel_node_id"), - value: channel.counterpartyNodeIdString - ) + DetailRow( + label: t("lightning__channel_node_id"), + value: channel.counterpartyNodeIdString + ) - if let reason = channel.closureReason { - DetailRow(label: t("lightning__closure_reason"), value: reason) - } + if let reason = channel.closureReason { + DetailRow(label: t("lightning__closure_reason"), value: reason) } } } @@ -352,7 +347,7 @@ struct LightningConnectionDetailView: View { .frame(height: 50) } - Divider() + CustomDivider() } .frame(height: 51) } @@ -371,7 +366,7 @@ struct LightningConnectionDetailView: View { .frame(height: 50) } - Divider() + CustomDivider() } .frame(height: 51) } diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index 318783829..1cd390ad2 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -61,14 +61,13 @@ struct LightningConnectionsView: View { } .padding(.bottom, 16) - Divider() + CustomDivider() // Pending Connections section if !pendingConnections.isEmpty { - VStack(alignment: .leading, spacing: 16) { - CaptionMText(t("lightning__conn_pending")) - .padding(.top, 16) + SettingsSectionHeader(t("lightning__conn_pending")) + VStack(alignment: .leading, spacing: 16) { ForEach(Array(pendingConnections.enumerated()), id: \.element.channelId) { index, channel in let labelIndex = pendingConnections.count - index Button { @@ -93,7 +92,7 @@ struct LightningConnectionsView: View { ) .padding(.bottom, 16) - Divider() + CustomDivider() } } .buttonStyle(PlainButtonStyle()) @@ -105,10 +104,9 @@ struct LightningConnectionsView: View { // Open Connections section if !openChannels.isEmpty { - VStack(alignment: .leading, spacing: 16) { - CaptionMText(t("lightning__conn_open")) - .padding(.top, 16) + SettingsSectionHeader(t("lightning__conn_open")) + VStack(alignment: .leading, spacing: 16) { ForEach(Array(openChannels.enumerated()), id: \.element.channelId) { index, channel in let labelIndex = openChannels.count - index Button { @@ -133,7 +131,7 @@ struct LightningConnectionsView: View { ) .padding(.bottom, 16) - Divider() + CustomDivider() } .opacity((!channel.isChannelReady || !channel.isUsable) ? 0.64 : 1.0) } @@ -144,10 +142,9 @@ struct LightningConnectionsView: View { // Closed Connections section if showClosedConnections && !closedChannels.isEmpty { - VStack(alignment: .leading, spacing: 16) { - CaptionMText(t("lightning__conn_closed")) - .padding(.top, 16) + SettingsSectionHeader(t("lightning__conn_closed")) + VStack(alignment: .leading, spacing: 16) { ForEach(Array(closedChannels.enumerated()), id: \.element.channelId) { index, channel in let labelIndex = closedChannels.count - index Button { @@ -172,7 +169,7 @@ struct LightningConnectionsView: View { ) .padding(.bottom, 16) - Divider() + CustomDivider() } .opacity(0.64) } @@ -196,7 +193,6 @@ struct LightningConnectionsView: View { } Spacer() - // .frame(height: 32) HStack(spacing: 16) { CustomButton(title: t("lightning__conn_button_export_logs"), variant: .secondary) { diff --git a/Bitkit/Views/Settings/AppStatusView.swift b/Bitkit/Views/Settings/AppStatusView.swift index 7cc129760..daa2e0699 100644 --- a/Bitkit/Views/Settings/AppStatusView.swift +++ b/Bitkit/Views/Settings/AppStatusView.swift @@ -159,7 +159,7 @@ struct AppStatusView: View { description: description, status: status, onTap: { - navigation.navigate(.backupSettings) + navigation.navigate(.dataBackups) } ) .accessibilityIdentifier("Status-backup") diff --git a/Bitkit/Views/Settings/BlocktankRegtestView.swift b/Bitkit/Views/Settings/BlocktankRegtestView.swift index 53176c339..130ed77b3 100644 --- a/Bitkit/Views/Settings/BlocktankRegtestView.swift +++ b/Bitkit/Views/Settings/BlocktankRegtestView.swift @@ -1,57 +1,41 @@ import SwiftUI -struct RegtestButton: View { - let title: String - let action: () async throws -> Void - - @EnvironmentObject var app: AppViewModel - @State private var isLoading = false - - var body: some View { - Button(isLoading ? "Loading..." : title) { - isLoading = true - Task { - do { - try await action() - } catch { - Logger.error("Regtest action failed: \(error.localizedDescription)", context: "BlocktankRegtestView") - app.toast(type: .error, title: "Regtest action failed: \(error.localizedDescription)") - } - isLoading = false - } - } - .disabled(isLoading) - .opacity(isLoading ? 0.5 : 1) - } -} - -struct BlocktankRegtestView: View { +struct BlocktankRegtestScreen: View { @EnvironmentObject var app: AppViewModel @EnvironmentObject var wallet: WalletViewModel + @State private var result: String = "" - @State private var mineBlockCount: String = "1" + @State private var selectedMineBlockCount: Int = 1 @State private var depositAmount: String = "100000" @State private var depositAddress: String = "" @State private var paymentInvoice: String = "" @State private var paymentAmount: String = "" @State private var forceCloseAfterSeconds: String = "" @State private var showingResult = false + @State private var isDepositLoading = false + @State private var isMiningLoading = false + @State private var isPayInvoiceLoading = false + @State private var isClosingChannelLoading = false + + private let mineBlockOptions = [1, 3, 20, 144] var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: "Blocktank Regtest") .padding(.horizontal, 16) - List { - serverInfoSection - depositSection - miningSection - lightningPaymentSection - channelCloseSection + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 16) { + depositSection + miningSection + lightningPaymentSection + channelCloseSection + } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } .navigationBarHidden(true) - .bottomSafeAreaPadding() .onAppear { // Generate a fresh address when the view appears Task { @@ -66,190 +50,207 @@ struct BlocktankRegtestView: View { } } - var serverInfoSection: some View { - Section { - Text(Env.blocktankClientServer) - } footer: { - Text("These actions are executed on the staging Blocktank server node.") - } - } - var depositSection: some View { - Section { - HStack { - TextField("Address", text: $depositAddress) - .lineLimit(1) - .truncationMode(.middle) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader("Deposit") - Button { - if let string = UIPasteboard.general.string { - depositAddress = string - } - } label: { - Image(systemName: "doc.on.clipboard") - } + VStack(alignment: .leading, spacing: 8) { + HStack { + TextField("Address", text: $depositAddress) + .lineLimit(1) + .truncationMode(.middle) - Button { - Task { - do { - let newAddress = try await LightningService.shared.newAddress() - depositAddress = newAddress - } catch { - app.toast(type: .error, title: "Failed to generate address", description: error.localizedDescription) + Button { + if let string = UIPasteboard.general.string { + depositAddress = string } + } label: { + Image(systemName: "doc.on.clipboard") } - } label: { - Image(systemName: "arrow.clockwise") - } - } - - TextField("Amount (sats)", text: $depositAmount) - .keyboardType(.numberPad) - RegtestButton(title: "Make Deposit") { - Logger.debug("Initiating regtest deposit with amount: \(depositAmount)", context: "BlocktankRegtestView") - guard let amount = UInt64(depositAmount) else { - Logger.error("Invalid deposit amount: \(depositAmount)", context: "BlocktankRegtestView") - throw ValidationError("Invalid amount") + Button { + Task { + do { + let newAddress = try await LightningService.shared.newAddress() + depositAddress = newAddress + } catch { + app.toast(type: .error, title: "Failed to generate address", description: error.localizedDescription) + } + } + } label: { + Image(systemName: "arrow.clockwise") + } } - // Generate a new address for each deposit - let newAddress = try await LightningService.shared.newAddress() - Logger.debug("Generated new address for deposit: \(newAddress)", context: "BlocktankRegtestView") - - let txId = try await CoreService.shared.blocktank.regtestDepositFunds( - address: newAddress, - amountSat: amount - ) - Logger.debug("Deposit successful with txId: \(txId)", context: "BlocktankRegtestView") - app.toast(type: .success, title: "Success", description: "Deposit successful. TxID: \(txId)") + TextField("Amount (sats)", text: $depositAmount) + .keyboardType(.numberPad) - // Update the displayed address to the new one - depositAddress = newAddress + CustomButton(title: "Deposit", size: .small, isDisabled: depositAmount.isEmpty, isLoading: isDepositLoading) { + isDepositLoading = true + defer { isDepositLoading = false } + do { + Logger.debug("Initiating regtest deposit with amount: \(depositAmount)", context: "BlocktankRegtestScreen") + guard let amount = UInt64(depositAmount) else { + Logger.error("Invalid deposit amount: \(depositAmount)", context: "BlocktankRegtestScreen") + throw ValidationError("Invalid amount") + } - // Sync wallet after deposit without waiting - Task { - try? await wallet.sync() + let newAddress = try await LightningService.shared.newAddress() + Logger.debug("Generated new address for deposit: \(newAddress)", context: "BlocktankRegtestScreen") + + let txId = try await CoreService.shared.blocktank.regtestDepositFunds( + address: newAddress, + amountSat: amount + ) + Logger.debug("Deposit successful with txId: \(txId)", context: "BlocktankRegtestScreen") + app.toast(type: .success, title: "Success", description: "Deposit successful. TxID: \(txId)") + depositAddress = newAddress + Task { try? await wallet.sync() } + } catch { + Logger.error("Regtest action failed: \(error.localizedDescription)", context: "BlocktankRegtestScreen") + app.toast(type: .error, title: "Regtest action failed: \(error.localizedDescription)") + } } } - .disabled(depositAmount.isEmpty) - .tint(.orange) - } header: { - Text("Deposit") } } var miningSection: some View { - Section { - HStack { - TextField("Block count", text: $mineBlockCount) - .keyboardType(.numberPad) - - RegtestButton(title: "Mine Blocks") { - Logger.debug("Starting regtest mining with block count: \(mineBlockCount)", context: "BlocktankRegtestView") - guard let count = UInt32(mineBlockCount) else { - Logger.error("Invalid block count: \(mineBlockCount)", context: "BlocktankRegtestView") - throw ValidationError("Invalid block count") + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader("Mining") + + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + ForEach(mineBlockOptions, id: \.self) { count in + Button { + selectedMineBlockCount = count + } label: { + BodyMSBText("\(count)", textColor: selectedMineBlockCount == count ? .white : .textSecondary) + .frame(maxWidth: .infinity) + .padding(.vertical, 10) + .background(selectedMineBlockCount == count ? Color.brandAccent : Color.white10) + .cornerRadius(8) + } + .buttonStyle(PlainButtonStyle()) } - try await CoreService.shared.blocktank.regtestMineBlocks(count) - Logger.debug("Successfully mined \(count) blocks", context: "BlocktankRegtestView") - app.toast(type: .success, title: "Success", description: "Successfully mined \(count) blocks") + } - // Sync wallet after mining blocks without waiting - Task { - try? await wallet.sync() + CustomButton(title: "Mine Blocks", size: .small, isDisabled: selectedMineBlockCount == 0, isLoading: isMiningLoading) { + isMiningLoading = true + defer { isMiningLoading = false } + do { + let count = UInt32(selectedMineBlockCount) + Logger.debug("Starting regtest mining with block count: \(count)", context: "BlocktankRegtestScreen") + try await CoreService.shared.blocktank.regtestMineBlocks(count) + Logger.debug("Successfully mined \(count) blocks", context: "BlocktankRegtestScreen") + app.toast(type: .success, title: "Success", description: "Successfully mined \(count) blocks") + Task { try? await wallet.sync() } + } catch { + Logger.error("Regtest action failed: \(error.localizedDescription)", context: "BlocktankRegtestScreen") + app.toast(type: .error, title: "Regtest action failed: \(error.localizedDescription)") } } - .tint(.orange) } - } header: { - Text("Mining") } } var lightningPaymentSection: some View { - Section { - HStack { - TextField("Invoice", text: $paymentInvoice) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader("Lightning Payment") + + VStack(alignment: .leading, spacing: 8) { + HStack { + TextField("Invoice", text: $paymentInvoice) - Button { - if let string = UIPasteboard.general.string { - paymentInvoice = string + Button { + if let string = UIPasteboard.general.string { + paymentInvoice = string + } + } label: { + Image(systemName: "doc.on.clipboard") } - } label: { - Image(systemName: "doc.on.clipboard") } - } - TextField("Amount (optional, sats)", text: $paymentAmount) - .keyboardType(.numberPad) + TextField("Amount (optional, sats)", text: $paymentAmount) + .keyboardType(.numberPad) - RegtestButton(title: "Pay Invoice") { - Logger.debug("Initiating regtest payment with invoice: \(paymentInvoice), amount: \(paymentAmount)", context: "BlocktankRegtestView") - let amount = paymentAmount.isEmpty ? nil : UInt64(paymentAmount) ?? 0 - let paymentId = try await CoreService.shared.blocktank.regtestPayInvoice(paymentInvoice, amountSat: amount) - Logger.debug("Payment successful with ID: \(paymentId)", context: "BlocktankRegtestView") - app.toast(type: .success, title: "Success", description: "Payment successful. ID: \(paymentId)") + CustomButton(title: "Pay Invoice", size: .small, isDisabled: paymentInvoice.isEmpty, isLoading: isPayInvoiceLoading) { + isPayInvoiceLoading = true + defer { isPayInvoiceLoading = false } + do { + Logger.debug( + "Initiating regtest payment with invoice: \(paymentInvoice), amount: \(paymentAmount)", + context: "BlocktankRegtestScreen" + ) + let amount = paymentAmount.isEmpty ? nil : UInt64(paymentAmount) ?? 0 + let paymentId = try await CoreService.shared.blocktank.regtestPayInvoice(paymentInvoice, amountSat: amount) + Logger.debug("Payment successful with ID: \(paymentId)", context: "BlocktankRegtestScreen") + app.toast(type: .success, title: "Success", description: "Payment successful. ID: \(paymentId)") + } catch { + Logger.error("Regtest action failed: \(error.localizedDescription)", context: "BlocktankRegtestScreen") + app.toast(type: .error, title: "Regtest action failed: \(error.localizedDescription)") + } + } } - .disabled(paymentInvoice.isEmpty) - .tint(.orange) - } header: { - Text("Lightning Payment") } } var channelCloseSection: some View { - Section { - TextField("Force close after (seconds)", text: $forceCloseAfterSeconds) - .keyboardType(.numberPad) - - if let channels = wallet.channels, !channels.isEmpty { - ForEach(channels, id: \.channelId) { channel in - VStack(alignment: .leading) { - Text(channel.channelId) - .font(.caption) - - Text("Ready: \(channel.isChannelReady ? "✅" : "❌")") - Text("Usable: \(channel.isUsable ? "✅" : "❌")") - - RegtestButton(title: "Close This Channel") { - Logger.debug("Closing channel: \(channel.channelId)", context: "BlocktankRegtestView") - - let closeAfter = forceCloseAfterSeconds.isEmpty ? nil : UInt64(forceCloseAfterSeconds) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader("Channel close from Blocktank side") - let closingTxId = try await CoreService.shared.blocktank.regtestRemoteCloseChannel( - channel: channel, - forceCloseAfterSeconds: closeAfter - ) + VStack(alignment: .leading, spacing: 8) { + TextField("Force close after (seconds)", text: $forceCloseAfterSeconds) + .keyboardType(.numberPad) - Logger.debug("Channel closed successfully with txId: \(closingTxId)", context: "BlocktankRegtestView") - app.toast(type: .success, title: "Success", description: "Channel closed. Closing TxID: \(closingTxId)") + if let channels = wallet.channels, !channels.isEmpty { + ForEach(channels, id: \.channelId) { channel in + VStack(alignment: .leading) { + CaptionMText(channel.channelId, textColor: .textSecondary) + + HStack(spacing: 6) { + BodyMText("Ready:", textColor: .textPrimary) + Image(systemName: channel.isChannelReady ? "checkmark.circle.fill" : "xmark.circle.fill") + .font(.system(size: 17)) + .foregroundColor(channel.isChannelReady ? .greenAccent : .redAccent) + } + HStack(spacing: 6) { + BodyMText("Usable:", textColor: .textPrimary) + Image(systemName: channel.isUsable ? "checkmark.circle.fill" : "xmark.circle.fill") + .font(.system(size: 17)) + .foregroundColor(channel.isUsable ? .greenAccent : .redAccent) + } + + CustomButton(title: "Close This Channel", size: .small, isDisabled: false, isLoading: isClosingChannelLoading) { + isClosingChannelLoading = true + defer { isClosingChannelLoading = false } + do { + Logger.debug("Closing channel: \(channel.channelId)", context: "BlocktankRegtestScreen") + let closeAfter = forceCloseAfterSeconds.isEmpty ? nil : UInt64(forceCloseAfterSeconds) + let closingTxId = try await CoreService.shared.blocktank.regtestRemoteCloseChannel( + channel: channel, + forceCloseAfterSeconds: closeAfter + ) + Logger.debug("Channel closed successfully with txId: \(closingTxId)", context: "BlocktankRegtestScreen") + app.toast(type: .success, title: "Success", description: "Channel closed. Closing TxID: \(closingTxId)") + } catch { + Logger.error("Regtest action failed: \(error.localizedDescription)", context: "BlocktankRegtestScreen") + app.toast(type: .error, title: "Regtest action failed: \(error.localizedDescription)") + } + } + .padding(.top, 8) } - .tint(.red) - .padding(.top, 8) + .padding(.vertical, 4) } - .padding(.vertical, 4) + } else { + BodyMText("No channels available") + .padding(.vertical, 8) } - } else { - Text("No channels available") - .foregroundColor(.secondary) - .padding(.vertical, 8) } - } header: { - Text("Channel close from Blocktank side") } } } -#Preview { - NavigationStack { - BlocktankRegtestView() - .environmentObject(AppViewModel()) - .environmentObject(WalletViewModel()) - } - .preferredColorScheme(.dark) -} - private struct ValidationError: LocalizedError { let message: String diff --git a/Bitkit/Views/Settings/DevSettingsView.swift b/Bitkit/Views/Settings/DevSettingsView.swift index 73e4d2756..0e3544b1d 100644 --- a/Bitkit/Views/Settings/DevSettingsView.swift +++ b/Bitkit/Views/Settings/DevSettingsView.swift @@ -18,12 +18,12 @@ struct DevSettingsView: View { VStack(alignment: .leading, spacing: 0) { if Env.network == .regtest { NavigationLink(value: Route.blocktankRegtest) { - SettingsListLabel(title: "Blocktank Regtest") + SettingsRow(title: "Blocktank Regtest") } } if Env.network == .regtest { - SettingsListLabel( + SettingsRow( title: "Override Fees", rightIcon: nil, toggle: $feeEstimatesManager.devOverrideFeeEstimates @@ -31,19 +31,19 @@ struct DevSettingsView: View { } NavigationLink(value: Route.ldkDebug) { - SettingsListLabel(title: "LDK") + SettingsRow(title: "LDK") } NavigationLink(value: Route.vssDebug) { - SettingsListLabel(title: "VSS") + SettingsRow(title: "VSS") } NavigationLink(value: Route.probingTool) { - SettingsListLabel(title: "Probing Tool") + SettingsRow(title: "Probing Tool") } NavigationLink(value: Route.orders) { - SettingsListLabel(title: "Orders") + SettingsRow(title: "Orders") } Button { @@ -57,7 +57,7 @@ struct DevSettingsView: View { } } } label: { - SettingsListLabel(title: "Generate Test Activities", rightIcon: nil) + SettingsRow(title: "Generate Test Activities", rightIcon: nil) } Button { @@ -71,11 +71,11 @@ struct DevSettingsView: View { } } } label: { - SettingsListLabel(title: "Reset All Activities", rightIcon: nil) + SettingsRow(title: "Reset All Activities", rightIcon: nil) } NavigationLink(value: Route.logs) { - SettingsListLabel(title: "Show Logs") + SettingsRow(title: "Show Logs") } Button { @@ -100,7 +100,7 @@ struct DevSettingsView: View { } } } label: { - SettingsListLabel(title: "Export Logs", rightIcon: nil) + SettingsRow(title: "Export Logs", rightIcon: nil) } Button { @@ -117,13 +117,13 @@ struct DevSettingsView: View { } } } label: { - SettingsListLabel(title: "Test Push Notification", rightIcon: nil) + SettingsRow(title: "Test Push Notification", rightIcon: nil) } Button { fatalError("Simulate Crash") } label: { - SettingsListLabel(title: "Simulate Crash", rightIcon: nil) + SettingsRow(title: "Simulate Crash", rightIcon: nil) } Button { @@ -139,7 +139,7 @@ struct DevSettingsView: View { } } } label: { - SettingsListLabel(title: "Wipe Wallet", rightIcon: nil) + SettingsRow(title: "Wipe Wallet", rightIcon: nil) } } .padding(.horizontal, 16) diff --git a/Bitkit/Views/Settings/General/DefaultUnitSettingsView.swift b/Bitkit/Views/Settings/General/DefaultUnitSettingsView.swift index e5d2b53e4..6a8efe7f0 100644 --- a/Bitkit/Views/Settings/General/DefaultUnitSettingsView.swift +++ b/Bitkit/Views/Settings/General/DefaultUnitSettingsView.swift @@ -6,64 +6,61 @@ struct DefaultUnitSettingsView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: t("settings__general__unit_title")) + .padding(.horizontal, 16) ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 8) { - CaptionMText(t("settings__general__unit_display")) - .padding(.vertical, 16) - .frame(maxWidth: .infinity, alignment: .leading) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader(t("settings__general__unit_display")) Button(action: { currency.primaryDisplay = .bitcoin }) { - SettingsListLabel( + SettingsRow( title: t("settings__general__unit_bitcoin"), iconName: "b-unit", + iconColor: .textPrimary, rightIcon: currency.primaryDisplay == .bitcoin ? .checkmark : nil ) } - .buttonStyle(PlainButtonStyle()) if let rate = currency.convert(sats: 1)?.currency { Button(action: { currency.primaryDisplay = .fiat }) { - SettingsListLabel( + SettingsRow( title: rate, iconName: "globe", + iconColor: .textPrimary, rightIcon: currency.primaryDisplay == .fiat ? .checkmark : nil ) } - .buttonStyle(PlainButtonStyle()) } BodyMText(t("settings__general__unit_note", variables: ["currency": currency.selectedCurrency])) .padding(.vertical, 16) - } - VStack(alignment: .leading, spacing: 8) { - CaptionMText(t("settings__general__denomination_label")) - .padding(.vertical, 16) - .frame(maxWidth: .infinity, alignment: .leading) + CustomDivider() + + SettingsSectionHeader(t("settings__general__denomination_label")) + .padding(.top, 16) ForEach(BitcoinDisplayUnit.allCases, id: \.self) { unit in Button(action: { currency.displayUnit = unit }) { - SettingsListLabel( + SettingsRow( title: t("settings__general__denomination_\(unit.rawValue)"), rightIcon: currency.displayUnit == unit ? .checkmark : nil ) } - .buttonStyle(PlainButtonStyle()) .accessibilityIdentifier(unit.testIdentifier) } } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() } } diff --git a/Bitkit/Views/Settings/General/LanguageSettingsScreen.swift b/Bitkit/Views/Settings/General/LanguageSettingsScreen.swift index d6c0a4957..97d58429d 100644 --- a/Bitkit/Views/Settings/General/LanguageSettingsScreen.swift +++ b/Bitkit/Views/Settings/General/LanguageSettingsScreen.swift @@ -8,7 +8,7 @@ struct LanguageSettingsScreen: View { Button(action: { selectLanguage(language) }) { - SettingsListLabel( + SettingsRow( title: language.name, rightIcon: languageManager.currentLanguage.code == language.code ? .checkmark : nil ) @@ -26,22 +26,21 @@ struct LanguageSettingsScreen: View { var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: t("settings__general__language_title")) + .padding(.horizontal, 16) ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__general__language_other")) - .padding(.vertical, 16) - .frame(maxWidth: .infinity, alignment: .leading) + SettingsSectionHeader(t("settings__general__language_other")) ForEach(SupportedLanguage.allLanguages, id: \.id) { language in languageRow(language) } } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() .alert("Language Changed", isPresented: $showAlert) { Button("OK", role: .cancel) {} } message: { diff --git a/Bitkit/Views/Settings/General/LocalCurrencySettingsView.swift b/Bitkit/Views/Settings/General/LocalCurrencySettingsView.swift index d92f79e3f..a6144fa5a 100644 --- a/Bitkit/Views/Settings/General/LocalCurrencySettingsView.swift +++ b/Bitkit/Views/Settings/General/LocalCurrencySettingsView.swift @@ -2,6 +2,8 @@ import SwiftUI struct LocalCurrencySettingsView: View { @EnvironmentObject var currency: CurrencyViewModel + @EnvironmentObject var navigation: NavigationViewModel + @State private var searchText = "" private let mostUsedCurrencies = ["USD", "GBP", "CAD", "CNY", "EUR"] @@ -25,7 +27,7 @@ struct LocalCurrencySettingsView: View { } private func currencyRow(_ rate: FxRate) -> some View { - SettingsListLabel( + SettingsRow( title: "\(rate.quote) (\(rate.currencySymbol))", rightIcon: currency.selectedCurrency == rate.quote ? .checkmark : nil, testIdentifier: "Currency-\(rate.quote)" @@ -35,6 +37,7 @@ struct LocalCurrencySettingsView: View { currency.selectedCurrency = rate.quote Task { await currency.refresh() + navigation.navigateBack() } } .accessibilityAddTraits(.isButton) @@ -64,10 +67,7 @@ struct LocalCurrencySettingsView: View { ScrollView(showsIndicators: false) { if !availableMostUsed.isEmpty { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__general__currency_most_used")) - .padding(.top, 16) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, alignment: .leading) + SettingsSectionHeader(t("settings__general__currency_most_used")) ForEach(availableMostUsed, id: \.quote) { rate in currencyRow(rate) @@ -76,15 +76,13 @@ struct LocalCurrencySettingsView: View { } VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__general__currency_other")) - .padding(.top, 24) - .padding(.bottom, 8) - .frame(maxWidth: .infinity, alignment: .leading) + SettingsSectionHeader(t("settings__general__currency_other")) ForEach(otherCurrencies, id: \.quote) { rate in currencyRow(rate) } } + .padding(.top, 16) CaptionText(t("settings__general__currency_footer")) .padding(.top, 16) diff --git a/Bitkit/Views/Settings/General/TagSettingsView.swift b/Bitkit/Views/Settings/General/TagSettingsView.swift index e9601854e..61ec7bc61 100644 --- a/Bitkit/Views/Settings/General/TagSettingsView.swift +++ b/Bitkit/Views/Settings/General/TagSettingsView.swift @@ -1,18 +1,16 @@ import SwiftUI struct TagSettingsView: View { - @EnvironmentObject var app: AppViewModel @EnvironmentObject var tagManager: TagManager var body: some View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: t("settings__general__tags")) + .padding(.horizontal, 16) ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__general__tags_previously")) - .padding(.top, 24) - .padding(.bottom, 16) + SettingsSectionHeader(t("settings__general__tags_previously")) TagsListView( tags: tagManager.lastUsedTags, @@ -22,10 +20,10 @@ struct TagSettingsView: View { } ) } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } } .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() } } diff --git a/Bitkit/Views/Settings/General/WidgetsSettingsScreen.swift b/Bitkit/Views/Settings/General/WidgetsSettingsScreen.swift new file mode 100644 index 000000000..c60e42ca1 --- /dev/null +++ b/Bitkit/Views/Settings/General/WidgetsSettingsScreen.swift @@ -0,0 +1,82 @@ +import SwiftUI + +struct WidgetsSettingsScreen: View { + @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var settings: SettingsViewModel + @EnvironmentObject var suggestionsManager: SuggestionsManager + @EnvironmentObject var widgets: WidgetsViewModel + + @State private var showWidgetsResetAlert = false + @State private var showSuggestionsResetAlert = false + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: t("settings__widgets__nav_title")) + .padding(.horizontal, 16) + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader(t("settings__widgets__section_display")) + + SettingsRow( + title: t("settings__widgets__showWidgets"), + toggle: $settings.showWidgets + ) + + SettingsRow( + title: t("settings__widgets__showWidgetTitles"), + toggle: $settings.showWidgetTitles + ) + + SettingsSectionHeader(t("settings__widgets__section_reset")) + .padding(.top, 16) + + Button(action: { + showWidgetsResetAlert = true + }) { + SettingsRow( + title: t("settings__widgets__reset_widgets"), + iconName: "arrow-counter-clockwise" + ) + } + + Button(action: { + showSuggestionsResetAlert = true + }) { + SettingsRow( + title: t("settings__widgets__reset_suggestions"), + iconName: "arrow-counter-clockwise" + ) + } + } + .padding(.horizontal, 16) + .bottomSafeAreaPadding() + } + } + .navigationBarHidden(true) + .alert(t("settings__widgets__reset_widgets_dialog_title"), isPresented: $showWidgetsResetAlert) { + Button(t("settings__adv__reset_confirm"), role: .destructive) { + widgets.clearWidgets() + navigation.reset() + } + .accessibilityIdentifier("DialogConfirm") + + Button(t("common__dialog_cancel"), role: .cancel) {} + .accessibilityIdentifier("DialogCancel") + } message: { + Text(t("settings__widgets__reset_widgets_dialog_description")) + } + .alert(t("settings__adv__reset_title"), isPresented: $showSuggestionsResetAlert) { + Button(t("settings__adv__reset_confirm"), role: .destructive) { + suggestionsManager.resetDismissed() + navigation.reset() + } + .accessibilityIdentifier("DialogConfirm") + + Button(t("common__dialog_cancel"), role: .cancel) {} + .accessibilityIdentifier("DialogCancel") + } message: { + Text(t("settings__adv__reset_desc")) + } + } +} diff --git a/Bitkit/Views/Settings/General/WidgetsSettingsView.swift b/Bitkit/Views/Settings/General/WidgetsSettingsView.swift deleted file mode 100644 index 30e5a99b2..000000000 --- a/Bitkit/Views/Settings/General/WidgetsSettingsView.swift +++ /dev/null @@ -1,37 +0,0 @@ -import SwiftUI - -struct WidgetsSettingsView: View { - @EnvironmentObject var settings: SettingsViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__widgets__nav_title")) - .padding(.horizontal, 16) - - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 0) { - SettingsListLabel( - title: t("settings__widgets__showWidgets"), - toggle: $settings.showWidgets - ) - - SettingsListLabel( - title: t("settings__widgets__showWidgetTitles"), - toggle: $settings.showWidgetTitles - ) - } - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - } - } - .navigationBarHidden(true) - } -} - -#Preview { - NavigationView { - WidgetsSettingsView() - .environmentObject(SettingsViewModel.shared) - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Settings/GeneralSettingsView.swift b/Bitkit/Views/Settings/GeneralSettingsView.swift index 454d14e84..ccbc84f53 100644 --- a/Bitkit/Views/Settings/GeneralSettingsView.swift +++ b/Bitkit/Views/Settings/GeneralSettingsView.swift @@ -8,82 +8,93 @@ struct GeneralSettingsView: View { @StateObject private var languageManager = LanguageManager.shared var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__general_title")) + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + // Interface section + SettingsSectionHeader(t("settings__general__section_interface")) - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 0) { - NavigationLink(value: Route.languageSettings) { - SettingsListLabel( - title: t("settings__general__language"), - rightText: languageManager.currentLanguageDisplayName - ) - } + NavigationLink(value: Route.languageSettings) { + SettingsRow( + title: t("settings__general__language"), + iconName: "translate", + rightText: languageManager.currentLanguageDisplayName + ) + } - NavigationLink(value: Route.currencySettings) { - SettingsListLabel( - title: t("settings__general__currency_local"), - rightText: currency.selectedCurrency - ) - } - .accessibilityIdentifier("CurrenciesSettings") + NavigationLink(value: Route.currencySettings) { + SettingsRow( + title: t("settings__general__currency_local"), + iconName: "coins", + rightText: "\(currency.selectedCurrency) (\(currency.symbol))" + ) + } + .accessibilityIdentifier("CurrenciesSettings") - NavigationLink(value: Route.unitSettings) { - SettingsListLabel( - title: t("settings__general__unit"), - rightText: currency.primaryDisplay == .bitcoin ? currency.primaryDisplay.rawValue : currency.selectedCurrency - ) - } - .accessibilityIdentifier("UnitSettings") + NavigationLink(value: Route.unitSettings) { + SettingsRow( + title: t("settings__general__unit"), + iconName: currency.primaryDisplay == .bitcoin ? "b-unit" : "globe", + rightText: currency.primaryDisplay == .bitcoin ? currency.primaryDisplay.rawValue : currency.selectedCurrency + ) + } + .accessibilityIdentifier("UnitSettings") + + NavigationLink(value: Route.widgetsSettings) { + SettingsRow( + title: t("settings__widgets__nav_title"), + iconName: "cube", + rightText: settings.showWidgets ? t("common__on") : t("common__off") + ) + } + .accessibilityIdentifier("WidgetsSettings") - NavigationLink(value: Route.transactionSpeedSettings) { - SettingsListLabel( - title: t("settings__general__speed"), - rightText: settings.defaultTransactionSpeed.title + if !tagManager.lastUsedTags.isEmpty { + NavigationLink(value: Route.tagSettings) { + SettingsRow( + title: t("settings__general__tags"), + iconName: "tag", + rightText: String(tagManager.lastUsedTags.count) ) } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("TransactionSpeedSettings") + .accessibilityIdentifier("TagsSettings") + } - if !tagManager.lastUsedTags.isEmpty { - NavigationLink(value: Route.tagSettings) { - SettingsListLabel( - title: t("settings__general__tags"), - rightText: String(tagManager.lastUsedTags.count) - ) - } - .accessibilityIdentifier("TagsSettings") - } + // Payments section + SettingsSectionHeader(t("settings__general__section_payments")) + .padding(.top, 16) - NavigationLink(value: Route.widgetsSettings) { - SettingsListLabel( - title: t("settings__widgets__nav_title"), - rightText: settings.showWidgets ? t("common__on") : t("common__off") - ) - } - .accessibilityIdentifier("WidgetsSettings") + NavigationLink(value: Route.transactionSpeedSettings) { + SettingsRow( + title: t("settings__general__speed"), + iconName: settings.defaultTransactionSpeed.iconName, + rightText: settings.defaultTransactionSpeed.title + ) + } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("TransactionSpeedSettings") - NavigationLink(value: app.hasSeenQuickpayIntro ? Route.quickpay : Route.quickpayIntro) { - SettingsListLabel( - title: t("settings__quickpay__nav_title"), - rightText: settings.enableQuickpay ? t("common__on") : t("common__off") - ) - } - .accessibilityIdentifier("QuickpaySettings") + NavigationLink(value: app.hasSeenQuickpayIntro ? Route.quickpay : Route.quickpayIntro) { + SettingsRow( + title: t("settings__quickpay__nav_title"), + iconName: "caret-double-right", + rightText: settings.enableQuickpay ? t("common__on") : t("common__off") + ) + } + .accessibilityIdentifier("QuickpaySettings") - NavigationLink(value: app.hasSeenNotificationsIntro ? Route.notifications : Route.notificationsIntro) { - SettingsListLabel( - title: t("settings__notifications__nav_title"), - rightText: settings.enableNotifications ? t("common__on") : t("common__off") - ) - } - .accessibilityIdentifier("NotificationsSettings") + NavigationLink(value: app.hasSeenNotificationsIntro ? Route.notifications : Route.notificationsIntro) { + SettingsRow( + title: t("settings__notifications__nav_title"), + iconName: "bell", + rightText: settings.enableNotifications ? t("common__on") : t("common__off") + ) } + .accessibilityIdentifier("NotificationsSettings") } + .padding(.top, 16) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() } } diff --git a/Bitkit/Views/Settings/LdkDebugScreen.swift b/Bitkit/Views/Settings/LdkDebugScreen.swift index 69d1556cb..49439a570 100644 --- a/Bitkit/Views/Settings/LdkDebugScreen.swift +++ b/Bitkit/Views/Settings/LdkDebugScreen.swift @@ -17,7 +17,7 @@ struct LdkDebugScreen: View { VStack(alignment: .leading, spacing: 32) { // Add Peer VStack(alignment: .leading, spacing: 8) { - CaptionMText("Add Peer") + SettingsSectionHeader("Add Peer") TextField("039b8d4d...a8f3eae3@127.0.0.1:9735", text: $nodeUri) @@ -37,7 +37,7 @@ struct LdkDebugScreen: View { // Network Graph Storage VStack(alignment: .leading, spacing: 8) { - CaptionMText("Network Graph Storage") + SettingsSectionHeader("Network Graph Storage") HStack(spacing: 8) { CustomButton(title: "Log Graph Info", size: .small) { @@ -56,7 +56,7 @@ struct LdkDebugScreen: View { // Node VStack(alignment: .leading, spacing: 8) { - CaptionMText("Node") + SettingsSectionHeader("Node") HStack(spacing: 8) { CustomButton(title: "Restart", size: .small, isLoading: isRestartingNode) { @@ -69,7 +69,7 @@ struct LdkDebugScreen: View { // Peer Simulation VStack(alignment: .leading, spacing: 8) { - CaptionMText("Peer Simulation") + SettingsSectionHeader("Peer Simulation") Picker("Peer Simulation", selection: Binding( get: { WalletViewModel.peerSimulation }, diff --git a/Bitkit/Views/Settings/MainSettings.swift b/Bitkit/Views/Settings/MainSettings.swift deleted file mode 100644 index d558b1119..000000000 --- a/Bitkit/Views/Settings/MainSettings.swift +++ /dev/null @@ -1,107 +0,0 @@ -import SwiftUI - -struct MainSettings: View { - @EnvironmentObject private var app: AppViewModel - - @AppStorage("showDevSettings") private var showDevSettings = Env.isDebug - @State private var cogTapCount = 0 - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__settings")) - - GeometryReader { geometry in - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 0) { - NavigationLink(value: Route.generalSettings) { - SettingsListLabel( - title: t("settings__general_title"), - iconName: "gear-six" - ) - } - .accessibilityIdentifier("GeneralSettings") - - NavigationLink(value: Route.securitySettings) { - SettingsListLabel( - title: t("settings__security_title"), - iconName: "shield" - ) - } - .accessibilityIdentifier("SecuritySettings") - - NavigationLink(value: Route.backupSettings) { - SettingsListLabel( - title: t("settings__backup_title"), - iconName: "rewind" - ) - } - .accessibilityIdentifier("BackupSettings") - - NavigationLink(value: Route.advancedSettings) { - SettingsListLabel( - title: t("settings__advanced_title"), - iconName: "sliders" - ) - } - .accessibilityIdentifier("AdvancedSettings") - - NavigationLink(value: Route.support) { - SettingsListLabel( - title: t("settings__support_title"), - iconName: "chat" - ) - } - .accessibilityIdentifier("Support") - - NavigationLink(value: Route.about) { - SettingsListLabel( - title: t("settings__about_title"), - iconName: "info" - ) - } - .accessibilityIdentifier("About") - - if showDevSettings { - NavigationLink(value: Route.devSettings) { - SettingsListLabel( - title: t("settings__dev_title"), - iconName: "game-controller" - ) - } - .accessibilityIdentifier("DevSettings") - } - - Spacer() - - Image("cog") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .frame(maxWidth: .infinity, alignment: .center) - .onTapGesture { - cogTapCount += 1 - - // Toggle dev settings every 5 taps - if cogTapCount >= 5 { - showDevSettings.toggle() - cogTapCount = 0 - - app.toast( - type: .success, - title: t(showDevSettings ? "settings__dev_enabled_title" : "settings__dev_disabled_title"), - description: t(showDevSettings ? "settings__dev_enabled_message" : "settings__dev_disabled_message") - ) - } - } - .accessibilityIdentifier("DevOptions") - - Spacer() - } - .frame(minHeight: geometry.size.height) - } - } - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - } -} diff --git a/Bitkit/Views/Settings/MainSettingsScreen.swift b/Bitkit/Views/Settings/MainSettingsScreen.swift new file mode 100644 index 000000000..93088c337 --- /dev/null +++ b/Bitkit/Views/Settings/MainSettingsScreen.swift @@ -0,0 +1,43 @@ +import SwiftUI + +enum SettingsTab: String, CaseIterable, CustomStringConvertible { + case general + case security + case advanced + + var description: String { + switch self { + case .general: return t("settings__general_title") + case .security: return t("settings__security_title") + case .advanced: return t("settings__advanced_title") + } + } +} + +struct MainSettingsScreen: View { + @State private var selectedTab: SettingsTab = .general + + private var settingsTabItems: [TabItem] { + SettingsTab.allCases.map { TabItem($0) } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: t("settings__settings")) + .padding(.horizontal, 16) + + SegmentedControl(selectedTab: $selectedTab, tabItems: settingsTabItems) + .padding(.horizontal, 16) + + Group { + switch selectedTab { + case .general: GeneralSettingsView() + case .security: SecuritySettingsView() + case .advanced: AdvancedSettingsView() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationBarHidden(true) + } +} diff --git a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift index 0ef7fbe1e..eeae11de8 100644 --- a/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift +++ b/Bitkit/Views/Settings/Notifications/NotificationsSettings.swift @@ -29,7 +29,7 @@ struct NotificationsSettings: View { VStack(alignment: .leading, spacing: 0) { NavigationBar(title: t("settings__notifications__nav_title")) - SettingsListLabel( + SettingsRow( title: t("settings__notifications__settings__toggle"), toggle: $settings.enableNotifications, disabled: isDenied @@ -51,7 +51,7 @@ struct NotificationsSettings: View { CaptionMText(t("settings__notifications__settings__privacy__label")) .padding(.top, 32) - SettingsListLabel( + SettingsRow( title: t("settings__notifications__settings__privacy__text"), toggle: $settings.enableNotificationsAmount ) diff --git a/Bitkit/Views/Settings/Quickpay/QuickpaySettings.swift b/Bitkit/Views/Settings/Quickpay/QuickpaySettings.swift index d19182eb1..ca920ec57 100644 --- a/Bitkit/Views/Settings/Quickpay/QuickpaySettings.swift +++ b/Bitkit/Views/Settings/Quickpay/QuickpaySettings.swift @@ -13,7 +13,7 @@ struct QuickpaySettings: View { GeometryReader { geometry in ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - SettingsListLabel( + SettingsRow( title: t("settings__quickpay__settings__toggle"), toggle: $settings.enableQuickpay, testIdentifier: "QuickpayToggle" @@ -24,8 +24,8 @@ struct QuickpaySettings: View { ) .padding(.top, 16) - VStack(alignment: .leading, spacing: 16) { - CaptionMText(t("settings__quickpay__settings__label")) + VStack(alignment: .leading, spacing: 0) { + SettingsSectionHeader(t("settings__quickpay__settings__label")) CustomSlider(value: $settings.quickpayAmount, steps: sliderSteps) } .padding(.top, 32) diff --git a/Bitkit/Views/Settings/Security/ChangePinScreen.swift b/Bitkit/Views/Settings/Security/ChangePinScreen.swift new file mode 100644 index 000000000..7ab33adfb --- /dev/null +++ b/Bitkit/Views/Settings/Security/ChangePinScreen.swift @@ -0,0 +1,76 @@ +import SwiftUI + +struct ChangePinScreen: View { + @EnvironmentObject private var settings: SettingsViewModel + @EnvironmentObject private var sheets: SheetViewModel + + var navTitle: String { + settings.pinEnabled ? t("security__pin_disable_title") : t("settings__security__pin") + } + + var description: String { + settings.pinEnabled ? t("security__pin_disable_text") : t("security__pin_security_text") + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: navTitle) + .padding(.bottom, 16) + + BodyMText(description) + + Spacer() + + Image("shield-figure") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 256, height: 256) + .frame(maxWidth: .infinity) + .padding(.top, 32) + + Spacer() + + HStack(alignment: .center, spacing: 16) { + if settings.pinEnabled { + CustomButton(title: t("security__cp_title"), variant: .secondary) { + sheets.showSheet(.security, data: SecurityConfig(showLaterButton: false)) + } + .accessibilityIdentifier("PINCode") + + CustomButton(title: t("security__pin_disable_button")) { + sheets.showSheet(.security, data: SecurityConfig(showLaterButton: false)) + } + .accessibilityIdentifier("DisablePin") + } else { + CustomButton(title: t("security__pin_enable_button")) { + sheets.showSheet(.security, data: SecurityConfig(showLaterButton: false)) + } + .accessibilityIdentifier("EnablePin") + } + } + + // CustomButton( + // title: t("security__pin_disable_button"), + // destination: PinCheckView( + // title: t("security__pin_enter"), + // explanation: "", + // onCancel: {}, + // onPinVerified: { pin in + // do { + // try settings.removePin(pin: pin) + // dismiss() + // } catch { + // Logger.error("Failed to remove PIN: \(error)", context: "DisablePinView") + // // Still dismiss even if there's an error, as the PIN was verified + // dismiss() + // } + // } + // ) + // ) + // .accessibilityIdentifier("DisablePin") + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() + } +} diff --git a/Bitkit/Views/Settings/Backup/BackupSettings.swift b/Bitkit/Views/Settings/Security/DataBackupsScreen.swift similarity index 76% rename from Bitkit/Views/Settings/Backup/BackupSettings.swift rename to Bitkit/Views/Settings/Security/DataBackupsScreen.swift index de0909a96..31b84c1a2 100644 --- a/Bitkit/Views/Settings/Backup/BackupSettings.swift +++ b/Bitkit/Views/Settings/Security/DataBackupsScreen.swift @@ -1,19 +1,5 @@ import SwiftUI -private struct SettingsLabel: View { - let text: String - - init(_ text: String) { - self.text = text - } - - var body: some View { - CaptionMText(text) - .frame(height: 40) - .frame(maxWidth: .infinity, alignment: .leading) - } -} - private struct StatusItemView: View { let imageName: String let title: String @@ -51,8 +37,7 @@ private struct StatusItemView: View { } } -struct BackupSettings: View { - @EnvironmentObject var sheets: SheetViewModel +struct DataBackupsScreen: View { @StateObject private var viewModel = BackupViewModel() private var allSynced: Bool { @@ -64,26 +49,12 @@ struct BackupSettings: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__backup_title")) + NavigationBar(title: t("settings__data_backups_nav_title")) GeometryReader { geometry in ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - Button(action: { - sheets.showSheet(.backup, data: BackupConfig(view: .mnemonic)) - }) { - SettingsListLabel(title: t("settings__backup__wallet")) - } - .accessibilityIdentifier("BackupWallet") - - NavigationLink(value: Route.resetAndRestore) { - SettingsListLabel(title: t("settings__backup__reset")) - } - .accessibilityIdentifier("ResetAndRestore") - HStack(alignment: .center, spacing: 8) { - SettingsLabel(t("settings__backup__latest")) - if Env.isE2E, allSynced { Image("check") .resizable() @@ -112,6 +83,8 @@ struct BackupSettings: View { viewModel.triggerBackup(for: category) } ) + + Divider() } Spacer() diff --git a/Bitkit/Views/Settings/Security/DisablePinView.swift b/Bitkit/Views/Settings/Security/DisablePinView.swift deleted file mode 100644 index f285e0425..000000000 --- a/Bitkit/Views/Settings/Security/DisablePinView.swift +++ /dev/null @@ -1,57 +0,0 @@ -import SwiftUI - -struct DisablePinView: View { - @Environment(\.dismiss) private var dismiss - @EnvironmentObject var settings: SettingsViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("security__pin_disable_title")) - .padding(.bottom, 16) - - BodyMText(t("security__pin_disable_text")) - - Spacer() - - Image("shield-figure") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .frame(maxWidth: .infinity) - .padding(.top, 32) - - Spacer() - - CustomButton( - title: t("security__pin_disable_button"), - destination: PinCheckView( - title: t("security__pin_enter"), - explanation: "", - onCancel: {}, - onPinVerified: { pin in - do { - try settings.removePin(pin: pin) - dismiss() - } catch { - Logger.error("Failed to remove PIN: \(error)", context: "DisablePinView") - // Still dismiss even if there's an error, as the PIN was verified - dismiss() - } - } - ) - ) - .accessibilityIdentifier("DisablePin") - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - } -} - -#Preview { - NavigationStack { - DisablePinView() - } - .preferredColorScheme(.dark) - .environmentObject(SettingsViewModel.shared) -} diff --git a/Bitkit/Views/Settings/Backup/ResetAndRestore.swift b/Bitkit/Views/Settings/Security/ResetScreen.swift similarity index 96% rename from Bitkit/Views/Settings/Backup/ResetAndRestore.swift rename to Bitkit/Views/Settings/Security/ResetScreen.swift index 97bcd23ab..89e55da30 100644 --- a/Bitkit/Views/Settings/Backup/ResetAndRestore.swift +++ b/Bitkit/Views/Settings/Security/ResetScreen.swift @@ -1,6 +1,6 @@ import SwiftUI -struct ResetAndRestore: View { +struct ResetScreen: View { @EnvironmentObject var app: AppViewModel @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel @@ -10,7 +10,7 @@ struct ResetAndRestore: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__backup__title")) + NavigationBar(title: t("settings__reset_nav_title")) VStack(spacing: 0) { BodyMText(t("security__reset_text")) diff --git a/Bitkit/Views/Settings/SecurityPrivacySettingsView.swift b/Bitkit/Views/Settings/SecuritySettingsView.swift similarity index 70% rename from Bitkit/Views/Settings/SecurityPrivacySettingsView.swift rename to Bitkit/Views/Settings/SecuritySettingsView.swift index 8cff031b8..70a2c4653 100644 --- a/Bitkit/Views/Settings/SecurityPrivacySettingsView.swift +++ b/Bitkit/Views/Settings/SecuritySettingsView.swift @@ -1,7 +1,7 @@ import LocalAuthentication import SwiftUI -struct SecurityPrivacySettingsView: View { +struct SecuritySettingsView: View { @EnvironmentObject var app: AppViewModel @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var settings: SettingsViewModel @@ -12,12 +12,9 @@ struct SecurityPrivacySettingsView: View { private var biometryTypeName: String { switch Env.biometryType { - case .touchID: - return t("security__bio_touch_id") - case .faceID: - return t("security__bio_face_id") - default: - return t("security__bio_face_id") // Default to Face ID + case .touchID: return t("security__bio_touch_id") + case .faceID: return t("security__bio_face_id") + default: return t("security__bio_face_id") // Default to Face ID } } @@ -29,71 +26,57 @@ struct SecurityPrivacySettingsView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("settings__security__title")) - .padding(.bottom, 16) - .padding(.horizontal, 16) - ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - // Privacy Settings Section - SettingsListLabel( - title: t("settings__security__swipe_balance_to_hide"), - toggle: $settings.swipeBalanceToHide, - testIdentifier: "SwipeBalanceToHide" - ) - - SettingsListLabel( - title: t("settings__security__hide_balance_on_open"), - toggle: $settings.hideBalanceOnOpen, - testIdentifier: "HideBalanceOnOpen" - ) - - SettingsListLabel( - title: t("settings__security__clipboard"), - toggle: $settings.readClipboard, - testIdentifier: "AutoReadClipboard" - ) + // Backup section + SettingsSectionHeader(t("settings__security__section_backup")) + + Button(action: { + sheets.showSheet(.backup, data: BackupConfig(view: .mnemonic)) + }) { + SettingsRow( + title: t("settings__backup__wallet"), + iconName: "lock-key" + ) + } + .accessibilityIdentifier("BackupWallet") - SettingsListLabel( - title: t("settings__security__warn_100"), - toggle: $settings.warnWhenSendingOver100, - testIdentifier: "SendAmountWarning" - ) + NavigationLink(value: Route.dataBackups) { + SettingsRow( + title: t("settings__backup__data"), + iconName: "database" + ) + } + .accessibilityIdentifier("BackupSettings") - // PIN Code Section - if !settings.pinEnabled { - Button { - sheets.showSheet(.security, data: SecurityConfig(showLaterButton: false)) - } label: { - SettingsListLabel( - title: t("settings__security__pin"), - rightText: t("settings__security__pin_disabled") - ) - } - .accessibilityIdentifier("PINCode") - } else { - NavigationLink(value: Route.disablePin) { - SettingsListLabel( - title: t("settings__security__pin"), - rightText: t("settings__security__pin_enabled") - ) - } - .accessibilityIdentifier("PINCode") + NavigationLink(value: Route.reset) { + SettingsRow( + title: t("settings__backup__reset"), + iconName: "arrow-counter-clockwise" + ) } + .accessibilityIdentifier("ResetAndRestore") + + // Safety section + SettingsSectionHeader(t("settings__security__section_safety")) + .padding(.top, 16) + + NavigationLink(value: Route.changePin) { + SettingsRow( + title: t("settings__security__pin"), + iconName: "shield", + rightText: settings.pinEnabled ? t("settings__security__pin_enabled") : t("settings__security__pin_disabled") + ) + } + .accessibilityIdentifier("PINCode") if settings.pinEnabled { - NavigationLink(value: Route.changePin) { - SettingsListLabel( - title: t("settings__security__pin_change") - ) - } - .accessibilityIdentifier("PINChange") - Button { showPinCheckForPayments = true } label: { - SettingsListLabel( + SettingsRow( title: t("settings__security__pin_payments"), + iconName: "coins", rightIcon: nil, toggle: Binding( get: { settings.requirePinForPayments }, @@ -104,9 +87,9 @@ struct SecurityPrivacySettingsView: View { .accessibilityIdentifier("EnablePinForPayments") if isBiometricAvailable { - // Biometrics toggle with custom handling - SettingsListLabel( + SettingsRow( title: t("settings__security__use_bio", variables: ["biometryTypeName": biometryTypeName]), + iconName: "smiley", toggle: Binding( get: { settings.useBiometrics }, set: { newValue in @@ -115,18 +98,46 @@ struct SecurityPrivacySettingsView: View { ), testIdentifier: "UseBiometryInstead" ) - - // Footer text for Biometrics - BodySText(t("settings__security__footer", variables: ["biometryTypeName": biometryTypeName])) - .padding(.top, 16) } } + + SettingsRow( + title: t("settings__security__warn_100"), + iconName: "warning", + toggle: $settings.warnWhenSendingOver100, + testIdentifier: "SendAmountWarning" + ) + + // Privacy section + SettingsSectionHeader(t("settings__security__section_privacy")) + .padding(.top, 16) + + SettingsRow( + title: t("settings__security__swipe_balance_to_hide"), + iconName: "hand-pointing", + toggle: $settings.swipeBalanceToHide, + testIdentifier: "SwipeBalanceToHide" + ) + + SettingsRow( + title: t("settings__security__hide_balance_on_open"), + iconName: "eye-slash", + toggle: $settings.hideBalanceOnOpen, + testIdentifier: "HideBalanceOnOpen" + ) + + SettingsRow( + title: t("settings__security__clipboard"), + iconName: "clipboard", + toggle: $settings.readClipboard, + testIdentifier: "AutoReadClipboard" + ) } + .padding(.top, 16) .padding(.horizontal, 16) .bottomSafeAreaPadding() } } - .navigationBarHidden(true) .navigationDestination(isPresented: $showPinCheckForPayments) { PinCheckView( title: t("security__pin_enter"), @@ -137,10 +148,7 @@ struct SecurityPrivacySettingsView: View { } ) } - .alert( - t("security__bio_error_title"), - isPresented: $showingBiometricError - ) { + .alert(t("security__bio_error_title"), isPresented: $showingBiometricError) { Button(t("common__ok")) { // Error handled, user acknowledged } @@ -155,7 +163,7 @@ struct SecurityPrivacySettingsView: View { requestBiometricPermission { success in if success { settings.useBiometrics = true - Logger.debug("Biometric authentication enabled", context: "SecurityPrivacySettingsView") + Logger.debug("Biometric authentication enabled", context: "SecuritySettingsView") } else { // Authentication failed - keep toggle off // The toggle will automatically revert since we're not setting the value @@ -167,7 +175,7 @@ struct SecurityPrivacySettingsView: View { requestBiometricPermission { success in if success { settings.useBiometrics = false - Logger.debug("Biometric authentication disabled", context: "SecurityPrivacySettingsView") + Logger.debug("Biometric authentication disabled", context: "SecuritySettingsView") } else { // Authentication failed - keep toggle on // The toggle will automatically revert since we're not setting the value @@ -234,12 +242,12 @@ struct SecurityPrivacySettingsView: View { showingBiometricError = true } - Logger.error("Biometric authentication error: \(error)", context: "SecurityPrivacySettingsView") + Logger.error("Biometric authentication error: \(error)", context: "SecuritySettingsView") } } #Preview { - SecurityPrivacySettingsView() + SecuritySettingsView() .environmentObject(SheetViewModel()) .environmentObject(SettingsViewModel.shared) .preferredColorScheme(.dark) diff --git a/Bitkit/Views/Settings/SupportScreen.swift b/Bitkit/Views/Settings/SupportScreen.swift new file mode 100644 index 000000000..8b5c97061 --- /dev/null +++ b/Bitkit/Views/Settings/SupportScreen.swift @@ -0,0 +1,178 @@ +import SwiftUI + +private struct DiagonalCut: Shape { + var cornerRadius: CGFloat = 36 + + func path(in rect: CGRect) -> Path { + var path = Path() + let r = min(cornerRadius, rect.width / 4, rect.height / 4) + + let leftCutY = rect.maxY - 210 + path.move(to: CGPoint(x: rect.minX, y: leftCutY)) + + let rightCutY = rect.maxY - 300 + path.addLine(to: CGPoint(x: rect.maxX, y: rightCutY)) + + // Right edge to just above bottom-right corner + path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - r)) + // Rounded bottom-right corner + path.addArc( + center: CGPoint(x: rect.maxX - r, y: rect.maxY - r), + radius: r, + startAngle: Angle(radians: 0), + endAngle: Angle(radians: .pi / 2), + clockwise: false + ) + // Bottom edge to just before bottom-left corner + path.addLine(to: CGPoint(x: rect.minX + r, y: rect.maxY)) + // Rounded bottom-left corner + path.addArc( + center: CGPoint(x: rect.minX + r, y: rect.maxY - r), + radius: r, + startAngle: Angle(radians: .pi / 2), + endAngle: Angle(radians: .pi), + clockwise: false + ) + path.closeSubpath() + + return path + } +} + +struct SupportScreen: View { + @EnvironmentObject private var app: AppViewModel + @Environment(\.openURL) private var openURL + + @State private var versionTapCount = 0 + + @AppStorage("showDevSettings") private var showDevSettings = Env.isDebug + + private var appVersion: String { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" + return "\(version) (\(build))" + } + + private var shareText: String { + return t( + "settings__about__shareText", + variables: ["appStoreUrl": Env.appStoreUrl, "playStoreUrl": Env.playStoreUrl] + ) + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: t("settings__support__title")) + .padding(.horizontal, 16) + .padding(.bottom, 16) + + GeometryReader { geometry in + ScrollView(showsIndicators: false) { + ZStack { + // Orange diagonal background (scrolls with content) + Color.brandAccent + .clipShape(DiagonalCut()) + .ignoresSafeArea() + + VStack(alignment: .leading, spacing: 0) { + BodyMText(t("settings__support__text")) + .padding(.bottom, 16) + + VStack(spacing: 0) { + NavigationLink(value: Route.reportIssue) { + SettingsRow(title: t("settings__support__report"), iconName: "warning") + } + + Button(action: { + openURL(URL(string: Env.helpUrl)!) + }) { + SettingsRow(title: t("settings__support__help"), iconName: "question") + } + + NavigationLink(value: Route.appStatus) { + SettingsRow(title: t("settings__support__status"), iconName: "power") + } + .accessibilityIdentifier("AppStatus") + + Button(action: { + openURL(URL(string: Env.termsOfServiceUrl)!) + }) { + SettingsRow(title: t("settings__about__legal"), iconName: "file-text") + } + + ShareLink(item: shareText, message: Text(shareText)) { + SettingsRow(title: t("settings__about__share"), iconName: "share") + } + + Button(action: { + onVersionTap() + }) { + SettingsRow( + title: t("settings__about__version"), + iconName: "stack", + rightText: appVersion, + rightIcon: nil + ) + } + } + + Spacer(minLength: 32) + + VStack(alignment: .center, spacing: 0) { + Image("logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxHeight: 100) + .accessibilityIdentifier("AboutLogo") + } + .frame(maxWidth: .infinity) + .padding(.bottom, 16) + + Social() + .padding(.bottom, 16) + + BodyMText("Bitkit was crafted by Synonym Software, S.A. DE C.V. ©2025. All rights reserved.") + .padding(.bottom, 16) + + HStack(alignment: .center, spacing: 10) { + Image("synonym-logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 24) + + Image("tether-logo") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 16) + } + .frame(maxWidth: .infinity, alignment: .center) + .frame(height: 24) + .padding(.bottom, 32) + } + .frame(minHeight: geometry.size.height) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() + } + } + } + .ignoresSafeArea() + } + .navigationBarHidden(true) + } + + private func onVersionTap() { + versionTapCount += 1 + + // When tapped 5 times, toggle developer mode + if versionTapCount >= 5 { + versionTapCount = 0 + showDevSettings.toggle() + + app.toast( + type: .info, + title: t(showDevSettings ? "settings__dev_enabled_title" : "settings__dev_disabled_title"), + description: t(showDevSettings ? "settings__dev_enabled_message" : "settings__dev_disabled_message") + ) + } + } +} diff --git a/Bitkit/Views/Settings/SupportView.swift b/Bitkit/Views/Settings/SupportView.swift deleted file mode 100644 index af614d099..000000000 --- a/Bitkit/Views/Settings/SupportView.swift +++ /dev/null @@ -1,55 +0,0 @@ -import SwiftUI - -struct SupportView: View { - @Environment(\.openURL) private var openURL - - var body: some View { - VStack(spacing: 0) { - NavigationBar(title: t("settings__support__title")) - .padding(.bottom, 16) - - BodyMText(t("settings__support__text")) - .padding(.bottom, 16) - - VStack(spacing: 0) { - NavigationLink(value: Route.reportIssue) { - SettingsListLabel(title: t("settings__support__report")) - } - - Button(action: { - openURL(URL(string: Env.helpUrl)!) - }) { - SettingsListLabel(title: t("settings__support__help")) - } - - NavigationLink(value: Route.appStatus) { - SettingsListLabel(title: t("settings__support__status")) - } - .accessibilityIdentifier("AppStatus") - } - - Spacer(minLength: 32) - - VStack { - Image("question-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(maxHeight: 256) - } - - Spacer(minLength: 32) - - Social() - } - .navigationBarHidden(true) - .padding(.horizontal, 16) - .bottomSafeAreaPadding() - } -} - -#Preview { - NavigationView { - SupportView() - } - .preferredColorScheme(.dark) -} diff --git a/Bitkit/Views/Settings/TransactionSpeed/TransactionSpeedSettingsView.swift b/Bitkit/Views/Settings/TransactionSpeed/TransactionSpeedSettingsView.swift index e95b42aff..ed9c0183a 100644 --- a/Bitkit/Views/Settings/TransactionSpeed/TransactionSpeedSettingsView.swift +++ b/Bitkit/Views/Settings/TransactionSpeed/TransactionSpeedSettingsView.swift @@ -66,8 +66,7 @@ struct TransactionSpeedSettingsView: View { ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - CaptionMText(t("settings__general__speed_default")) - .frame(height: 50) + SettingsSectionHeader(t("settings__general__speed_default")) VStack(spacing: 0) { TransactionSpeedSettingsRow( @@ -79,7 +78,7 @@ struct TransactionSpeedSettingsView: View { } ) - Divider() + CustomDivider() TransactionSpeedSettingsRow( speed: .normal, @@ -90,7 +89,7 @@ struct TransactionSpeedSettingsView: View { } ) - Divider() + CustomDivider() TransactionSpeedSettingsRow( speed: .slow, @@ -101,7 +100,7 @@ struct TransactionSpeedSettingsView: View { } ) - Divider() + CustomDivider() TransactionSpeedSettingsRow( speed: .custom(satsPerVByte: 1), // Placeholder diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 03d5f3a5f..541f23223 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -46,13 +46,13 @@ struct ReceiveQr: View { // Show unified tab when we have a Lightning invoice (even if channels not yet usable) if !wallet.bolt11.isEmpty { return [ - TabItem(.savings), - TabItem(.unified, activeColor: .white), + TabItem(.savings, activeColor: .brandAccent), + TabItem(.unified, activeColor: .textPrimary), TabItem(.spending, activeColor: .purpleAccent), ] } else { return [ - TabItem(.savings), + TabItem(.savings, activeColor: .textPrimary), TabItem(.spending, activeColor: .purpleAccent), ] }