From 61db06c5d6ec78beda55d872fedfd4234cac9e1b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 07:57:56 -0300 Subject: [PATCH 01/29] feat: port price widgets related screens to figma V61 --- Bitkit/Components/Widgets/PriceWidget.swift | 226 +++++++-------- Bitkit/MainNavView.swift | 7 +- Bitkit/Models/PriceWidgetOptions.swift | 4 +- .../Localization/en.lproj/Localizable.strings | 9 + Bitkit/Utilities/WidgetsBackupConverter.swift | 5 +- .../Widgets/PriceWidgetPreviewView.swift | 263 ++++++++++++++++++ Bitkit/Views/Widgets/WidgetEditItemView.swift | 38 ++- Bitkit/Views/Widgets/WidgetEditLogic.swift | 21 +- Bitkit/Views/Widgets/WidgetEditModels.swift | 83 +++--- Bitkit/Views/Widgets/WidgetEditView.swift | 14 +- BitkitWidget/PriceHomeScreenWidget.swift | 220 +++++++-------- changelog.d/next/price-widget-v61.changed.md | 1 + 12 files changed, 563 insertions(+), 328 deletions(-) create mode 100644 Bitkit/Views/Widgets/PriceWidgetPreviewView.swift create mode 100644 changelog.d/next/price-widget-v61.changed.md diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index 8da348dab..a5935567e 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -1,21 +1,14 @@ import Charts import SwiftUI -/// A widget that displays cryptocurrency price information with chart +/// Displays Bitcoin price for the user's selected trading pair and timeframe (Figma v61). struct PriceWidget: View { - /// Configuration options for the widget var options: PriceWidgetOptions = .init() - - /// Flag indicating if the widget is in editing mode var isEditing: Bool = false - - /// Callback to signal when editing should end var onEditingEnd: (() -> Void)? - /// Price view model singleton @StateObject private var viewModel = PriceViewModel.shared - /// Initialize the widget init( options: PriceWidgetOptions = PriceWidgetOptions(), isEditing: Bool = false, @@ -32,91 +25,121 @@ struct PriceWidget: View { isEditing: isEditing, onEditingEnd: onEditingEnd ) { - VStack(spacing: 0) { - if viewModel.isLoading && filteredPriceData.isEmpty { - WidgetContentBuilder.loadingView() - } else if viewModel.error != nil { - WidgetContentBuilder.errorView(t("widgets__price__error")) - } else { - ForEach(filteredPriceData, id: \.name) { priceData in - PriceRow(data: priceData) - .accessibilityIdentifier("PriceWidgetRow-\(priceData.name)") - } - } - - if let firstPair = filteredPriceData.first { - PriceChart( - values: firstPair.pastValues, - isPositive: firstPair.change.isPositive, - period: options.selectedPeriod.rawValue - ) - .frame(height: 96) - .padding(.top, 8) - } - - if options.showSource { - WidgetContentBuilder.sourceRow(source: "Bitfinex.com") - .accessibilityIdentifier("PriceWidgetSource") - } - } + content } - .onAppear { - fetchPriceData() - } - .onChange(of: options.selectedPairs) { - fetchPriceData() - } - .onChange(of: options.selectedPeriod) { - fetchPriceData() + .onAppear { fetchPriceData() } + .onChange(of: options.selectedPairs) { fetchPriceData() } + .onChange(of: options.selectedPeriod) { fetchPriceData() } + } + + @ViewBuilder + private var content: some View { + if viewModel.isLoading && primaryPrice == nil { + WidgetContentBuilder.loadingView() + } else if viewModel.error != nil { + WidgetContentBuilder.errorView(t("widgets__price__error")) + } else if let primary = primaryPrice { + PriceWidgetWideContent(data: primary, period: options.selectedPeriod) } } - private var filteredPriceData: [PriceData] { + /// Single pair (v61). Falls back to first available data if the selection isn't loaded yet. + private var primaryPrice: PriceData? { let currentPeriodData = viewModel.getCurrentData(for: options.selectedPeriod) - let dataByPair = Dictionary(uniqueKeysWithValues: currentPeriodData.map { ($0.name, $0) }) - return options.selectedPairs.compactMap { pair in - dataByPair[pair] + if let preferred = options.selectedPairs.first, + let match = currentPeriodData.first(where: { $0.name == preferred }) + { + return match } + return currentPeriodData.first } - /// Fetch price data from view model private func fetchPriceData() { viewModel.fetchPriceData(pairs: options.selectedPairs, period: options.selectedPeriod) } } -// MARK: - Price Row Component +// MARK: - Wide layout (in-app + carousel page) + +struct PriceWidgetWideContent: View { + let data: PriceData + let period: GraphPeriod + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 16) { + CaptionMText("\(data.name) \(period.rawValue)", textColor: .textSecondary) + .textCase(.uppercase) + .frame(maxWidth: .infinity, alignment: .leading) + + Text(data.change.formatted) + .font(Fonts.bold(size: 22)) + .foregroundColor(data.change.isPositive ? .greenAccent : .redAccent) + .lineLimit(1) + } + + Text(data.price) + .font(Fonts.bold(size: 34)) + .foregroundColor(.textPrimary) + .lineLimit(1) + .minimumScaleFactor(0.7) + .frame(maxWidth: .infinity, alignment: .leading) + } + + PriceChart(values: data.pastValues, isPositive: data.change.isPositive) + .frame(height: 48) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +// MARK: - Compact layout (small carousel preview only) -struct PriceRow: View { +struct PriceWidgetCompactContent: View { let data: PriceData + let period: GraphPeriod var body: some View { - HStack { - BodySSBText(data.name, textColor: .textSecondary) + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 0) { + CaptionMText(data.name, textColor: .textSecondary) + .textCase(.uppercase) + Spacer(minLength: 0) + CaptionMText(period.rawValue, textColor: .textSecondary) + .textCase(.uppercase) + } + + Text(data.price) + .font(Fonts.bold(size: 22)) + .foregroundColor(.textPrimary) + .lineLimit(1) + .minimumScaleFactor(0.7) - Spacer() + Text(data.change.formatted) + .font(Fonts.semiBold(size: 15)) + .foregroundColor(data.change.isPositive ? .greenAccent : .redAccent) + .lineLimit(1) + } - BodySSBText(data.change.formatted, textColor: data.change.isPositive ? .greenAccent : .redAccent) - .padding(.trailing, 8) - BodySSBText(data.price, textColor: .textPrimary) + PriceChart(values: data.pastValues, isPositive: data.change.isPositive) + .frame(height: 64) } - .frame(minHeight: 28) + .padding(16) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) + .background(Color.gray6) + .cornerRadius(16) } } -// MARK: - Price Chart Component +// MARK: - Chart (line-only per Figma v61) struct PriceChart: View { let values: [Double] let isPositive: Bool - let period: String - // Chart styling constants private let lineWidth: CGFloat = 1.3 - private let chartPadding: CGFloat = 4 - private let cornerRadius: CGFloat = 8 - private let gradientOpacityTop: CGFloat = 0.64 - private let gradientOpacityBottom: CGFloat = 0.08 private var normalizedValues: [Double] { guard values.count > 1 else { return values } @@ -127,76 +150,31 @@ struct PriceChart: View { guard range > 0 else { return values.map { _ in 0.5 } } - // Map to 0.15...0.85 range for more generous margins - // This prevents chart content from reaching the very edges where clipping occurs return values.map { value in let normalized = (value - minValue) / range - return 0.15 + (normalized * 0.7) // Maps 0-1 to 0.15-0.85 + return 0.15 + (normalized * 0.7) } } - private var chartColors: (gradient: [Color], line: Color) { - if isPositive { - return ( - gradient: [.greenAccent.opacity(gradientOpacityTop), .greenAccent.opacity(gradientOpacityBottom)], - line: .greenAccent - ) - } else { - return ( - gradient: [.redAccent.opacity(gradientOpacityTop), .redAccent.opacity(gradientOpacityBottom)], - line: .redAccent - ) - } + private var lineColor: Color { + isPositive ? .greenAccent : .redAccent } var body: some View { - ZStack(alignment: .bottomLeading) { - Chart { - ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in - // Area fill with gradient - AreaMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle( - LinearGradient( - colors: chartColors.gradient, - startPoint: .top, - endPoint: .bottom - ) - ) - .interpolationMethod(.catmullRom) - - // Line on top - LineMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle(chartColors.line) - .lineStyle(StrokeStyle(lineWidth: lineWidth)) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - // Y scale domain provides buffer zone beyond data range (0.15...0.85) - // This ensures chart elements (lines, curves) don't get clipped at edges - .chartYScale(domain: 0.1 ... 0.9) // Domain slightly larger than data range for extra buffer - // Apply rounded corners only to bottom - chart content extends to edges for visible clipping - // The internal margins above prevent any actual data from being cut off - .clipShape( - .rect( - topLeadingRadius: 0, - bottomLeadingRadius: cornerRadius, - bottomTrailingRadius: cornerRadius, - topTrailingRadius: 0 + Chart { + ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in + LineMark( + x: .value("Index", index), + y: .value("Price", value) ) - ) - - // Period label - CaptionBText(period, textColor: isPositive ? .green50 : .red50) - .padding(7) + .foregroundStyle(lineColor) + .lineStyle(StrokeStyle(lineWidth: lineWidth)) + .interpolationMethod(.catmullRom) + } } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .chartYScale(domain: 0.1 ... 0.9) } } diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 1eae66d3d..56858982d 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -430,7 +430,12 @@ struct MainNavView: View { // Widgets case .widgetsIntro: WidgetsIntroView() case .widgetsList: WidgetsListView() - case let .widgetDetail(widgetType): WidgetDetailView(id: widgetType) + case let .widgetDetail(widgetType): + if widgetType == .price { + PriceWidgetPreviewView() + } else { + WidgetDetailView(id: widgetType) + } case let .widgetEdit(widgetType): WidgetEditView(id: widgetType) // Settings diff --git a/Bitkit/Models/PriceWidgetOptions.swift b/Bitkit/Models/PriceWidgetOptions.swift index 94310d05f..987d838f7 100644 --- a/Bitkit/Models/PriceWidgetOptions.swift +++ b/Bitkit/Models/PriceWidgetOptions.swift @@ -1,8 +1,10 @@ import Foundation /// Options for configuring the in-app and home-screen price widgets (shared via App Group for the extension). +/// +/// `selectedPairs` is kept as an array for storage backwards-compatibility with v60. The v61 UI is +/// single-select and only ever reads/writes `[firstPair]`. struct PriceWidgetOptions: Codable, Equatable { var selectedPairs: [String] = ["BTC/USD"] var selectedPeriod: GraphPeriod = .oneDay - var showSource: Bool = false } diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 766d3dfda..1dd4673ca 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1387,6 +1387,15 @@ "widgets__price__name" = "Bitcoin Price"; "widgets__price__description" = "Check the latest Bitcoin exchange rates for a variety of fiat currencies."; "widgets__price__error" = "Couldn\'t get price data"; +"widgets__price__currency" = "Currency"; +"widgets__price__timeframe" = "Timeframe"; +"widgets__price__period_day" = "Day"; +"widgets__price__period_week" = "Week"; +"widgets__price__period_month" = "Month"; +"widgets__price__period_year" = "Year"; +"widgets__price__size_small" = "Small"; +"widgets__price__size_wide" = "Wide"; +"widgets__price__widget_settings" = "Widget Settings"; "widgets__news__name" = "Bitcoin Headlines"; "widgets__news__description" = "Read the latest & greatest Bitcoin headlines from various news sites."; "widgets__news__error" = "Couldn\'t get the latest news"; diff --git a/Bitkit/Utilities/WidgetsBackupConverter.swift b/Bitkit/Utilities/WidgetsBackupConverter.swift index 889a76095..f1ba83a71 100644 --- a/Bitkit/Utilities/WidgetsBackupConverter.swift +++ b/Bitkit/Utilities/WidgetsBackupConverter.swift @@ -66,7 +66,6 @@ enum WidgetsBackupConverter { pricePreferences = [ "enabledPairs": androidPairs.isEmpty ? ["BTC_USD"] : androidPairs, "period": androidPeriod, - "showSource": options.showSource, ] } case .calculator, .suggestions: @@ -179,8 +178,7 @@ enum WidgetsBackupConverter { let period = convertAndroidPeriodToIos(prefs["period"] as? String) let iosOptions = PriceWidgetOptions( selectedPairs: selectedPairs, - selectedPeriod: period, - showSource: prefs["showSource"] as? Bool ?? false + selectedPeriod: period ) optionsData = try? JSONEncoder().encode(iosOptions) } @@ -243,7 +241,6 @@ enum WidgetsBackupConverter { return [ "enabledPairs": androidPairs.isEmpty ? ["BTC_USD"] : androidPairs, "period": androidPeriod, - "showSource": defaults.showSource, ] } diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift new file mode 100644 index 000000000..4d4eeaf81 --- /dev/null +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -0,0 +1,263 @@ +import SwiftUI + +/// Preview screen for the Bitcoin Price widget (Figma v61). +/// +/// Replaces the generic `WidgetDetailView` for `.price` only — the other widgets continue to use +/// `WidgetDetailView`. Layout differences from the generic preview: centered top-bar title, +/// description, "Widget Settings" cell, and a Compact ↔ Wide carousel. +struct PriceWidgetPreviewView: View { + @EnvironmentObject private var navigation: NavigationViewModel + @EnvironmentObject private var widgets: WidgetsViewModel + + @StateObject private var viewModel = PriceViewModel.shared + + @State private var carouselPage: Int = 0 + @State private var showDeleteAlert = false + + private let widgetType: WidgetType = .price + + private var widgetName: String { + t("widgets__price__name") + } + + private var widgetDescription: String { + t("widgets__price__description") + } + + private var isWidgetSaved: Bool { + widgets.isWidgetSaved(widgetType) + } + + private var hasCustomOptions: Bool { + widgets.hasCustomOptions(for: widgetType) + } + + private var currentOptions: PriceWidgetOptions { + widgets.getOptions(for: widgetType, as: PriceWidgetOptions.self) + } + + private var primaryPrice: PriceData? { + let options = currentOptions + let currentPeriodData = viewModel.getCurrentData(for: options.selectedPeriod) + if let preferred = options.selectedPairs.first, + let match = currentPeriodData.first(where: { $0.name == preferred }) + { + return match + } + return currentPeriodData.first + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavigationBar(title: widgetName) + .padding(.bottom, 16) + + BodyMText(widgetDescription, textColor: .textSecondary) + .padding(.bottom, 16) + + Divider().background(Color.white.opacity(0.1)) + + widgetSettingsRow + + Divider().background(Color.white.opacity(0.1)) + + Spacer(minLength: 0) + + carousel + + sizeLabel + .padding(.top, 8) + + pageIndicator + .padding(.top, 8) + + Spacer(minLength: 0) + + buttonsRow + .padding(.top, 16) + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .onAppear { + let options = currentOptions + viewModel.fetchPriceData(pairs: options.selectedPairs, period: options.selectedPeriod) + } + .alert( + t("widgets__delete__title"), + isPresented: $showDeleteAlert, + actions: { + Button(t("common__cancel"), role: .cancel) { showDeleteAlert = false } + Button(t("common__delete_yes"), role: .destructive) { onDelete() } + }, + message: { + Text(t("widgets__delete__description", variables: ["name": widgetName])) + } + ) + } + + // MARK: - Widget Settings cell + + private var widgetSettingsRow: some View { + Button(action: { navigation.navigate(.widgetEdit(widgetType)) }) { + HStack(alignment: .center, spacing: 0) { + BodyMText(t("widgets__price__widget_settings"), textColor: .textPrimary) + + Spacer() + + BodyMText( + hasCustomOptions + ? t("widgets__widget__edit_custom") + : t("widgets__widget__edit_default"), + textColor: .textSecondary + ) + + Image("chevron") + .resizable() + .foregroundColor(.textSecondary) + .frame(width: 24, height: 24) + .padding(.leading, 5) + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + } + .buttonStyle(PlainButtonStyle()) + .padding(.vertical, 14) + .accessibilityIdentifier("WidgetEdit") + } + + // MARK: - Carousel + + private var carousel: some View { + TabView(selection: $carouselPage) { + compactPage + .tag(0) + + widePage + .tag(1) + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: 240) + } + + private var compactPage: some View { + HStack { + Spacer() + Group { + if let data = primaryPrice { + PriceWidgetCompactContent(data: data, period: currentOptions.selectedPeriod) + } else { + placeholderCompact + } + } + .frame(width: 163, height: 192) + Spacer() + } + } + + private var widePage: some View { + HStack { + Spacer() + Group { + if let data = primaryPrice { + PriceWidgetWideContent(data: data, period: currentOptions.selectedPeriod) + .padding(16) + .background(Color.gray6) + .cornerRadius(16) + } else { + placeholderWide + } + } + .frame(maxWidth: .infinity) + Spacer() + } + } + + private var placeholderCompact: some View { + Color.gray6 + .cornerRadius(16) + .overlay(ProgressView()) + } + + private var placeholderWide: some View { + Color.gray6 + .cornerRadius(16) + .frame(height: 130) + .overlay(ProgressView()) + } + + // MARK: - Size label & page indicator + + private var sizeLabel: some View { + HStack { + Spacer() + CaptionMText( + carouselPage == 0 + ? t("widgets__price__size_small") + : t("widgets__price__size_wide"), + textColor: .textSecondary + ) + .textCase(.uppercase) + Spacer() + } + } + + private var pageIndicator: some View { + HStack(spacing: 8) { + Spacer() + ForEach(0 ..< 2, id: \.self) { index in + Circle() + .fill(carouselPage == index ? Color.brandAccent : Color.white.opacity(0.32)) + .frame(width: 8, height: 8) + } + Spacer() + } + } + + // MARK: - Buttons + + private var buttonsRow: some View { + HStack(spacing: 16) { + if isWidgetSaved { + CustomButton( + title: t("common__delete"), + variant: .secondary, + size: .large, + shouldExpand: true + ) { + showDeleteAlert = true + } + .accessibilityIdentifier("WidgetDelete") + } + + CustomButton( + title: t("common__save"), + variant: .primary, + size: .large, + shouldExpand: true, + action: onSave + ) + .accessibilityIdentifier("WidgetSave") + } + } + + // MARK: - Actions + + private func onSave() { + widgets.saveWidget(widgetType) + navigation.reset() + } + + private func onDelete() { + widgets.deleteWidget(widgetType) + navigation.reset() + } +} + +#Preview { + NavigationStack { + PriceWidgetPreviewView() + .environmentObject(NavigationViewModel()) + .environmentObject(WidgetsViewModel()) + } + .preferredColorScheme(.dark) +} diff --git a/Bitkit/Views/Widgets/WidgetEditItemView.swift b/Bitkit/Views/Widgets/WidgetEditItemView.swift index 423477dbe..572044c4b 100644 --- a/Bitkit/Views/Widgets/WidgetEditItemView.swift +++ b/Bitkit/Views/Widgets/WidgetEditItemView.swift @@ -5,7 +5,24 @@ struct WidgetEditItemView: View { let onToggle: () -> Void var body: some View { - let content = VStack(spacing: 0) { + switch item.type { + case .sectionHeader: + item.titleView + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 8) + .padding(.bottom, 16) + case .staticItem: + row + case .toggleItem: + Button(action: onToggle) { + row + } + .buttonStyle(PlainButtonStyle()) + } + } + + private var row: some View { + VStack(spacing: 0) { HStack(spacing: 16) { item.titleView .frame(maxWidth: .infinity, alignment: .leading) @@ -17,24 +34,17 @@ struct WidgetEditItemView: View { .frame(maxWidth: .infinity, alignment: .trailing) } - Image("check-mark") - .resizable() - .foregroundColor(item.isChecked ? .brandAccent : .gray3) - .frame(width: 32, height: 32) + if item.type != .staticItem { + Image("check-mark") + .resizable() + .foregroundColor(item.isChecked ? .brandAccent : .gray3) + .frame(width: 32, height: 32) + } } .padding(.vertical, 16) .contentShape(Rectangle()) Divider() } - - if item.type == .staticItem { - content - } else { - Button(action: onToggle) { - content - } - .buttonStyle(PlainButtonStyle()) - } } } diff --git a/Bitkit/Views/Widgets/WidgetEditLogic.swift b/Bitkit/Views/Widgets/WidgetEditLogic.swift index 3820ab6f0..a991264c1 100644 --- a/Bitkit/Views/Widgets/WidgetEditLogic.swift +++ b/Bitkit/Views/Widgets/WidgetEditLogic.swift @@ -138,14 +138,8 @@ class WidgetEditLogic: ObservableObject { } case .price: switch item.key { - case "BTC/USD": - toggleTradingPair("BTC/USD") - case "BTC/EUR": - toggleTradingPair("BTC/EUR") - case "BTC/GBP": - toggleTradingPair("BTC/GBP") - case "BTC/JPY": - toggleTradingPair("BTC/JPY") + case "BTC/USD", "BTC/EUR", "BTC/GBP", "BTC/JPY": + selectTradingPair(item.key) case "1D": priceOptions.selectedPeriod = .oneDay case "1W": @@ -154,8 +148,6 @@ class WidgetEditLogic: ObservableObject { priceOptions.selectedPeriod = .oneMonth case "1Y": priceOptions.selectedPeriod = .oneYear - case "showSource": - priceOptions.showSource.toggle() default: break } @@ -165,12 +157,9 @@ class WidgetEditLogic: ObservableObject { onStateChange?() } - private func toggleTradingPair(_ pairName: String) { - if priceOptions.selectedPairs.contains(pairName) { - priceOptions.selectedPairs.removeAll { $0 == pairName } - } else { - priceOptions.selectedPairs.append(pairName) - } + /// Single-select per Figma v61 — replaces the array with a single pair. + private func selectTradingPair(_ pairName: String) { + priceOptions.selectedPairs = [pairName] } func loadCurrentOptions() { diff --git a/Bitkit/Views/Widgets/WidgetEditModels.swift b/Bitkit/Views/Widgets/WidgetEditModels.swift index 575f500b6..f4f75989e 100644 --- a/Bitkit/Views/Widgets/WidgetEditModels.swift +++ b/Bitkit/Views/Widgets/WidgetEditModels.swift @@ -1,10 +1,27 @@ import SwiftUI +// MARK: - GraphPeriod display + +extension GraphPeriod { + /// Full-word label shown in the Price edit screen (Day / Week / Month / Year). + /// The widget itself uses `rawValue` ("1D"/...) per Figma v61. + var editScreenLabel: String { + switch self { + case .oneDay: return t("widgets__price__period_day") + case .oneWeek: return t("widgets__price__period_week") + case .oneMonth: return t("widgets__price__period_month") + case .oneYear: return t("widgets__price__period_year") + } + } +} + // MARK: - Widget Edit Item Models enum WidgetItemType { case toggleItem case staticItem + /// Non-tappable section header (uppercase caption above a group of items). + case sectionHeader } struct WidgetEditItem { @@ -357,68 +374,62 @@ enum WidgetEditItemFactory { } @MainActor - static func getPriceItems(priceOptions: PriceWidgetOptions, priceDataByPeriod: [GraphPeriod: [PriceData]] = [:]) -> [WidgetEditItem] { + static func getPriceItems(priceOptions: PriceWidgetOptions, priceDataByPeriod _: [GraphPeriod: [PriceData]] = [:]) -> [WidgetEditItem] { var items: [WidgetEditItem] = [] - // Trading pair options with live or fallback prices - let fallbackPrices = ["$ 43,250", "€ 39,850", "£ 34,120", "¥ 6,245,000"] - - // Use current period data for trading pair prices - let currentPeriodData = priceDataByPeriod[priceOptions.selectedPeriod] ?? [] - - for (index, pair) in tradingPairNames.enumerated() { - // Try to find live data for this pair - let livePrice = currentPeriodData.first { $0.name == pair }?.price ?? fallbackPrices[index] + // CURRENCY section (single-select) + items.append(sectionHeaderItem(key: "currency_header", title: t("widgets__price__currency"))) + let selectedPair = priceOptions.selectedPairs.first + for pair in tradingPairNames { + let isSelected = selectedPair == pair items.append( WidgetEditItem( key: pair, type: .toggleItem, - title: pair, - value: livePrice, - isChecked: priceOptions.selectedPairs.contains(pair) + titleView: AnyView( + BodySSBText(pair, textColor: isSelected ? .textPrimary : .textSecondary) + ), + valueView: nil, + isChecked: isSelected ) ) } - // Period selection (radio group) with charts - let periods: [GraphPeriod] = [.oneDay, .oneWeek, .oneMonth, .oneYear] - - for period in periods { - // Get data for this specific period - let periodData = priceDataByPeriod[period] ?? [] - let firstPairData = periodData.first + // TIMEFRAME section (single-select). Full-word labels per Figma v61. + items.append(sectionHeaderItem(key: "timeframe_header", title: t("widgets__price__timeframe"))) + for period in GraphPeriod.allCases { + let isSelected = priceOptions.selectedPeriod == period items.append( WidgetEditItem( key: period.rawValue, type: .toggleItem, titleView: AnyView( - PriceChart( - values: firstPairData?.pastValues ?? [], - isPositive: firstPairData?.change.isPositive ?? true, - period: period.rawValue - ) + BodySSBText(period.editScreenLabel, textColor: isSelected ? .textPrimary : .textSecondary) ), valueView: nil, - isChecked: priceOptions.selectedPeriod == period + isChecked: isSelected ) ) } - items.append( - WidgetEditItem( - key: "showSource", - type: .toggleItem, - title: t("widgets__widget__source"), - valueView: AnyView(BodySSBText("Bitfinex.com", textColor: .textSecondary)), - isChecked: priceOptions.showSource - ) - ) - return items } + private static func sectionHeaderItem(key: String, title: String) -> WidgetEditItem { + WidgetEditItem( + key: key, + type: .sectionHeader, + titleView: AnyView( + CaptionMText(title, textColor: .textSecondary) + .textCase(.uppercase) + ), + valueView: nil, + isChecked: false + ) + } + @MainActor static func getWeatherItems( weatherViewModel: WeatherViewModel, diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index 8cfc81174..d96a89d17 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -57,14 +57,16 @@ struct WidgetEditView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: t("widgets__widget__edit")) + NavigationBar(title: id == .price ? widget.name : t("widgets__widget__edit")) .padding(.bottom, 16) - BodyMText( - t("widgets__widget__edit_description", variables: ["name": widget.name]), - textColor: .textSecondary - ) - .padding(.bottom, 16) + if id != .price { + BodyMText( + t("widgets__widget__edit_description", variables: ["name": widget.name]), + textColor: .textSecondary + ) + .padding(.bottom, 16) + } ScrollView(showsIndicators: false) { LazyVStack(spacing: 0) { diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index e1a44af7e..ae3c22e1b 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -86,123 +86,126 @@ struct PriceHomeScreenWidgetEntryView: View { var entry: PriceWidgetProvider.Entry var body: some View { - VStack(alignment: .leading, spacing: 8) { - content - if entry.options.showSource, !entry.prices.isEmpty { - HStack { - Spacer() - CaptionBText("Bitfinex.com", textColor: secondaryTextColor) - } - } - } - .containerBackground(for: .widget) { backgroundView } + content + .containerBackground(for: .widget) { backgroundView } } @ViewBuilder private var content: some View { if entry.showsError { errorView - } else if entry.prices.isEmpty { - ProgressView() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) - } else { + } else if let primary = primaryPrice { switch widgetFamily { case .systemSmall: - smallContent + compactLayout(data: primary) default: - rowsAndChart + wideLayout(data: primary) } + } else { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + } + } + + /// Always render the first selected pair (v61 is single-pair). + private var primaryPrice: PriceData? { + let preferred = entry.options.selectedPairs.first + if let preferred, let match = entry.prices.first(where: { $0.name == preferred }) { + return match } + return entry.prices.first } - // MARK: - Variants + // MARK: - Compact (small widget — 163×192) - private var smallContent: some View { - let primary = entry.prices.first - return VStack(alignment: .leading, spacing: 4) { - BodySSBText(primary?.name ?? "BTC/USD", textColor: secondaryTextColor) - .lineLimit(1) + private func compactLayout(data: PriceData) -> some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 0) { + captionUpText(data.name) + Spacer(minLength: 0) + captionUpText(entry.options.selectedPeriod.rawValue) + } - Text(primary?.price ?? "—") - .font(Fonts.bold(size: 22)) - .foregroundColor(valueTextColor) - .lineLimit(1) - .minimumScaleFactor(0.7) - .widgetAccentable() + priceText(data.price, size: 22, lineHeight: 26) - if let change = primary?.change { - BodySSBText(change.formatted, textColor: changeColor(isPositive: change.isPositive)) + Text(data.change.formatted) + .font(Fonts.semiBold(size: 15)) + .foregroundColor(changeColor(isPositive: data.change.isPositive)) .lineLimit(1) .widgetAccentable() } - Spacer(minLength: 0) + chart(values: data.pastValues, isPositive: data.change.isPositive, height: 64) } - .frame(maxWidth: .infinity, alignment: .leading) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - private var rowsAndChart: some View { - VStack(spacing: 0) { - ForEach(visibleRows, id: \.name) { data in - priceRow(data: data) - } + // MARK: - Wide (medium / large widget) - if let firstPair = entry.prices.first { - PriceWidgetChart( - values: firstPair.pastValues, - isPositive: firstPair.change.isPositive, - period: entry.options.selectedPeriod.rawValue, - renderingMode: widgetRenderingMode - ) - .frame(height: chartHeight) - .padding(.top, 8) + private func wideLayout(data: PriceData) -> some View { + VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .center, spacing: 16) { + captionUpText("\(data.name) \(entry.options.selectedPeriod.rawValue)") + .frame(maxWidth: .infinity, alignment: .leading) + + Text(data.change.formatted) + .font(Fonts.bold(size: 22)) + .foregroundColor(changeColor(isPositive: data.change.isPositive)) + .lineLimit(1) + .widgetAccentable() + } + + priceText(data.price, size: 34, lineHeight: 34) } - } - } - private var visibleRows: [PriceData] { - switch widgetFamily { - case .systemSmall: Array(entry.prices.prefix(1)) - case .systemMedium: Array(entry.prices.prefix(2)) - case .systemLarge, .systemExtraLarge: Array(entry.prices.prefix(4)) - default: Array(entry.prices.prefix(1)) + chart(values: data.pastValues, isPositive: data.change.isPositive, height: chartHeight) } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } private var chartHeight: CGFloat { switch widgetFamily { - case .systemMedium: 64 - case .systemLarge, .systemExtraLarge: 120 - default: 96 + case .systemLarge, .systemExtraLarge: return 120 + default: return 48 } } - private var errorView: some View { - Text("Couldn’t load price.") - .font(Fonts.medium(size: 14)) + // MARK: - Sub-views + + private func captionUpText(_ text: String) -> Text { + Text(text) + .font(Fonts.medium(size: 13)) + .tracking(1) .foregroundColor(secondaryTextColor) - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - // MARK: - Row - - private func priceRow(data: PriceData) -> some View { - HStack(spacing: 0) { - BodySSBText(data.name, textColor: secondaryTextColor) - .frame(maxWidth: .infinity, alignment: .leading) - .lineLimit(1) + private func priceText(_ value: String, size: CGFloat, lineHeight: CGFloat) -> some View { + Text(value) + .font(Fonts.bold(size: size)) + .foregroundColor(valueTextColor) + .lineLimit(1) + .minimumScaleFactor(0.7) + .widgetAccentable() + } - BodySSBText(data.change.formatted, textColor: changeColor(isPositive: data.change.isPositive)) - .padding(.trailing, 8) - .lineLimit(1) - .widgetAccentable() + private func chart(values: [Double], isPositive: Bool, height: CGFloat) -> some View { + PriceWidgetChart( + values: values, + isPositive: isPositive, + renderingMode: widgetRenderingMode + ) + .frame(height: height) + .widgetAccentable() + } - BodySSBText(data.price, textColor: valueTextColor) - .lineLimit(1) - .minimumScaleFactor(0.75) - .widgetAccentable() - } - .frame(minHeight: 24) + private var errorView: some View { + // Hardcoded — widget extension target does not bundle the app's localization helpers. + Text("Couldn’t load price.") + .font(Fonts.medium(size: 14)) + .foregroundColor(secondaryTextColor) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } // MARK: - Colors @@ -225,12 +228,11 @@ struct PriceHomeScreenWidgetEntryView: View { } } -// MARK: - Chart +// MARK: - Chart (line-only per Figma v61) private struct PriceWidgetChart: View { let values: [Double] let isPositive: Bool - let period: String let renderingMode: WidgetRenderingMode private var normalizedValues: [Double] { @@ -247,55 +249,21 @@ private struct PriceWidgetChart: View { return isPositive ? .greenAccent : .redAccent } - private var gradientColors: [Color] { - guard renderingMode == .fullColor else { return [.primary.opacity(0.3), .clear] } - let base: Color = isPositive ? .greenAccent : .redAccent - return [base.opacity(0.64), base.opacity(0.08)] - } - - private var labelColor: Color { - guard renderingMode == .fullColor else { return .secondary } - return isPositive ? .green50 : .red50 - } - var body: some View { - ZStack(alignment: .bottomLeading) { - Chart { - ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in - AreaMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle( - LinearGradient(colors: gradientColors, startPoint: .top, endPoint: .bottom) - ) - .interpolationMethod(.catmullRom) - - LineMark( - x: .value("Index", index), - y: .value("Price", value) - ) - .foregroundStyle(lineColor) - .lineStyle(StrokeStyle(lineWidth: 1.3)) - .interpolationMethod(.catmullRom) - } - } - .chartXAxis(.hidden) - .chartYAxis(.hidden) - .chartYScale(domain: 0.1 ... 0.9) - .clipShape( - .rect( - topLeadingRadius: 0, - bottomLeadingRadius: 8, - bottomTrailingRadius: 8, - topTrailingRadius: 0 + Chart { + ForEach(Array(normalizedValues.enumerated()), id: \.offset) { index, value in + LineMark( + x: .value("Index", index), + y: .value("Price", value) ) - ) - .widgetAccentable() - - CaptionBText(period, textColor: labelColor) - .padding(7) + .foregroundStyle(lineColor) + .lineStyle(StrokeStyle(lineWidth: 1.3)) + .interpolationMethod(.catmullRom) + } } + .chartXAxis(.hidden) + .chartYAxis(.hidden) + .chartYScale(domain: 0.1 ... 0.9) } } diff --git a/changelog.d/next/price-widget-v61.changed.md b/changelog.d/next/price-widget-v61.changed.md new file mode 100644 index 000000000..d6bc025ee --- /dev/null +++ b/changelog.d/next/price-widget-v61.changed.md @@ -0,0 +1 @@ +Redesign the Bitcoin Price widget (in-app and home screen) to match Figma v61: single-currency selection, dedicated wide/compact layouts, line-only chart, and an updated edit and preview flow. From 28a550f2e4e30060eaed4a9f22b51a82cf46ec88 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 08:07:53 -0300 Subject: [PATCH 02/29] fix: spacing and alignment --- .../Localization/en.lproj/Localizable.strings | 1 + .../Widgets/PriceWidgetPreviewView.swift | 55 ++++++++++--------- Bitkit/Views/Widgets/WidgetEditItemView.swift | 3 +- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 1dd4673ca..968bc586c 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1396,6 +1396,7 @@ "widgets__price__size_small" = "Small"; "widgets__price__size_wide" = "Wide"; "widgets__price__widget_settings" = "Widget Settings"; +"widgets__widget__save_widget" = "Save Widget"; "widgets__news__name" = "Bitcoin Headlines"; "widgets__news__description" = "Read the latest & greatest Bitcoin headlines from various news sites."; "widgets__news__error" = "Couldn\'t get the latest news"; diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 4d4eeaf81..48652a637 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -48,33 +48,36 @@ struct PriceWidgetPreviewView: View { } var body: some View { - VStack(alignment: .leading, spacing: 0) { + VStack(alignment: .leading, spacing: 16) { NavigationBar(title: widgetName) - .padding(.bottom, 16) - BodyMText(widgetDescription, textColor: .textSecondary) - .padding(.bottom, 16) + // Content (description + Widget Settings cell with surrounding dividers) + VStack(alignment: .leading, spacing: 0) { + BodyMText(widgetDescription, textColor: .textSecondary) + .padding(.bottom, 16) - Divider().background(Color.white.opacity(0.1)) + Divider().background(Color.white.opacity(0.1)) - widgetSettingsRow + widgetSettingsRow - Divider().background(Color.white.opacity(0.1)) + Divider().background(Color.white.opacity(0.1)) + } - Spacer(minLength: 0) + // Carousel section (centered widget + size label + page indicator) + VStack(spacing: 16) { + Spacer(minLength: 0) - carousel + carousel - sizeLabel - .padding(.top, 8) + Spacer(minLength: 0) - pageIndicator - .padding(.top, 8) + sizeLabel - Spacer(minLength: 0) + pageIndicator + } + .frame(maxWidth: .infinity, maxHeight: .infinity) buttonsRow - .padding(.top, 16) } .navigationBarHidden(true) .padding(.horizontal, 16) @@ -117,11 +120,10 @@ struct PriceWidgetPreviewView: View { .frame(width: 24, height: 24) .padding(.leading, 5) } - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, minHeight: 51) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) - .padding(.vertical, 14) .accessibilityIdentifier("WidgetEdit") } @@ -136,12 +138,12 @@ struct PriceWidgetPreviewView: View { .tag(1) } .tabViewStyle(.page(indexDisplayMode: .never)) - .frame(height: 240) + .frame(height: 320) } private var compactPage: some View { - HStack { - Spacer() + VStack { + Spacer(minLength: 0) Group { if let data = primaryPrice { PriceWidgetCompactContent(data: data, period: currentOptions.selectedPeriod) @@ -150,13 +152,14 @@ struct PriceWidgetPreviewView: View { } } .frame(width: 163, height: 192) - Spacer() + Spacer(minLength: 0) } + .frame(maxWidth: .infinity) } private var widePage: some View { - HStack { - Spacer() + VStack { + Spacer(minLength: 0) Group { if let data = primaryPrice { PriceWidgetWideContent(data: data, period: currentOptions.selectedPeriod) @@ -168,7 +171,7 @@ struct PriceWidgetPreviewView: View { } } .frame(maxWidth: .infinity) - Spacer() + Spacer(minLength: 0) } } @@ -181,7 +184,7 @@ struct PriceWidgetPreviewView: View { private var placeholderWide: some View { Color.gray6 .cornerRadius(16) - .frame(height: 130) + .frame(height: 152) .overlay(ProgressView()) } @@ -230,7 +233,7 @@ struct PriceWidgetPreviewView: View { } CustomButton( - title: t("common__save"), + title: t("widgets__widget__save_widget"), variant: .primary, size: .large, shouldExpand: true, diff --git a/Bitkit/Views/Widgets/WidgetEditItemView.swift b/Bitkit/Views/Widgets/WidgetEditItemView.swift index 572044c4b..4b4b8bbb7 100644 --- a/Bitkit/Views/Widgets/WidgetEditItemView.swift +++ b/Bitkit/Views/Widgets/WidgetEditItemView.swift @@ -9,8 +9,7 @@ struct WidgetEditItemView: View { case .sectionHeader: item.titleView .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 8) - .padding(.bottom, 16) + .padding(.vertical, 16) case .staticItem: row case .toggleItem: From 10be29edab7379dd4ddabf4f677c437e4e138e7e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 08:09:17 -0300 Subject: [PATCH 03/29] feat: hide menu button from nabigation bar --- Bitkit/Views/Widgets/PriceWidgetPreviewView.swift | 2 +- Bitkit/Views/Widgets/WidgetEditView.swift | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 48652a637..b8a341c60 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -49,7 +49,7 @@ struct PriceWidgetPreviewView: View { var body: some View { VStack(alignment: .leading, spacing: 16) { - NavigationBar(title: widgetName) + NavigationBar(title: widgetName, showMenuButton: false) // Content (description + Widget Settings cell with surrounding dividers) VStack(alignment: .leading, spacing: 0) { diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index d96a89d17..e119ee310 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -57,8 +57,11 @@ struct WidgetEditView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - NavigationBar(title: id == .price ? widget.name : t("widgets__widget__edit")) - .padding(.bottom, 16) + NavigationBar( + title: id == .price ? widget.name : t("widgets__widget__edit"), + showMenuButton: id != .price + ) + .padding(.bottom, 16) if id != .price { BodyMText( From 90680fed51b735002546101d84358a311ca6b467 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 08:15:20 -0300 Subject: [PATCH 04/29] fix: padding --- Bitkit/Views/Widgets/WidgetEditModels.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bitkit/Views/Widgets/WidgetEditModels.swift b/Bitkit/Views/Widgets/WidgetEditModels.swift index f4f75989e..fa1db48ca 100644 --- a/Bitkit/Views/Widgets/WidgetEditModels.swift +++ b/Bitkit/Views/Widgets/WidgetEditModels.swift @@ -396,8 +396,7 @@ enum WidgetEditItemFactory { ) } - // TIMEFRAME section (single-select). Full-word labels per Figma v61. - items.append(sectionHeaderItem(key: "timeframe_header", title: t("widgets__price__timeframe"))) + items.append(sectionHeaderItem(key: "timeframe_header", title: t("widgets__price__timeframe"), topInset: 16)) for period in GraphPeriod.allCases { let isSelected = priceOptions.selectedPeriod == period @@ -417,13 +416,14 @@ enum WidgetEditItemFactory { return items } - private static func sectionHeaderItem(key: String, title: String) -> WidgetEditItem { + private static func sectionHeaderItem(key: String, title: String, topInset: CGFloat = 0) -> WidgetEditItem { WidgetEditItem( key: key, type: .sectionHeader, titleView: AnyView( CaptionMText(title, textColor: .textSecondary) .textCase(.uppercase) + .padding(.top, topInset) ), valueView: nil, isChecked: false From 4dfd9a80d0cb134463b0f39afffe19313adc907a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 08:24:56 -0300 Subject: [PATCH 05/29] fix: remove systemLarge widget option --- BitkitWidget/PriceHomeScreenWidget.swift | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index ae3c22e1b..ed6fd31d4 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -160,18 +160,11 @@ struct PriceHomeScreenWidgetEntryView: View { priceText(data.price, size: 34, lineHeight: 34) } - chart(values: data.pastValues, isPositive: data.change.isPositive, height: chartHeight) + chart(values: data.pastValues, isPositive: data.change.isPositive, height: 48) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - private var chartHeight: CGFloat { - switch widgetFamily { - case .systemLarge, .systemExtraLarge: return 120 - default: return 48 - } - } - // MARK: - Sub-views private func captionUpText(_ text: String) -> Text { @@ -279,6 +272,6 @@ struct BitkitPriceWidget: Widget { } .configurationDisplayName("Bitcoin Price") .description("Latest Bitcoin price and chart, mirroring the in-app price widget.") - .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + .supportedFamilies([.systemSmall, .systemMedium]) } } From 15dc374167acb4e390272a0d2d9af67de73b4354 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 08:47:25 -0300 Subject: [PATCH 06/29] fix: collect results in input order instead of completion order --- BitkitWidget/PriceWidgetService.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/BitkitWidget/PriceWidgetService.swift b/BitkitWidget/PriceWidgetService.swift index 0574bdf22..c07ba499f 100644 --- a/BitkitWidget/PriceWidgetService.swift +++ b/BitkitWidget/PriceWidgetService.swift @@ -22,16 +22,19 @@ enum PriceWidgetService { // MARK: - Fresh Fetch static func fetchFreshPrices(pairs: [String], period: GraphPeriod) async throws -> [PriceData] { - let results = await withTaskGroup(of: PriceData?.self) { group -> [PriceData] in - for pair in pairs { - group.addTask { try? await fetchPair(pairName: pair, period: period) } + let results = await withTaskGroup(of: (Int, PriceData?).self) { group -> [PriceData] in + for (index, pair) in pairs.enumerated() { + group.addTask { + let data = try? await fetchPair(pairName: pair, period: period) + return (index, data) + } } - var collected: [PriceData] = [] - for await result in group { - if let result { collected.append(result) } + var collected: [(Int, PriceData)] = [] + for await (index, result) in group { + if let result { collected.append((index, result)) } } - return collected + return collected.sorted { $0.0 < $1.0 }.map(\.1) } guard !results.isEmpty else { throw FetchError.noPriceDataAvailable } From 600b422f1ffa05768bf76ce7b86bec28c7011d0a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:09:46 -0300 Subject: [PATCH 07/29] fix: pr comments --- changelog.d/next/{price-widget-v61.changed.md => 542.changed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{price-widget-v61.changed.md => 542.changed.md} (100%) diff --git a/changelog.d/next/price-widget-v61.changed.md b/changelog.d/next/542.changed.md similarity index 100% rename from changelog.d/next/price-widget-v61.changed.md rename to changelog.d/next/542.changed.md From 027da4129d5622213361d5ebba84a2b433eafd0a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:09:51 -0300 Subject: [PATCH 08/29] fix: pr comments --- Bitkit/Components/Widgets/PriceWidget.swift | 18 ++++++++++-------- .../Views/Widgets/PriceWidgetPreviewView.swift | 2 +- Bitkit/Views/Widgets/WidgetEditItemView.swift | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index a5935567e..c192f4e9c 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -73,10 +73,11 @@ struct PriceWidgetWideContent: View { .textCase(.uppercase) .frame(maxWidth: .infinity, alignment: .leading) - Text(data.change.formatted) - .font(Fonts.bold(size: 22)) - .foregroundColor(data.change.isPositive ? .greenAccent : .redAccent) - .lineLimit(1) + TitleText( + data.change.formatted, + textColor: data.change.isPositive ? .greenAccent : .redAccent + ) + .lineLimit(1) } Text(data.price) @@ -117,10 +118,11 @@ struct PriceWidgetCompactContent: View { .lineLimit(1) .minimumScaleFactor(0.7) - Text(data.change.formatted) - .font(Fonts.semiBold(size: 15)) - .foregroundColor(data.change.isPositive ? .greenAccent : .redAccent) - .lineLimit(1) + BodySSBText( + data.change.formatted, + textColor: data.change.isPositive ? .greenAccent : .redAccent + ) + .lineLimit(1) } PriceChart(values: data.pastValues, isPositive: data.change.isPositive) diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index b8a341c60..aa082f2e1 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -81,7 +81,7 @@ struct PriceWidgetPreviewView: View { } .navigationBarHidden(true) .padding(.horizontal, 16) - .onAppear { + .task { let options = currentOptions viewModel.fetchPriceData(pairs: options.selectedPairs, period: options.selectedPeriod) } diff --git a/Bitkit/Views/Widgets/WidgetEditItemView.swift b/Bitkit/Views/Widgets/WidgetEditItemView.swift index 4b4b8bbb7..c21c8ad01 100644 --- a/Bitkit/Views/Widgets/WidgetEditItemView.swift +++ b/Bitkit/Views/Widgets/WidgetEditItemView.swift @@ -33,10 +33,10 @@ struct WidgetEditItemView: View { .frame(maxWidth: .infinity, alignment: .trailing) } - if item.type != .staticItem { + if item.type != .staticItem, item.isChecked { Image("check-mark") .resizable() - .foregroundColor(item.isChecked ? .brandAccent : .gray3) + .foregroundColor(.brandAccent) .frame(width: 32, height: 32) } } From a17212970fa7771b1a63909a2290231a58ee032b Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:13:38 -0300 Subject: [PATCH 09/29] refactor: simplify doc --- Bitkit/Components/Widgets/PriceWidget.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index c192f4e9c..33e9dd547 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -1,7 +1,7 @@ import Charts import SwiftUI -/// Displays Bitcoin price for the user's selected trading pair and timeframe (Figma v61). +/// Displays Bitcoin price for the user's selected trading pair and timeframe. struct PriceWidget: View { var options: PriceWidgetOptions = .init() var isEditing: Bool = false @@ -43,7 +43,7 @@ struct PriceWidget: View { } } - /// Single pair (v61). Falls back to first available data if the selection isn't loaded yet. + /// Single pair. Falls back to first available data if the selection isn't loaded yet. private var primaryPrice: PriceData? { let currentPeriodData = viewModel.getCurrentData(for: options.selectedPeriod) if let preferred = options.selectedPairs.first, @@ -135,7 +135,7 @@ struct PriceWidgetCompactContent: View { } } -// MARK: - Chart (line-only per Figma v61) +// MARK: - Chart struct PriceChart: View { let values: [Double] From 73eba60646098867403815ba8552a30d57c61b47 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:15:57 -0300 Subject: [PATCH 10/29] refactor: replace onApper with task --- Bitkit/Components/Widgets/PriceWidget.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index 33e9dd547..b0d832ef8 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -27,7 +27,7 @@ struct PriceWidget: View { ) { content } - .onAppear { fetchPriceData() } + .task { fetchPriceData() } .onChange(of: options.selectedPairs) { fetchPriceData() } .onChange(of: options.selectedPeriod) { fetchPriceData() } } From d07317f89706fe6bb2d8dd7868917eed113308c8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:19:20 -0300 Subject: [PATCH 11/29] refactor: replace onChange with task id --- Bitkit/Components/Widgets/PriceWidget.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index b0d832ef8..f4deacc9b 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -27,9 +27,7 @@ struct PriceWidget: View { ) { content } - .task { fetchPriceData() } - .onChange(of: options.selectedPairs) { fetchPriceData() } - .onChange(of: options.selectedPeriod) { fetchPriceData() } + .task(id: options) { fetchPriceData() } } @ViewBuilder From 8b2ec9728d07ad7a37776cb4515bdc1ed88eb4b6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:41:38 -0300 Subject: [PATCH 12/29] refactor: simplify comments --- Bitkit/Views/Widgets/PriceWidgetPreviewView.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index aa082f2e1..5dbd7c1e9 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -1,10 +1,6 @@ import SwiftUI -/// Preview screen for the Bitcoin Price widget (Figma v61). -/// -/// Replaces the generic `WidgetDetailView` for `.price` only — the other widgets continue to use -/// `WidgetDetailView`. Layout differences from the generic preview: centered top-bar title, -/// description, "Widget Settings" cell, and a Compact ↔ Wide carousel. +/// Preview screen for the Bitcoin Price widget. struct PriceWidgetPreviewView: View { @EnvironmentObject private var navigation: NavigationViewModel @EnvironmentObject private var widgets: WidgetsViewModel From 658614369cf5138ddef4395bb9c20da6b11b7df6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:43:32 -0300 Subject: [PATCH 13/29] refactor: simplyfy comments --- Bitkit/Views/Widgets/PriceWidgetPreviewView.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 5dbd7c1e9..9a34abe36 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -47,7 +47,6 @@ struct PriceWidgetPreviewView: View { VStack(alignment: .leading, spacing: 16) { NavigationBar(title: widgetName, showMenuButton: false) - // Content (description + Widget Settings cell with surrounding dividers) VStack(alignment: .leading, spacing: 0) { BodyMText(widgetDescription, textColor: .textSecondary) .padding(.bottom, 16) @@ -59,7 +58,6 @@ struct PriceWidgetPreviewView: View { Divider().background(Color.white.opacity(0.1)) } - // Carousel section (centered widget + size label + page indicator) VStack(spacing: 16) { Spacer(minLength: 0) From fc0dc8ecb7324ab4348bea5307b23dad1ce67c47 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 09:46:20 -0300 Subject: [PATCH 14/29] refactor: simplify comments --- Bitkit/Models/PriceWidgetOptions.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Bitkit/Models/PriceWidgetOptions.swift b/Bitkit/Models/PriceWidgetOptions.swift index 987d838f7..d0bc0e51f 100644 --- a/Bitkit/Models/PriceWidgetOptions.swift +++ b/Bitkit/Models/PriceWidgetOptions.swift @@ -1,9 +1,6 @@ import Foundation /// Options for configuring the in-app and home-screen price widgets (shared via App Group for the extension). -/// -/// `selectedPairs` is kept as an array for storage backwards-compatibility with v60. The v61 UI is -/// single-select and only ever reads/writes `[firstPair]`. struct PriceWidgetOptions: Codable, Equatable { var selectedPairs: [String] = ["BTC/USD"] var selectedPeriod: GraphPeriod = .oneDay From d01c196e1da257c4762646ab71476e521ab9ea12 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 10:00:27 -0300 Subject: [PATCH 15/29] refactor: simplify comments --- Bitkit/Views/Widgets/WidgetEditLogic.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Bitkit/Views/Widgets/WidgetEditLogic.swift b/Bitkit/Views/Widgets/WidgetEditLogic.swift index a991264c1..562864e18 100644 --- a/Bitkit/Views/Widgets/WidgetEditLogic.swift +++ b/Bitkit/Views/Widgets/WidgetEditLogic.swift @@ -157,7 +157,6 @@ class WidgetEditLogic: ObservableObject { onStateChange?() } - /// Single-select per Figma v61 — replaces the array with a single pair. private func selectTradingPair(_ pairName: String) { priceOptions.selectedPairs = [pairName] } From 770511ec7ea2ba203b9789f33bf5a9d5e495675d Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 10:17:53 -0300 Subject: [PATCH 16/29] refactor: simplify comments --- BitkitWidget/PriceHomeScreenWidget.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index 68200c5dc..c3de7cd92 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -107,7 +107,6 @@ struct PriceHomeScreenWidgetEntryView: View { } } - /// Always render the first selected pair (v61 is single-pair). private var primaryPrice: PriceData? { let preferred = entry.options.selectedPairs.first if let preferred, let match = entry.prices.first(where: { $0.name == preferred }) { @@ -218,7 +217,7 @@ struct PriceHomeScreenWidgetEntryView: View { } } -// MARK: - Chart (line-only per Figma v61) +// MARK: - Chart private struct PriceWidgetChart: View { let values: [Double] From 5f0841fab2050e3b6f01d9bbb7f802b7a31b80d6 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 10:28:45 -0300 Subject: [PATCH 17/29] refactor: remove multi-pair legacy code --- Bitkit/Components/Widgets/PriceWidget.swift | 6 +-- Bitkit/Models/PriceWidgetOptions.swift | 38 ++++++++++++++++++- Bitkit/Utilities/WidgetsBackupConverter.swift | 28 ++++++-------- .../Widgets/PriceWidgetPreviewView.swift | 6 +-- Bitkit/Views/Widgets/WidgetEditLogic.swift | 6 +-- Bitkit/Views/Widgets/WidgetEditModels.swift | 2 +- BitkitWidget/PriceHomeScreenWidget.swift | 9 ++--- 7 files changed, 60 insertions(+), 35 deletions(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index f4deacc9b..0d1323770 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -44,16 +44,14 @@ struct PriceWidget: View { /// Single pair. Falls back to first available data if the selection isn't loaded yet. private var primaryPrice: PriceData? { let currentPeriodData = viewModel.getCurrentData(for: options.selectedPeriod) - if let preferred = options.selectedPairs.first, - let match = currentPeriodData.first(where: { $0.name == preferred }) - { + if let match = currentPeriodData.first(where: { $0.name == options.selectedPair }) { return match } return currentPeriodData.first } private func fetchPriceData() { - viewModel.fetchPriceData(pairs: options.selectedPairs, period: options.selectedPeriod) + viewModel.fetchPriceData(pairs: [options.selectedPair], period: options.selectedPeriod) } } diff --git a/Bitkit/Models/PriceWidgetOptions.swift b/Bitkit/Models/PriceWidgetOptions.swift index d0bc0e51f..f4f90dcb0 100644 --- a/Bitkit/Models/PriceWidgetOptions.swift +++ b/Bitkit/Models/PriceWidgetOptions.swift @@ -1,7 +1,41 @@ import Foundation -/// Options for configuring the in-app and home-screen price widgets (shared via App Group for the extension). +/// Options for configuring the in-app and home-screen price widgets (shared via App Group). +/// struct PriceWidgetOptions: Codable, Equatable { - var selectedPairs: [String] = ["BTC/USD"] + var selectedPair: String = "BTC/USD" var selectedPeriod: GraphPeriod = .oneDay + + init(selectedPair: String = "BTC/USD", selectedPeriod: GraphPeriod = .oneDay) { + self.selectedPair = selectedPair + self.selectedPeriod = selectedPeriod + } + + private enum CodingKeys: String, CodingKey { + case selectedPair + case selectedPairs // legacy v60 key + case selectedPeriod + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let pair = try container.decodeIfPresent(String.self, forKey: .selectedPair) { + selectedPair = pair + } else if let legacyPairs = try container.decodeIfPresent([String].self, forKey: .selectedPairs), + let first = legacyPairs.first + { + selectedPair = first + } else { + selectedPair = "BTC/USD" + } + + selectedPeriod = try container.decodeIfPresent(GraphPeriod.self, forKey: .selectedPeriod) ?? .oneDay + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(selectedPair, forKey: .selectedPair) + try container.encode(selectedPeriod, forKey: .selectedPeriod) + } } diff --git a/Bitkit/Utilities/WidgetsBackupConverter.swift b/Bitkit/Utilities/WidgetsBackupConverter.swift index f1ba83a71..6d0e392c8 100644 --- a/Bitkit/Utilities/WidgetsBackupConverter.swift +++ b/Bitkit/Utilities/WidgetsBackupConverter.swift @@ -59,12 +59,10 @@ enum WidgetsBackupConverter { } case .price: if let options = try? JSONDecoder().decode(PriceWidgetOptions.self, from: optionsData) { - let androidPairs = options.selectedPairs.map { pair in - pair.replacingOccurrences(of: "/", with: "_") - } + let androidPair = options.selectedPair.replacingOccurrences(of: "/", with: "_") let androidPeriod = convertIosPeriodToAndroid(options.selectedPeriod) pricePreferences = [ - "enabledPairs": androidPairs.isEmpty ? ["BTC_USD"] : androidPairs, + "enabledPairs": [androidPair.isEmpty ? "BTC_USD" : androidPair], "period": androidPeriod, ] } @@ -165,19 +163,19 @@ enum WidgetsBackupConverter { } case .price: if let prefs = jsonDict["pricePreferences"] as? [String: Any] { - var selectedPairs = ["BTC/USD"] - if let pairsArray = prefs["enabledPairs"] as? [String] { - selectedPairs = pairsArray.map { pairType in - pairType.replacingOccurrences(of: "_", with: "/") - } - if selectedPairs.isEmpty { - selectedPairs = ["BTC/USD"] + var selectedPair = "BTC/USD" + if let pairsArray = prefs["enabledPairs"] as? [String], + let firstAndroidPair = pairsArray.first + { + let converted = firstAndroidPair.replacingOccurrences(of: "_", with: "/") + if !converted.isEmpty { + selectedPair = converted } } let period = convertAndroidPeriodToIos(prefs["period"] as? String) let iosOptions = PriceWidgetOptions( - selectedPairs: selectedPairs, + selectedPair: selectedPair, selectedPeriod: period ) optionsData = try? JSONEncoder().encode(iosOptions) @@ -234,12 +232,10 @@ enum WidgetsBackupConverter { private static func getDefaultPricePreferences() -> [String: Any] { let defaults = PriceWidgetOptions() - let androidPairs = defaults.selectedPairs.map { pair in - pair.replacingOccurrences(of: "/", with: "_") - } + let androidPair = defaults.selectedPair.replacingOccurrences(of: "/", with: "_") let androidPeriod = convertIosPeriodToAndroid(defaults.selectedPeriod) return [ - "enabledPairs": androidPairs.isEmpty ? ["BTC_USD"] : androidPairs, + "enabledPairs": [androidPair.isEmpty ? "BTC_USD" : androidPair], "period": androidPeriod, ] } diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 9a34abe36..afead9ce9 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -35,9 +35,7 @@ struct PriceWidgetPreviewView: View { private var primaryPrice: PriceData? { let options = currentOptions let currentPeriodData = viewModel.getCurrentData(for: options.selectedPeriod) - if let preferred = options.selectedPairs.first, - let match = currentPeriodData.first(where: { $0.name == preferred }) - { + if let match = currentPeriodData.first(where: { $0.name == options.selectedPair }) { return match } return currentPeriodData.first @@ -77,7 +75,7 @@ struct PriceWidgetPreviewView: View { .padding(.horizontal, 16) .task { let options = currentOptions - viewModel.fetchPriceData(pairs: options.selectedPairs, period: options.selectedPeriod) + viewModel.fetchPriceData(pairs: [options.selectedPair], period: options.selectedPeriod) } .alert( t("widgets__delete__title"), diff --git a/Bitkit/Views/Widgets/WidgetEditLogic.swift b/Bitkit/Views/Widgets/WidgetEditLogic.swift index 562864e18..e6b806683 100644 --- a/Bitkit/Views/Widgets/WidgetEditLogic.swift +++ b/Bitkit/Views/Widgets/WidgetEditLogic.swift @@ -44,8 +44,8 @@ class WidgetEditLogic: ObservableObject { // Weather widget has multiple options, check if any are enabled return weatherOptions.showStatus || weatherOptions.showText || weatherOptions.showMedian || weatherOptions.showNextBlockFee case .price: - // Price widget has options, check if at least one trading pair is selected - return !priceOptions.selectedPairs.isEmpty + // Price widget always has a selected pair (single-select). + return true case .calculator, .suggestions: return false } @@ -158,7 +158,7 @@ class WidgetEditLogic: ObservableObject { } private func selectTradingPair(_ pairName: String) { - priceOptions.selectedPairs = [pairName] + priceOptions.selectedPair = pairName } func loadCurrentOptions() { diff --git a/Bitkit/Views/Widgets/WidgetEditModels.swift b/Bitkit/Views/Widgets/WidgetEditModels.swift index fa1db48ca..76ff00a6b 100644 --- a/Bitkit/Views/Widgets/WidgetEditModels.swift +++ b/Bitkit/Views/Widgets/WidgetEditModels.swift @@ -380,7 +380,7 @@ enum WidgetEditItemFactory { // CURRENCY section (single-select) items.append(sectionHeaderItem(key: "currency_header", title: t("widgets__price__currency"))) - let selectedPair = priceOptions.selectedPairs.first + let selectedPair = priceOptions.selectedPair for pair in tradingPairNames { let isSelected = selectedPair == pair items.append( diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index c3de7cd92..724faf2e1 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -50,7 +50,7 @@ struct PriceWidgetProvider: TimelineProvider { return } - let cached = PriceWidgetService.cachedPrices(pairs: options.selectedPairs, period: options.selectedPeriod) ?? [] + let cached = PriceWidgetService.cachedPrices(pairs: [options.selectedPair], period: options.selectedPeriod) ?? [] completion(PriceWidgetEntry(date: Date(), prices: cached, options: options, showsError: false)) } @@ -61,12 +61,12 @@ struct PriceWidgetProvider: TimelineProvider { let entry: PriceWidgetEntry do { let fresh = try await PriceWidgetService.fetchFreshPrices( - pairs: options.selectedPairs, + pairs: [options.selectedPair], period: options.selectedPeriod ) entry = PriceWidgetEntry(date: Date(), prices: fresh, options: options, showsError: false) } catch { - let cached = PriceWidgetService.cachedPrices(pairs: options.selectedPairs, period: options.selectedPeriod) ?? [] + let cached = PriceWidgetService.cachedPrices(pairs: [options.selectedPair], period: options.selectedPeriod) ?? [] entry = PriceWidgetEntry(date: Date(), prices: cached, options: options, showsError: cached.isEmpty) } @@ -108,8 +108,7 @@ struct PriceHomeScreenWidgetEntryView: View { } private var primaryPrice: PriceData? { - let preferred = entry.options.selectedPairs.first - if let preferred, let match = entry.prices.first(where: { $0.name == preferred }) { + if let match = entry.prices.first(where: { $0.name == entry.options.selectedPair }) { return match } return entry.prices.first From 51c2f4130e8083158419f766534db8d68c336039 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 10:52:06 -0300 Subject: [PATCH 18/29] fix: fallback to os widget options after remove in-app --- Bitkit/ViewModels/WidgetsViewModel.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Bitkit/ViewModels/WidgetsViewModel.swift b/Bitkit/ViewModels/WidgetsViewModel.swift index 67a6a804e..ee0edad19 100644 --- a/Bitkit/ViewModels/WidgetsViewModel.swift +++ b/Bitkit/ViewModels/WidgetsViewModel.swift @@ -232,6 +232,10 @@ class WidgetsViewModel: ObservableObject { return options } + if type == .price, let priceOptions = PriceHomeScreenWidgetOptionsStore.load() as? T { + return priceOptions + } + // Return default options if none saved return getDefaultOptions(for: type) as! T } From afb421ad2f6ddf53c18971043380db0610357d9c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 11:10:09 -0300 Subject: [PATCH 19/29] fix: make chart height adaptable --- BitkitWidget/PriceHomeScreenWidget.swift | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index 724faf2e1..55ab2b7c0 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -117,7 +117,7 @@ struct PriceHomeScreenWidgetEntryView: View { // MARK: - Compact (small widget — 163×192) private func compactLayout(data: PriceData) -> some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 0) { captionUpText(data.name) @@ -134,15 +134,17 @@ struct PriceHomeScreenWidgetEntryView: View { .widgetAccentable() } - chart(values: data.pastValues, isPositive: data.change.isPositive, height: 64) + Spacer(minLength: 8) + + chart(values: data.pastValues, isPositive: data.change.isPositive, idealHeight: 64) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - // MARK: - Wide (medium / large widget) + // MARK: - Wide (medium widget — 343×152) private func wideLayout(data: PriceData) -> some View { - VStack(alignment: .leading, spacing: 8) { + VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .center, spacing: 16) { captionUpText("\(data.name) \(entry.options.selectedPeriod.rawValue)") @@ -158,7 +160,9 @@ struct PriceHomeScreenWidgetEntryView: View { priceText(data.price, size: 34, lineHeight: 34) } - chart(values: data.pastValues, isPositive: data.change.isPositive, height: 48) + Spacer(minLength: 4) + + chart(values: data.pastValues, isPositive: data.change.isPositive, idealHeight: 48, minHeight: 24) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } @@ -181,13 +185,13 @@ struct PriceHomeScreenWidgetEntryView: View { .widgetAccentable() } - private func chart(values: [Double], isPositive: Bool, height: CGFloat) -> some View { + private func chart(values: [Double], isPositive: Bool, idealHeight: CGFloat, minHeight: CGFloat = 32) -> some View { PriceWidgetChart( values: values, isPositive: isPositive, renderingMode: widgetRenderingMode ) - .frame(height: height) + .frame(minHeight: minHeight, maxHeight: idealHeight) .widgetAccentable() } From 5be60140abacd71b66749b9c5daf499cded7ad99 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Wed, 6 May 2026 13:31:14 -0300 Subject: [PATCH 20/29] feat: set backgroud color Gray7 --- Bitkit/Components/NavigationBar.swift | 19 ++++++++++++++----- .../Widgets/PriceWidgetPreviewView.swift | 4 +++- Bitkit/Views/Widgets/WidgetEditView.swift | 5 ++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Bitkit/Components/NavigationBar.swift b/Bitkit/Components/NavigationBar.swift index 36fb13dbb..787334442 100644 --- a/Bitkit/Components/NavigationBar.swift +++ b/Bitkit/Components/NavigationBar.swift @@ -7,6 +7,7 @@ struct NavigationBar: View { let title: String let showBackButton: Bool let showMenuButton: Bool + let showGradient: Bool let action: AnyView? let icon: String? let onBack: (() -> Void)? @@ -15,6 +16,7 @@ struct NavigationBar: View { title: String, showBackButton: Bool = true, showMenuButton: Bool = true, + showGradient: Bool = true, action: AnyView? = nil, icon: String? = nil, onBack: (() -> Void)? = nil @@ -22,6 +24,7 @@ struct NavigationBar: View { self.title = title self.showBackButton = showBackButton self.showMenuButton = showMenuButton + self.showGradient = showGradient self.action = action self.icon = icon self.onBack = onBack @@ -89,11 +92,17 @@ struct NavigationBar: View { } } .frame(height: 48) - .background(LinearGradient( - colors: [.black, .black.opacity(0)], - startPoint: .top, - endPoint: .bottom - )) + .background( + Group { + if showGradient { + LinearGradient( + colors: [.black, .black.opacity(0)], + startPoint: .top, + endPoint: .bottom + ) + } + } + ) .zIndex(.infinity) } } diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index afead9ce9..54d7bca05 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -43,7 +43,7 @@ struct PriceWidgetPreviewView: View { var body: some View { VStack(alignment: .leading, spacing: 16) { - NavigationBar(title: widgetName, showMenuButton: false) + NavigationBar(title: widgetName, showMenuButton: false, showGradient: false) VStack(alignment: .leading, spacing: 0) { BodyMText(widgetDescription, textColor: .textSecondary) @@ -73,6 +73,8 @@ struct PriceWidgetPreviewView: View { } .navigationBarHidden(true) .padding(.horizontal, 16) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray7.ignoresSafeArea()) .task { let options = currentOptions viewModel.fetchPriceData(pairs: [options.selectedPair], period: options.selectedPeriod) diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index e119ee310..4c3e81a88 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -59,7 +59,8 @@ struct WidgetEditView: View { VStack(alignment: .leading, spacing: 0) { NavigationBar( title: id == .price ? widget.name : t("widgets__widget__edit"), - showMenuButton: id != .price + showMenuButton: id != .price, + showGradient: id != .price ) .padding(.bottom, 16) @@ -112,6 +113,8 @@ struct WidgetEditView: View { } .navigationBarHidden(true) .padding(.horizontal, 16) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(Color.gray7.ignoresSafeArea()) .onAppear { if editLogic == nil { let logic = WidgetEditLogic(widgetType: id, widgetsViewModel: widgets) From 6e3ab90ddda2937ac9e9296d2d9a017632f10a5a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 13:51:43 -0300 Subject: [PATCH 21/29] fix: reuse existing text component and remove scale factor --- BitkitWidget/PriceHomeScreenWidget.swift | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index 55ab2b7c0..a01033047 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -120,12 +120,12 @@ struct PriceHomeScreenWidgetEntryView: View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 0) { - captionUpText(data.name) + CaptionMText(data.name, textColor: secondaryTextColor) Spacer(minLength: 0) - captionUpText(entry.options.selectedPeriod.rawValue) + CaptionMText(entry.options.selectedPeriod.rawValue, textColor: secondaryTextColor) } - priceText(data.price, size: 22, lineHeight: 26) + priceText(data.price, size: 22) Text(data.change.formatted) .font(Fonts.semiBold(size: 15)) @@ -147,7 +147,7 @@ struct PriceHomeScreenWidgetEntryView: View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 4) { HStack(alignment: .center, spacing: 16) { - captionUpText("\(data.name) \(entry.options.selectedPeriod.rawValue)") + CaptionMText("\(data.name) \(entry.options.selectedPeriod.rawValue)", textColor: secondaryTextColor) .frame(maxWidth: .infinity, alignment: .leading) Text(data.change.formatted) @@ -157,7 +157,7 @@ struct PriceHomeScreenWidgetEntryView: View { .widgetAccentable() } - priceText(data.price, size: 34, lineHeight: 34) + priceText(data.price, size: 34) } Spacer(minLength: 4) @@ -169,19 +169,11 @@ struct PriceHomeScreenWidgetEntryView: View { // MARK: - Sub-views - private func captionUpText(_ text: String) -> Text { - Text(text) - .font(Fonts.medium(size: 13)) - .tracking(1) - .foregroundColor(secondaryTextColor) - } - - private func priceText(_ value: String, size: CGFloat, lineHeight: CGFloat) -> some View { + private func priceText(_ value: String, size: CGFloat) -> some View { Text(value) .font(Fonts.bold(size: size)) .foregroundColor(valueTextColor) .lineLimit(1) - .minimumScaleFactor(0.7) .widgetAccentable() } From b2df71f314a2717f52ce5551ed299adb2e331ce8 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 13:58:59 -0300 Subject: [PATCH 22/29] fix: display white32 checkmark for unselected item --- Bitkit/Views/Widgets/WidgetEditItemView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Bitkit/Views/Widgets/WidgetEditItemView.swift b/Bitkit/Views/Widgets/WidgetEditItemView.swift index c21c8ad01..65c6d6c9f 100644 --- a/Bitkit/Views/Widgets/WidgetEditItemView.swift +++ b/Bitkit/Views/Widgets/WidgetEditItemView.swift @@ -33,10 +33,10 @@ struct WidgetEditItemView: View { .frame(maxWidth: .infinity, alignment: .trailing) } - if item.type != .staticItem, item.isChecked { + if item.type != .staticItem { Image("check-mark") .resizable() - .foregroundColor(.brandAccent) + .foregroundColor(item.isChecked ? .brandAccent : .white32) .frame(width: 32, height: 32) } } From 7994460cb14182925f53c476667a03d91ff2a216 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 14:08:25 -0300 Subject: [PATCH 23/29] fix: vertical padding anchored to checkbox image --- Bitkit/Views/Widgets/WidgetEditItemView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Bitkit/Views/Widgets/WidgetEditItemView.swift b/Bitkit/Views/Widgets/WidgetEditItemView.swift index 65c6d6c9f..ce47e12b3 100644 --- a/Bitkit/Views/Widgets/WidgetEditItemView.swift +++ b/Bitkit/Views/Widgets/WidgetEditItemView.swift @@ -21,7 +21,7 @@ struct WidgetEditItemView: View { } private var row: some View { - VStack(spacing: 0) { + VStack(spacing: 8) { HStack(spacing: 16) { item.titleView .frame(maxWidth: .infinity, alignment: .leading) @@ -40,10 +40,11 @@ struct WidgetEditItemView: View { .frame(width: 32, height: 32) } } - .padding(.vertical, 16) + .frame(minHeight: 32) .contentShape(Rectangle()) Divider() } + .padding(.top, 8) } } From 200d2f84f7ea3ab5694bbe0e1d00b3fcac76c66a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 14:18:42 -0300 Subject: [PATCH 24/29] fix: remove the gray bg and custom bg from Navigation bar --- Bitkit/Components/NavigationBar.swift | 19 +++++-------------- .../Widgets/PriceWidgetPreviewView.swift | 3 +-- Bitkit/Views/Widgets/WidgetEditView.swift | 4 +--- 3 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Bitkit/Components/NavigationBar.swift b/Bitkit/Components/NavigationBar.swift index 787334442..36fb13dbb 100644 --- a/Bitkit/Components/NavigationBar.swift +++ b/Bitkit/Components/NavigationBar.swift @@ -7,7 +7,6 @@ struct NavigationBar: View { let title: String let showBackButton: Bool let showMenuButton: Bool - let showGradient: Bool let action: AnyView? let icon: String? let onBack: (() -> Void)? @@ -16,7 +15,6 @@ struct NavigationBar: View { title: String, showBackButton: Bool = true, showMenuButton: Bool = true, - showGradient: Bool = true, action: AnyView? = nil, icon: String? = nil, onBack: (() -> Void)? = nil @@ -24,7 +22,6 @@ struct NavigationBar: View { self.title = title self.showBackButton = showBackButton self.showMenuButton = showMenuButton - self.showGradient = showGradient self.action = action self.icon = icon self.onBack = onBack @@ -92,17 +89,11 @@ struct NavigationBar: View { } } .frame(height: 48) - .background( - Group { - if showGradient { - LinearGradient( - colors: [.black, .black.opacity(0)], - startPoint: .top, - endPoint: .bottom - ) - } - } - ) + .background(LinearGradient( + colors: [.black, .black.opacity(0)], + startPoint: .top, + endPoint: .bottom + )) .zIndex(.infinity) } } diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 54d7bca05..57fba9cb4 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -43,7 +43,7 @@ struct PriceWidgetPreviewView: View { var body: some View { VStack(alignment: .leading, spacing: 16) { - NavigationBar(title: widgetName, showMenuButton: false, showGradient: false) + NavigationBar(title: widgetName, showMenuButton: false) VStack(alignment: .leading, spacing: 0) { BodyMText(widgetDescription, textColor: .textSecondary) @@ -74,7 +74,6 @@ struct PriceWidgetPreviewView: View { .navigationBarHidden(true) .padding(.horizontal, 16) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray7.ignoresSafeArea()) .task { let options = currentOptions viewModel.fetchPriceData(pairs: [options.selectedPair], period: options.selectedPeriod) diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index 4c3e81a88..020bbebf7 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -59,8 +59,7 @@ struct WidgetEditView: View { VStack(alignment: .leading, spacing: 0) { NavigationBar( title: id == .price ? widget.name : t("widgets__widget__edit"), - showMenuButton: id != .price, - showGradient: id != .price + showMenuButton: id != .price ) .padding(.bottom, 16) @@ -114,7 +113,6 @@ struct WidgetEditView: View { .navigationBarHidden(true) .padding(.horizontal, 16) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color.gray7.ignoresSafeArea()) .onAppear { if editLogic == nil { let logic = WidgetEditLogic(widgetType: id, widgetsViewModel: widgets) From 870d5e804c0cd411491f096df46ed43a2af0fdf4 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 14:24:03 -0300 Subject: [PATCH 25/29] fix: try to fetch real data for preview --- BitkitWidget/PriceHomeScreenWidget.swift | 36 ++++++++++++++++++------ 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/BitkitWidget/PriceHomeScreenWidget.swift b/BitkitWidget/PriceHomeScreenWidget.swift index a01033047..f30057c11 100644 --- a/BitkitWidget/PriceHomeScreenWidget.swift +++ b/BitkitWidget/PriceHomeScreenWidget.swift @@ -34,23 +34,43 @@ struct PriceWidgetProvider: TimelineProvider { }() func placeholder(in _: Context) -> PriceWidgetEntry { - Self.mockEntry + let options = PriceHomeScreenWidgetOptionsStore.load() + if let cached = PriceWidgetService.cachedPrices(pairs: [options.selectedPair], period: options.selectedPeriod), + !cached.isEmpty + { + return PriceWidgetEntry(date: Date(), prices: cached, options: options, showsError: false) + } + return Self.mockEntry } func getSnapshot(in context: Context, completion: @escaping (PriceWidgetEntry) -> Void) { let options = PriceHomeScreenWidgetOptionsStore.load() + let cached = PriceWidgetService.cachedPrices(pairs: [options.selectedPair], period: options.selectedPeriod) ?? [] + + if !cached.isEmpty { + completion(PriceWidgetEntry(date: Date(), prices: cached, options: options, showsError: false)) + return + } if context.isPreview { - completion(PriceWidgetEntry( - date: Self.mockEntry.date, - prices: Self.mockEntry.prices, - options: options, - showsError: false - )) + Task { + if let fresh = try? await PriceWidgetService.fetchFreshPrices( + pairs: [options.selectedPair], + period: options.selectedPeriod + ), !fresh.isEmpty { + completion(PriceWidgetEntry(date: Date(), prices: fresh, options: options, showsError: false)) + } else { + completion(PriceWidgetEntry( + date: Self.mockEntry.date, + prices: Self.mockEntry.prices, + options: options, + showsError: false + )) + } + } return } - let cached = PriceWidgetService.cachedPrices(pairs: [options.selectedPair], period: options.selectedPeriod) ?? [] completion(PriceWidgetEntry(date: Date(), prices: cached, options: options, showsError: false)) } From d2f4120ea937b326e46bbf4756ae84d8ec1d0839 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 14:30:24 -0300 Subject: [PATCH 26/29] refactor: make string keys generic to be reused in the furue implementtions --- Bitkit/Resources/Localization/en.lproj/Localizable.strings | 6 +++--- Bitkit/Views/Widgets/PriceWidgetPreviewView.swift | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index afad97a20..4bcd88e86 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -1397,9 +1397,9 @@ "widgets__price__period_week" = "Week"; "widgets__price__period_month" = "Month"; "widgets__price__period_year" = "Year"; -"widgets__price__size_small" = "Small"; -"widgets__price__size_wide" = "Wide"; -"widgets__price__widget_settings" = "Widget Settings"; +"widgets__widget__size_small" = "Small"; +"widgets__widget__size_wide" = "Wide"; +"widgets__widget__settings" = "Widget Settings"; "widgets__widget__save_widget" = "Save Widget"; "widgets__news__name" = "Bitcoin Headlines"; "widgets__news__description" = "Read the latest & greatest Bitcoin headlines from various news sites."; diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index 57fba9cb4..ff2ca7b5b 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -96,7 +96,7 @@ struct PriceWidgetPreviewView: View { private var widgetSettingsRow: some View { Button(action: { navigation.navigate(.widgetEdit(widgetType)) }) { HStack(alignment: .center, spacing: 0) { - BodyMText(t("widgets__price__widget_settings"), textColor: .textPrimary) + BodyMText(t("widgets__widget__settings"), textColor: .textPrimary) Spacer() @@ -188,8 +188,8 @@ struct PriceWidgetPreviewView: View { Spacer() CaptionMText( carouselPage == 0 - ? t("widgets__price__size_small") - : t("widgets__price__size_wide"), + ? t("widgets__widget__size_small") + : t("widgets__widget__size_wide"), textColor: .textSecondary ) .textCase(.uppercase) From 1c8350ad020146cb2d230e022a141ffabf2a869c Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 14:38:39 -0300 Subject: [PATCH 27/29] fix: make prevew frame height adaptable --- Bitkit/Views/Widgets/PriceWidgetPreviewView.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift index ff2ca7b5b..ef037893c 100644 --- a/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift +++ b/Bitkit/Views/Widgets/PriceWidgetPreviewView.swift @@ -57,12 +57,8 @@ struct PriceWidgetPreviewView: View { } VStack(spacing: 16) { - Spacer(minLength: 0) - carousel - Spacer(minLength: 0) - sizeLabel pageIndicator @@ -74,6 +70,7 @@ struct PriceWidgetPreviewView: View { .navigationBarHidden(true) .padding(.horizontal, 16) .frame(maxWidth: .infinity, maxHeight: .infinity) + .bottomSafeAreaPadding() .task { let options = currentOptions viewModel.fetchPriceData(pairs: [options.selectedPair], period: options.selectedPeriod) @@ -131,7 +128,7 @@ struct PriceWidgetPreviewView: View { .tag(1) } .tabViewStyle(.page(indexDisplayMode: .never)) - .frame(height: 320) + .frame(maxHeight: .infinity) } private var compactPage: some View { From 88843b67b218d7bd6df6574ad64c3de3ea86daba Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 7 May 2026 15:24:38 -0300 Subject: [PATCH 28/29] fix: remove app group fallback --- Bitkit/ViewModels/WidgetsViewModel.swift | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Bitkit/ViewModels/WidgetsViewModel.swift b/Bitkit/ViewModels/WidgetsViewModel.swift index ee0edad19..8bd9a4d23 100644 --- a/Bitkit/ViewModels/WidgetsViewModel.swift +++ b/Bitkit/ViewModels/WidgetsViewModel.swift @@ -232,10 +232,6 @@ class WidgetsViewModel: ObservableObject { return options } - if type == .price, let priceOptions = PriceHomeScreenWidgetOptionsStore.load() as? T { - return priceOptions - } - // Return default options if none saved return getDefaultOptions(for: type) as! T } @@ -258,6 +254,10 @@ class WidgetsViewModel: ObservableObject { } persistSavedWidgets() + + if type == .price, let priceOptions = options as? PriceWidgetOptions { + syncPriceOptionsToHomeScreenWidget(priceOptions) + } } catch { print("Failed to save widget options: \(error)") } @@ -305,7 +305,6 @@ class WidgetsViewModel: ObservableObject { savedWidgets = savedWidgetsWithOptions.map { $0.toWidget() } persistSavedWidgets() } - syncPriceOptionsToHomeScreenWidget() } private func persistSavedWidgets() { @@ -315,12 +314,12 @@ class WidgetsViewModel: ObservableObject { } catch { print("Failed to persist widgets: \(error)") } - syncPriceOptionsToHomeScreenWidget() } - /// Keeps the home-screen WidgetKit price widget in sync with in-app price widget options (App Group). - private func syncPriceOptionsToHomeScreenWidget() { - let options: PriceWidgetOptions = getOptions(for: .price, as: PriceWidgetOptions.self) + /// Mirrors in-app price widget options to the App Group so the home-screen WidgetKit widget can read them. + /// Only invoked when the user explicitly changes price widget options — adding, deleting, or resetting + /// in-app widgets must not affect the independent OS home-screen widget. + private func syncPriceOptionsToHomeScreenWidget(_ options: PriceWidgetOptions) { PriceHomeScreenWidgetOptionsStore.save(options) PriceHomeScreenWidgetOptionsStore.reloadHomeScreenWidgetIfNeeded() } From 9b221d001d2e272887119b085d07df16398ef106 Mon Sep 17 00:00:00 2001 From: Piotr Stachyra Date: Fri, 8 May 2026 11:44:09 +0200 Subject: [PATCH 29/29] test: widget test ids adjustment --- Bitkit/Components/Widgets/PriceWidget.swift | 8 ++++++++ Bitkit/Views/Widgets/WidgetEditView.swift | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Bitkit/Components/Widgets/PriceWidget.swift b/Bitkit/Components/Widgets/PriceWidget.swift index 0d1323770..eb75c65fc 100644 --- a/Bitkit/Components/Widgets/PriceWidget.swift +++ b/Bitkit/Components/Widgets/PriceWidget.swift @@ -74,7 +74,9 @@ struct PriceWidgetWideContent: View { textColor: data.change.isPositive ? .greenAccent : .redAccent ) .lineLimit(1) + .accessibilityIdentifier("price_card_pair_change_\(data.name)") } + .accessibilityIdentifier("PriceWidgetRow-\(data.name)") Text(data.price) .font(Fonts.bold(size: 34)) @@ -82,10 +84,12 @@ struct PriceWidgetWideContent: View { .lineLimit(1) .minimumScaleFactor(0.7) .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityIdentifier("price_card_pair_price_\(data.name)") } PriceChart(values: data.pastValues, isPositive: data.change.isPositive) .frame(height: 48) + .accessibilityIdentifier("price_card_chart") } .frame(maxWidth: .infinity, alignment: .leading) } @@ -107,22 +111,26 @@ struct PriceWidgetCompactContent: View { CaptionMText(period.rawValue, textColor: .textSecondary) .textCase(.uppercase) } + .accessibilityIdentifier("price_card_small_pair_row_\(data.name)") Text(data.price) .font(Fonts.bold(size: 22)) .foregroundColor(.textPrimary) .lineLimit(1) .minimumScaleFactor(0.7) + .accessibilityIdentifier("price_card_small_pair_price_\(data.name)") BodySSBText( data.change.formatted, textColor: data.change.isPositive ? .greenAccent : .redAccent ) .lineLimit(1) + .accessibilityIdentifier("price_card_small_pair_change_\(data.name)") } PriceChart(values: data.pastValues, isPositive: data.change.isPositive) .frame(height: 64) + .accessibilityIdentifier("price_card_small_chart") } .padding(16) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) diff --git a/Bitkit/Views/Widgets/WidgetEditView.swift b/Bitkit/Views/Widgets/WidgetEditView.swift index 020bbebf7..09152b67e 100644 --- a/Bitkit/Views/Widgets/WidgetEditView.swift +++ b/Bitkit/Views/Widgets/WidgetEditView.swift @@ -78,7 +78,7 @@ struct WidgetEditView: View { item: item, onToggle: { editLogic?.toggleOption(item) } ) - .accessibilityIdentifier("WidgetEditField-\(item.key)") + .accessibilityIdentifier("\(item.key)_setting_row") } } .id(refreshTrigger) // Force refresh when refreshTrigger changes