diff --git a/Bitkit/Components/ConnectionIssuesView.swift b/Bitkit/Components/ConnectionIssuesView.swift new file mode 100644 index 000000000..ee2c79d96 --- /dev/null +++ b/Bitkit/Components/ConnectionIssuesView.swift @@ -0,0 +1,121 @@ +import SwiftUI + +/// A full-screen overlay displayed when the device loses internet connectivity. +/// Shows a phone illustration with animated dashed gradient rings and a loading spinner. +struct ConnectionIssuesView: View { + let title: String + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + SheetHeader(title: title, showBackButton: false) + + Spacer().frame(height: 24) + + ZStack(alignment: .center) { + DashedRingsLayer(radii: [200]) + + Image("phone") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 311) + + DashedRingsLayer(radii: [150, 100, 50]) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + + DisplayText( + t("other__connection_issues_title"), + accentColor: .yellowAccent + ) + + Spacer().frame(height: 8) + + BodyMText( + t("other__connection_issues_explain"), + textColor: .white64 + ) + .frame(maxWidth: .infinity, alignment: .leading) + + Spacer().frame(height: 24) + + ActivityIndicator() + .frame(maxWidth: .infinity) + + Spacer().frame(height: 16) + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .sheetBackground() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .accessibilityIdentifier("ConnectionIssuesView") + } +} + +// MARK: - Dashed Gradient Rings + +private struct DashedRingsLayer: View { + let radii: [CGFloat] + + var body: some View { + Canvas { context, size in + let center = CGPoint(x: size.width * 0.25, y: size.height * 0.40) + + for radius in radii { + let rect = CGRect( + x: center.x - radius, + y: center.y - radius, + width: radius * 2, + height: radius * 2 + ) + + var path = Path() + path.addEllipse(in: rect) + + let gradient = Gradient(colors: [.black, .yellowAccent]) + let startPoint = CGPoint(x: rect.minX, y: rect.minY) + let endPoint = CGPoint(x: rect.maxX, y: rect.maxY) + + context.stroke( + path, + with: .linearGradient(gradient, startPoint: startPoint, endPoint: endPoint), + style: StrokeStyle(lineWidth: 1, dash: [8, 6]) + ) + } + } + .allowsHitTesting(false) + } +} + +// MARK: - View Modifier + +private struct ConnectionIssuesOverlayModifier: ViewModifier { + let title: String + @EnvironmentObject private var network: NetworkMonitor + + func body(content: Content) -> some View { + ZStack { + content + + if !network.isConnected { + ConnectionIssuesView(title: title) + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.3), value: network.isConnected) + } +} + +extension View { + /// Overlays a `ConnectionIssuesView` when the device is offline. + /// The underlying content remains mounted so navigation state and inputs are preserved. + func connectionIssuesOverlay(title: String) -> some View { + modifier(ConnectionIssuesOverlayModifier(title: title)) + } +} + +// MARK: - Preview + +#Preview { + ConnectionIssuesView(title: "Send Bitcoin") + .preferredColorScheme(.dark) +} diff --git a/Bitkit/Components/Header.swift b/Bitkit/Components/Header.swift index 8d962d98b..4c0b4ff7f 100644 --- a/Bitkit/Components/Header.swift +++ b/Bitkit/Components/Header.swift @@ -65,7 +65,6 @@ struct Header: View { .padding(.trailing, 10) } - @ViewBuilder private var profileButton: some View { Button { if pubkyProfile.isAuthenticated || pubkyProfile.cachedName != nil { diff --git a/Bitkit/Components/ProfileEditFormView.swift b/Bitkit/Components/ProfileEditFormView.swift index 285bfb011..5a79eaa02 100644 --- a/Bitkit/Components/ProfileEditFormView.swift +++ b/Bitkit/Components/ProfileEditFormView.swift @@ -94,7 +94,6 @@ struct ProfileEditFormView: View { // MARK: - Pubky Key Section - @ViewBuilder private var pubkyKeySection: some View { VStack(spacing: 8) { CaptionMText(publicKeyLabel, textColor: .white64) @@ -111,7 +110,6 @@ struct ProfileEditFormView: View { // MARK: - Bio Section - @ViewBuilder private var bioSection: some View { VStack(alignment: .leading, spacing: 8) { CaptionMText(t("profile__create_bio_label"), textColor: .white64) @@ -129,7 +127,6 @@ struct ProfileEditFormView: View { // MARK: - Links Section - @ViewBuilder private var linksSection: some View { VStack(alignment: .leading, spacing: 8) { ForEach(links.indices, id: \.self) { index in @@ -198,7 +195,6 @@ struct ProfileEditFormView: View { // MARK: - Delete Section - @ViewBuilder private func deleteSection(label: String, action: @escaping () -> Void) -> some View { VStack(alignment: .leading, spacing: 8) { CaptionMText(t("profile__edit_delete_section"), textColor: .white64) @@ -221,7 +217,6 @@ struct ProfileEditFormView: View { // MARK: - Tags Section - @ViewBuilder private var tagsSection: some View { VStack(alignment: .leading, spacing: 8) { if !tags.isEmpty { @@ -246,7 +241,6 @@ struct ProfileEditFormView: View { } } - @ViewBuilder private var footerBar: some View { VStack(spacing: 0) { LinearGradient( diff --git a/Bitkit/Components/PubkyImage.swift b/Bitkit/Components/PubkyImage.swift index 9af821326..ab0e7b4cc 100644 --- a/Bitkit/Components/PubkyImage.swift +++ b/Bitkit/Components/PubkyImage.swift @@ -31,7 +31,6 @@ struct PubkyImage: View { } } - @ViewBuilder private var placeholder: some View { Circle() .fill(Color.gray5) diff --git a/Bitkit/Components/SyncNodeView.swift b/Bitkit/Components/SyncNodeView.swift index 1112293b3..d8e10083f 100644 --- a/Bitkit/Components/SyncNodeView.swift +++ b/Bitkit/Components/SyncNodeView.swift @@ -87,3 +87,35 @@ struct SyncNodeView: View { } } } + +// MARK: - View Modifier + +private struct SyncNodeOverlayModifier: ViewModifier { + @EnvironmentObject private var wallet: WalletViewModel + + private var shouldShowSyncOverlay: Bool { + guard wallet.nodeLifecycleState == .running else { return true } + let hasAnyChannels = (wallet.channels?.isEmpty == false) || wallet.channelCount > 0 + guard hasAnyChannels else { return false } + return !wallet.hasUsableChannels + } + + func body(content: Content) -> some View { + ZStack { + content + + if shouldShowSyncOverlay { + SyncNodeView() + .transition(.opacity) + } + } + .animation(.easeInOut(duration: 0.3), value: shouldShowSyncOverlay) + } +} + +extension View { + /// Overlays a `SyncNodeView` when the node is not running or channels aren't usable yet. + func syncNodeOverlay() -> some View { + modifier(SyncNodeOverlayModifier()) + } +} diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 911bcca45..b9da780b5 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -462,7 +462,6 @@ struct MainNavView: View { } } - @ViewBuilder private func missingPendingImportView(fallbackRoute: Route) -> some View { Color.customBlack .task { diff --git a/Bitkit/Managers/ContactsManager.swift b/Bitkit/Managers/ContactsManager.swift index 429567ffe..cd7bbe6f8 100644 --- a/Bitkit/Managers/ContactsManager.swift +++ b/Bitkit/Managers/ContactsManager.swift @@ -63,7 +63,7 @@ enum ContactsManagerError: LocalizedError { // MARK: - PubkyContact -struct PubkyContact: Identifiable, Hashable, Sendable { +struct PubkyContact: Identifiable, Hashable { let id: String let publicKey: String let profile: PubkyProfile @@ -656,13 +656,11 @@ class ContactsManager: ObservableObject { } let normalized = message.lowercased() - let indicatesMissingResource = normalized.contains("404") + return normalized.contains("404") || normalized.contains("no such file") || normalized.contains("does not exist") || normalized.contains("profile not found") || normalized.contains("profilenotfound") || (normalized.contains("fetch failed") && normalized.contains("not found")) - - return indicatesMissingResource } } diff --git a/Bitkit/Managers/NetworkMonitor.swift b/Bitkit/Managers/NetworkMonitor.swift index 49b6c716e..2c0b65acd 100644 --- a/Bitkit/Managers/NetworkMonitor.swift +++ b/Bitkit/Managers/NetworkMonitor.swift @@ -25,8 +25,11 @@ final class NetworkMonitor: ObservableObject { // Set the pathUpdateHandler monitor.pathUpdateHandler = { [weak self] path in DispatchQueue.main.async { + let wasConnected = self?.isConnected + let isNowConnected = path.status == .satisfied + // Check if the device is connected to the internet - self?.isConnected = path.status == .satisfied + self?.isConnected = isNowConnected // Check if the network is expensive (e.g. cellular data) self?.isExpensive = path.isExpensive @@ -36,6 +39,14 @@ final class NetworkMonitor: ObservableObject { // Update the network path self?.nwPath = path + + if wasConnected != isNowConnected { + let interfaceType = path.availableInterfaces.first?.type + Logger + .debug( + "Network connectivity changed: \(isNowConnected ? "connected" : "disconnected") (interface: \(String(describing: interfaceType)), status: \(path.status))" + ) + } } } diff --git a/Bitkit/Managers/PubkyProfileManager.swift b/Bitkit/Managers/PubkyProfileManager.swift index e93d8155a..4d912a5df 100644 --- a/Bitkit/Managers/PubkyProfileManager.swift +++ b/Bitkit/Managers/PubkyProfileManager.swift @@ -39,7 +39,7 @@ class PubkyProfileManager: ObservableObject { // MARK: - Initialization & Session Restoration - private enum InitResult: Sendable { + private enum InitResult { case noSession case restored(publicKey: String) case restorationFailed diff --git a/Bitkit/Models/PubkyProfile.swift b/Bitkit/Models/PubkyProfile.swift index a228ab37b..5445d1c5e 100644 --- a/Bitkit/Models/PubkyProfile.swift +++ b/Bitkit/Models/PubkyProfile.swift @@ -79,7 +79,7 @@ struct PubkyProfileData: Codable { // MARK: - PubkyProfileLink -struct PubkyProfileLink: Identifiable, Sendable { +struct PubkyProfileLink: Identifiable { let id = UUID() let label: String let url: String @@ -87,7 +87,7 @@ struct PubkyProfileLink: Identifiable, Sendable { // MARK: - PubkyProfile -struct PubkyProfile: Sendable { +struct PubkyProfile { let publicKey: String let name: String let bio: String diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index e3fba4dae..2e6b06b95 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -391,6 +391,8 @@ "other__connection_reconnect_msg" = "Lost connection to Electrum, trying to reconnect..."; "other__connection_back_title" = "Internet Connection Restored"; "other__connection_back_msg" = "Bitkit successfully reconnected to the Internet."; +"other__connection_issues_title" = "Connection\nIssues"; +"other__connection_issues_explain" = "It appears you're disconnected. Please check your connection. Bitkit will try to reconnect every few seconds."; "other__high_balance__nav_title" = "High Balance"; "other__high_balance__title" = "High\nBalance"; "other__high_balance__text" = "Your wallet balance exceeds $500.\nFor your security, consider moving some of your savings to an offline wallet."; diff --git a/Bitkit/Views/Contacts/AddContactView.swift b/Bitkit/Views/Contacts/AddContactView.swift index eee6425ea..d09591c94 100644 --- a/Bitkit/Views/Contacts/AddContactView.swift +++ b/Bitkit/Views/Contacts/AddContactView.swift @@ -44,7 +44,6 @@ struct AddContactView: View { @State private var dashedCircleRotation: Double = 0 - @ViewBuilder private var loadingContent: some View { VStack(spacing: 0) { CaptionMText(truncatedPublicKey, textColor: .white64) @@ -83,7 +82,6 @@ struct AddContactView: View { } } - @ViewBuilder private var retrievingAnimation: some View { ZStack { Image("ellipse-outer-green") @@ -107,7 +105,6 @@ struct AddContactView: View { // MARK: - Result State - @ViewBuilder private func resultContent(_ profile: PubkyProfile) -> some View { VStack(spacing: 0) { ScrollView { @@ -154,7 +151,6 @@ struct AddContactView: View { // MARK: - Error State - @ViewBuilder private var errorContent: some View { VStack(spacing: 16) { Spacer() diff --git a/Bitkit/Views/Contacts/ContactDetailView.swift b/Bitkit/Views/Contacts/ContactDetailView.swift index b021dca34..3776b46cd 100644 --- a/Bitkit/Views/Contacts/ContactDetailView.swift +++ b/Bitkit/Views/Contacts/ContactDetailView.swift @@ -49,7 +49,6 @@ struct ContactDetailView: View { // MARK: - Contact Body - @ViewBuilder private func contactBody(_ profile: PubkyProfile) -> some View { ScrollView { VStack(spacing: 0) { @@ -88,7 +87,6 @@ struct ContactDetailView: View { // MARK: - Action Buttons - @ViewBuilder private var contactActions: some View { HStack(spacing: 16) { GradientCircleButton(icon: "copy", accessibilityLabel: t("common__copy")) { @@ -111,7 +109,6 @@ struct ContactDetailView: View { // MARK: - Links / Metadata - @ViewBuilder private func linksSection(_ profile: PubkyProfile) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(Array(profile.links.enumerated()), id: \.element.id) { index, link in @@ -122,7 +119,6 @@ struct ContactDetailView: View { // MARK: - Tags - @ViewBuilder private func tagsSection(_ profile: PubkyProfile) -> some View { VStack(alignment: .leading, spacing: 8) { CaptionMText(t("profile__create_tags_label"), textColor: .white64) @@ -140,7 +136,6 @@ struct ContactDetailView: View { } } - @ViewBuilder private var addTagButton: some View { IconActionButton( icon: "tag", @@ -203,7 +198,6 @@ struct ContactDetailView: View { // MARK: - Loading & Empty States - @ViewBuilder private var loadingContent: some View { VStack { Spacer() @@ -213,7 +207,6 @@ struct ContactDetailView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } - @ViewBuilder private var emptyContent: some View { VStack(spacing: 16) { Spacer() diff --git a/Bitkit/Views/Contacts/ContactImportOverviewView.swift b/Bitkit/Views/Contacts/ContactImportOverviewView.swift index d5264c5d7..e6c062047 100644 --- a/Bitkit/Views/Contacts/ContactImportOverviewView.swift +++ b/Bitkit/Views/Contacts/ContactImportOverviewView.swift @@ -56,7 +56,6 @@ struct ContactImportOverviewView: View { // MARK: - Profile Row - @ViewBuilder private var profileRow: some View { HStack(alignment: .top, spacing: 16) { HeadlineText(profile.name) @@ -84,7 +83,6 @@ struct ContactImportOverviewView: View { // MARK: - Contacts Summary - @ViewBuilder private var contactsSummary: some View { HStack(spacing: 16) { BodyMSBText(t("contacts__import_friends_count", variables: ["count": "\(contacts.count)"])) @@ -141,7 +139,6 @@ struct ContactImportOverviewView: View { // MARK: - Button Bar - @ViewBuilder private var buttonBar: some View { HStack(spacing: 16) { CustomButton(title: t("contacts__import_select"), variant: .secondary) { diff --git a/Bitkit/Views/Contacts/ContactImportSelectView.swift b/Bitkit/Views/Contacts/ContactImportSelectView.swift index d9c34f556..43b4da728 100644 --- a/Bitkit/Views/Contacts/ContactImportSelectView.swift +++ b/Bitkit/Views/Contacts/ContactImportSelectView.swift @@ -86,7 +86,6 @@ struct ContactImportSelectView: View { // MARK: - Checkmark - @ViewBuilder private func checkmark(isSelected: Bool) -> some View { ZStack { if isSelected { @@ -109,7 +108,6 @@ struct ContactImportSelectView: View { // MARK: - Contact Avatar - @ViewBuilder private func contactAvatar(name: String, imageUrl: String?) -> some View { Group { if let imageUrl { @@ -130,7 +128,6 @@ struct ContactImportSelectView: View { // MARK: - Footer Bar - @ViewBuilder private var footerBar: some View { VStack(spacing: 0) { CustomDivider() @@ -167,7 +164,6 @@ struct ContactImportSelectView: View { // MARK: - Pill Button - @ViewBuilder private func pillButton(title: String, isActive: Bool, action: @escaping () -> Void) -> some View { Button(action: action) { Text(title) diff --git a/Bitkit/Views/Contacts/ContactsListView.swift b/Bitkit/Views/Contacts/ContactsListView.swift index 5aa9d0858..572a745ec 100644 --- a/Bitkit/Views/Contacts/ContactsListView.swift +++ b/Bitkit/Views/Contacts/ContactsListView.swift @@ -65,7 +65,6 @@ struct ContactsListView: View { // MARK: - Search Bar + Add Button - @ViewBuilder private var searchAndAddBar: some View { HStack(spacing: 12) { HStack(spacing: 12) { @@ -118,7 +117,6 @@ struct ContactsListView: View { // MARK: - My Profile Section - @ViewBuilder private func myProfileSection(_ profile: PubkyProfile) -> some View { VStack(alignment: .leading, spacing: 0) { sectionHeader(t("contacts__my_profile")) @@ -163,7 +161,6 @@ struct ContactsListView: View { // MARK: - Section Header - @ViewBuilder private func sectionHeader(_ title: String) -> some View { CaptionMText(title, textColor: .white64) .padding(.vertical, 16) @@ -171,7 +168,6 @@ struct ContactsListView: View { // MARK: - Contact Row - @ViewBuilder private func contactRow(name: String, truncatedKey: String, imageUrl: String?, onTap: @escaping () -> Void) -> some View { Button(action: onTap) { HStack(spacing: 16) { @@ -191,7 +187,6 @@ struct ContactsListView: View { .accessibilityLabel(name) } - @ViewBuilder private func contactAvatar(name: String, imageUrl: String?) -> some View { Group { if let imageUrl { @@ -225,7 +220,6 @@ struct ContactsListView: View { // MARK: - Loading & Empty States - @ViewBuilder private var loadingContent: some View { VStack { Spacer() @@ -235,7 +229,6 @@ struct ContactsListView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } - @ViewBuilder private func errorContent(message: String) -> some View { VStack(spacing: 16) { Spacer() @@ -260,7 +253,6 @@ struct ContactsListView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } - @ViewBuilder private var emptyContent: some View { VStack(spacing: 0) { if pubkyProfile.isAuthenticated, let profile = pubkyProfile.profile { diff --git a/Bitkit/Views/Contacts/EditContactView.swift b/Bitkit/Views/Contacts/EditContactView.swift index 36d30bc17..92758916f 100644 --- a/Bitkit/Views/Contacts/EditContactView.swift +++ b/Bitkit/Views/Contacts/EditContactView.swift @@ -60,7 +60,6 @@ struct EditContactView: View { // MARK: - Avatar - @ViewBuilder private var avatarSection: some View { PhotosPicker(selection: $selectedPhotoItem, matching: .images) { Group { diff --git a/Bitkit/Views/Profile/AddLinkSheet.swift b/Bitkit/Views/Profile/AddLinkSheet.swift index 82416a037..72dc4763f 100644 --- a/Bitkit/Views/Profile/AddLinkSheet.swift +++ b/Bitkit/Views/Profile/AddLinkSheet.swift @@ -66,7 +66,6 @@ struct AddLinkSheet: View { } } - @ViewBuilder private var labelFieldWithSuggestions: some View { HStack(spacing: 0) { ZStack(alignment: .leading) { diff --git a/Bitkit/Views/Profile/AddProfileTagSheet.swift b/Bitkit/Views/Profile/AddProfileTagSheet.swift index 846b8d4f7..3617b33f1 100644 --- a/Bitkit/Views/Profile/AddProfileTagSheet.swift +++ b/Bitkit/Views/Profile/AddProfileTagSheet.swift @@ -45,7 +45,6 @@ struct AddProfileTagSheet: View { } } - @ViewBuilder private var tagFieldWithSuggestions: some View { HStack(spacing: 0) { ZStack(alignment: .leading) { diff --git a/Bitkit/Views/Profile/CreateProfileView.swift b/Bitkit/Views/Profile/CreateProfileView.swift index de1e4fca0..16a2f872e 100644 --- a/Bitkit/Views/Profile/CreateProfileView.swift +++ b/Bitkit/Views/Profile/CreateProfileView.swift @@ -77,7 +77,6 @@ struct CreateProfileView: View { // MARK: - Avatar Section - @ViewBuilder private var avatarSection: some View { PhotosPicker(selection: $selectedPhotoItem, matching: .images) { avatarContent @@ -112,7 +111,6 @@ struct CreateProfileView: View { // MARK: - Name Input - @ViewBuilder private var nameInput: some View { SwiftUI.TextField( t("profile__create_name_placeholder"), @@ -129,7 +127,6 @@ struct CreateProfileView: View { // MARK: - Pubky Key Section - @ViewBuilder private var pubkyKeySection: some View { VStack(spacing: 8) { CaptionMText(t("profile__create_pubky_display_label"), textColor: .white64) @@ -147,7 +144,6 @@ struct CreateProfileView: View { // MARK: - Loading - @ViewBuilder private var loadingView: some View { VStack(spacing: 12) { Spacer() diff --git a/Bitkit/Views/Profile/EditProfileView.swift b/Bitkit/Views/Profile/EditProfileView.swift index dc53ede02..b1fe68284 100644 --- a/Bitkit/Views/Profile/EditProfileView.swift +++ b/Bitkit/Views/Profile/EditProfileView.swift @@ -59,7 +59,6 @@ struct EditProfileView: View { // MARK: - Avatar Picker - @ViewBuilder private var avatarPicker: some View { PhotosPicker(selection: $selectedPhotoItem, matching: .images) { avatarContent diff --git a/Bitkit/Views/Profile/ProfileView.swift b/Bitkit/Views/Profile/ProfileView.swift index cf364160e..62e5609a5 100644 --- a/Bitkit/Views/Profile/ProfileView.swift +++ b/Bitkit/Views/Profile/ProfileView.swift @@ -46,7 +46,6 @@ struct ProfileView: View { // MARK: - Profile Content - @ViewBuilder private func profileContent(_ profile: PubkyProfile) -> some View { ScrollView { VStack(spacing: 0) { @@ -86,7 +85,6 @@ struct ProfileView: View { // MARK: - Actions (edit, copy, share) - @ViewBuilder private var profileActions: some View { HStack(spacing: 16) { GradientCircleButton(icon: "pencil", accessibilityLabel: t("profile__edit")) { @@ -111,7 +109,6 @@ struct ProfileView: View { // MARK: - QR Code - @ViewBuilder private func profileQRCode(_ profile: PubkyProfile) -> some View { VStack(spacing: 12) { ZStack { @@ -133,7 +130,6 @@ struct ProfileView: View { // MARK: - Links / Metadata - @ViewBuilder private func profileLinks(_ profile: PubkyProfile) -> some View { VStack(alignment: .leading, spacing: 0) { ForEach(Array(profile.links.enumerated()), id: \.element.id) { index, link in @@ -144,7 +140,6 @@ struct ProfileView: View { // MARK: - Tags - @ViewBuilder private func profileTags(_ profile: PubkyProfile) -> some View { VStack(alignment: .leading, spacing: 8) { CaptionMText(t("profile__create_tags_label"), textColor: .white64) @@ -160,7 +155,6 @@ struct ProfileView: View { // MARK: - Loading / Empty States - @ViewBuilder private var loadingContent: some View { VStack { Spacer() @@ -170,7 +164,6 @@ struct ProfileView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } - @ViewBuilder private var emptyContent: some View { VStack(spacing: 16) { Spacer() diff --git a/Bitkit/Views/Profile/PubkyChoiceView.swift b/Bitkit/Views/Profile/PubkyChoiceView.swift index c00e7f317..9002b6a61 100644 --- a/Bitkit/Views/Profile/PubkyChoiceView.swift +++ b/Bitkit/Views/Profile/PubkyChoiceView.swift @@ -62,7 +62,6 @@ struct PubkyChoiceView: View { // MARK: - Title Section - @ViewBuilder private var titleSection: some View { VStack(alignment: .leading, spacing: 8) { DisplayText( @@ -82,7 +81,6 @@ struct PubkyChoiceView: View { // MARK: - Option Cards - @ViewBuilder private var optionCards: some View { VStack(spacing: 8) { choiceCard( @@ -110,7 +108,6 @@ struct PubkyChoiceView: View { } } - @ViewBuilder private func choiceCard( icon: String? = nil, systemIcon: String? = nil, @@ -198,7 +195,6 @@ struct PubkyChoiceView: View { // MARK: - Ring Waiting Card - @ViewBuilder private var ringWaitingCard: some View { VStack(spacing: 12) { HStack(spacing: 16) { @@ -234,7 +230,6 @@ struct PubkyChoiceView: View { // MARK: - Background Illustrations - @ViewBuilder private var backgroundIllustrations: some View { GeometryReader { geo in Image("tag-pubky") diff --git a/Bitkit/Views/Scanner/ScannerScreen.swift b/Bitkit/Views/Scanner/ScannerScreen.swift index 1a6609050..a446c61d5 100644 --- a/Bitkit/Views/Scanner/ScannerScreen.swift +++ b/Bitkit/Views/Scanner/ScannerScreen.swift @@ -58,6 +58,8 @@ struct ScannerScreen: View { .navigationBarHidden(true) .padding(.horizontal, 16) .bottomSafeAreaPadding() + .syncNodeOverlay() + .connectionIssuesOverlay(title: t("other__qr_scan")) .onAppear { scanner.configure( app: app, diff --git a/Bitkit/Views/Scanner/ScannerSheet.swift b/Bitkit/Views/Scanner/ScannerSheet.swift index 511287b8f..f6badb694 100644 --- a/Bitkit/Views/Scanner/ScannerSheet.swift +++ b/Bitkit/Views/Scanner/ScannerSheet.swift @@ -85,6 +85,8 @@ struct ScannerSheet: View { .presentationDragIndicator(.visible) } } + .syncNodeOverlay() + .connectionIssuesOverlay(title: t("other__qr_scan")) } private func handleManualEntrySubmit() async { diff --git a/Bitkit/Views/Sheets/ForceTransferSheet.swift b/Bitkit/Views/Sheets/ForceTransferSheet.swift index 9fba2250d..9fd120882 100644 --- a/Bitkit/Views/Sheets/ForceTransferSheet.swift +++ b/Bitkit/Views/Sheets/ForceTransferSheet.swift @@ -29,6 +29,7 @@ struct ForceTransferSheet: View { onContinue: onForceTransfer ) } + .connectionIssuesOverlay(title: t("lightning__force_nav_title")) } private func onCancel() { diff --git a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift index 03e3723b8..ed22131f8 100644 --- a/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift +++ b/Bitkit/Views/Sheets/PubkyAuthApproval/PubkyAuthApprovalSheet.swift @@ -92,7 +92,6 @@ struct PubkyAuthApprovalSheet: View { // MARK: - Authorize State (Screen 3) - @ViewBuilder private var authorizeContent: some View { VStack(alignment: .leading, spacing: 0) { descriptionText @@ -125,7 +124,6 @@ struct PubkyAuthApprovalSheet: View { // MARK: - Authorizing State (Screen 4) - @ViewBuilder private var authorizingContent: some View { VStack(alignment: .leading, spacing: 0) { descriptionText @@ -149,7 +147,6 @@ struct PubkyAuthApprovalSheet: View { // MARK: - Success State (Screen 5) - @ViewBuilder private var successContent: some View { VStack(alignment: .leading, spacing: 0) { successDescriptionText @@ -178,7 +175,6 @@ struct PubkyAuthApprovalSheet: View { config.request.serviceNames.joined(separator: " and ") } - @ViewBuilder private var descriptionText: some View { BodyMText( t("pubky_auth__description_prefix") + "" + serviceText + "" + t("pubky_auth__description_suffix"), @@ -201,7 +197,6 @@ struct PubkyAuthApprovalSheet: View { .lineSpacing(4) } - @ViewBuilder private var permissionsSection: some View { VStack(alignment: .leading, spacing: 8) { CaptionMText(t("pubky_auth__requested_permissions"), textColor: .white64) @@ -214,7 +209,6 @@ struct PubkyAuthApprovalSheet: View { } } - @ViewBuilder private func permissionRow(_ permission: PubkyAuthPermission) -> some View { HStack(spacing: 4) { Image(systemName: "folder") @@ -230,13 +224,11 @@ struct PubkyAuthApprovalSheet: View { } } - @ViewBuilder private var trustWarning: some View { BodySText(t("pubky_auth__trust_warning")) .lineSpacing(4) } - @ViewBuilder private var profileCard: some View { VStack(alignment: .leading, spacing: 16) { CaptionMText( diff --git a/Bitkit/Views/Transfer/SavingsConfirmView.swift b/Bitkit/Views/Transfer/SavingsConfirmView.swift index 7c7f7681b..8fa8dcef5 100644 --- a/Bitkit/Views/Transfer/SavingsConfirmView.swift +++ b/Bitkit/Views/Transfer/SavingsConfirmView.swift @@ -100,6 +100,7 @@ struct SavingsConfirmView: View { .navigationBarHidden(true) .padding(.horizontal, 16) .bottomSafeAreaPadding() + .connectionIssuesOverlay(title: t("lightning__transfer__nav_title")) } } diff --git a/Bitkit/Views/Transfer/SpendingAmount.swift b/Bitkit/Views/Transfer/SpendingAmount.swift index 750832191..3918060e2 100644 --- a/Bitkit/Views/Transfer/SpendingAmount.swift +++ b/Bitkit/Views/Transfer/SpendingAmount.swift @@ -85,6 +85,7 @@ struct SpendingAmount: View { await calculateMaxTransferAmount() } } + .connectionIssuesOverlay(title: t("lightning__transfer__nav_title")) } private var actionButtons: some View { diff --git a/Bitkit/Views/Transfer/SpendingConfirm.swift b/Bitkit/Views/Transfer/SpendingConfirm.swift index 9501d9a39..7bca45408 100644 --- a/Bitkit/Views/Transfer/SpendingConfirm.swift +++ b/Bitkit/Views/Transfer/SpendingConfirm.swift @@ -134,6 +134,7 @@ struct SpendingConfirm: View { .task { await calculateTransactionFee() } + .connectionIssuesOverlay(title: t("lightning__transfer__nav_title")) } private func onConfirm() async { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift index 9cf6a790d..f393af867 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveSheet.swift @@ -44,6 +44,7 @@ struct ReceiveSheet: View { } } } + .connectionIssuesOverlay(title: t("wallet__receive_bitcoin")) .onAppear { wallet.invoiceAmountSats = 0 wallet.invoiceNote = "" diff --git a/Bitkit/Views/Wallets/Send/SendSheet.swift b/Bitkit/Views/Wallets/Send/SendSheet.swift index 3d3883ca7..c09bfb0a3 100644 --- a/Bitkit/Views/Wallets/Send/SendSheet.swift +++ b/Bitkit/Views/Wallets/Send/SendSheet.swift @@ -95,6 +95,7 @@ struct SendSheet: View { } } .animation(.easeInOut(duration: 0.3), value: shouldShowSyncOverlay) + .connectionIssuesOverlay(title: t("wallet__send_bitcoin")) .onAppear { tagManager.clearSelectedTags() wallet.resetSendState(speed: settings.defaultTransactionSpeed) diff --git a/BitkitTests/PubkyProfileManagerTests.swift b/BitkitTests/PubkyProfileManagerTests.swift index 55ecce146..751efb080 100644 --- a/BitkitTests/PubkyProfileManagerTests.swift +++ b/BitkitTests/PubkyProfileManagerTests.swift @@ -10,25 +10,25 @@ final class PubkyProfileManagerTests: XCTestCase { let json = """ {"signupCode":"abc-123","homeserverPubky":"z6MkPubkyTestKey"} """ - let data = json.data(using: .utf8)! + let data = try XCTUnwrap(json.data(using: .utf8)) let response = try JSONDecoder().decode(HomegateResponse.self, from: data) XCTAssertEqual(response.signupCode, "abc-123") XCTAssertEqual(response.homeserverPubky, "z6MkPubkyTestKey") } - func testHomegateResponseRejectsIncompleteJson() { + func testHomegateResponseRejectsIncompleteJson() throws { let json = """ {"signupCode":"abc-123"} """ - let data = json.data(using: .utf8)! + let data = try XCTUnwrap(json.data(using: .utf8)) XCTAssertThrowsError(try JSONDecoder().decode(HomegateResponse.self, from: data)) } - func testHomegateResponseRejectsEmptyJson() { + func testHomegateResponseRejectsEmptyJson() throws { let json = "{}" - let data = json.data(using: .utf8)! + let data = try XCTUnwrap(json.data(using: .utf8)) XCTAssertThrowsError(try JSONDecoder().decode(HomegateResponse.self, from: data)) } @@ -37,7 +37,7 @@ final class PubkyProfileManagerTests: XCTestCase { let json = """ {"signupCode":"abc","homeserverPubky":"z6Mk","extra":"ignored"} """ - let data = json.data(using: .utf8)! + let data = try XCTUnwrap(json.data(using: .utf8)) let response = try JSONDecoder().decode(HomegateResponse.self, from: data) XCTAssertEqual(response.signupCode, "abc") diff --git a/CHANGELOG.md b/CHANGELOG.md index c071de730..3952463ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Connection issues overlay on Send, Receive, Transfer, and Force Transfer flows #524 - Pubky profile onboarding with contact sync, import, and editing #476 - Add transfer from savings button on empty spending wallet when user has on-chain balance #523