Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
769b1bc
feat: migrate weather widget to figma v61 and port OS widget
jvsena42 May 18, 2026
30a1c5a
fix: use shared localize key
jvsena42 May 18, 2026
4f9a610
fix: remove unnecessary minimumScaleFactor
jvsena42 May 18, 2026
091fec9
fix: verticall padding
jvsena42 May 18, 2026
2ec7914
fix: apply accent color
jvsena42 May 18, 2026
0b48820
refactor: remove dead code
jvsena42 May 18, 2026
4b8af03
Merge branch 'feat/os-widgets' into feat/weather-v61
jvsena42 May 18, 2026
319b1ba
Merge branch 'feat/os-widgets' into feat/weather-v61
jvsena42 May 18, 2026
5458fe3
refactor: use existing text components
jvsena42 May 18, 2026
917940d
refactor: cleanup comments
jvsena42 May 18, 2026
9c3b6c1
fix: display two lines in description
jvsena42 May 18, 2026
e5f9695
refactor: cleanup comments
jvsena42 May 19, 2026
c600609
refactor: cleanup comments
jvsena42 May 19, 2026
8ec8226
refactor: comments cleanup
jvsena42 May 19, 2026
f96e4d2
refactor: comments cleanup
jvsena42 May 19, 2026
d5b37b1
fix: cross-restore keys
jvsena42 May 19, 2026
f0bbeaa
refactor: comments cleanup
jvsena42 May 19, 2026
5bea776
refactor: comments cleanup
jvsena42 May 19, 2026
f89df73
refactor: comments cleanup
jvsena42 May 19, 2026
cfcc9ae
refactor: comments cleanup
jvsena42 May 19, 2026
59e2789
refactor: comments cleanup
jvsena42 May 19, 2026
2450205
refactor: use shared logic for fee conditions calculation
jvsena42 May 19, 2026
6b82ff9
fix: home widget stale data when user dont open the app for too long
jvsena42 May 19, 2026
d0586ca
refactor: simplify weather fetch from cache
jvsena42 May 19, 2026
39d8638
fix: prefer cached data for preview
jvsena42 May 19, 2026
21bbe7c
feat: short title translations
jvsena42 May 19, 2026
5d836c7
fix: add deduplication check
jvsena42 May 19, 2026
5ca4bea
fix: bump refresh interval
jvsena42 May 19, 2026
d0c6ad0
fix: display user selected currency
jvsena42 May 19, 2026
fab8d9b
chore: lint
jvsena42 May 19, 2026
101bccb
refactor: share api call logic
jvsena42 May 19, 2026
da496e7
fis: support for migrate legacy options
jvsena42 May 19, 2026
f1c0986
refactor: drop dead fields
jvsena42 May 19, 2026
54c74ea
fix: add eagerly LanguageManager and migration service bypass the new…
jvsena42 May 19, 2026
e4da6cc
fix: unit tests compilation error
jvsena42 May 19, 2026
03537c0
fix: uppercase text
jvsena42 May 20, 2026
931dddb
fix: uppercase text
jvsena42 May 20, 2026
6161eb8
fix: scale components for iphone SE
jvsena42 May 20, 2026
4f0e3b6
fix: text scaling
jvsena42 May 20, 2026
265c38c
fix: use FootnoteText for compact mode
jvsena42 May 20, 2026
c31230d
fix: space between
jvsena42 May 20, 2026
8908843
fix: localization
jvsena42 May 20, 2026
8c06658
feat: update description
jvsena42 May 20, 2026
e85b807
feat: change value color according conditions
jvsena42 May 20, 2026
f7cf61f
fix: ensure the weather cache can be reformatted into the new curren…
jvsena42 May 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
Fonts/InterTight-Regular.ttf,
Constants/WidgetEnv.swift,
Fonts/InterTight-SemiBold.ttf,
Components/Widgets/WeatherWidgetContent.swift,
Models/BlocksWidgetData.swift,
Models/BlocksWidgetFields.swift,
Models/BlocksWidgetOptions.swift,
Expand All @@ -198,12 +199,17 @@
Models/NewsWidgetOptions.swift,
Models/PriceWidgetData.swift,
Models/PriceWidgetOptions.swift,
Models/WeatherWidgetData.swift,
Models/WeatherWidgetOptions.swift,
Services/Widgets/BlocksHomeScreenWidgetOptionsStore.swift,
Services/Widgets/MempoolWeatherAPI.swift,
Services/Widgets/NewsHomeScreenWidgetOptionsStore.swift,
Services/Widgets/PriceHomeScreenWidgetOptionsStore.swift,
Services/Widgets/WeatherHomeScreenWidgetOptionsStore.swift,
Styles/Colors.swift,
Styles/Fonts.swift,
Styles/TextStyle.swift,
Utilities/LocalizeHelpers.swift,
);
target = 4A319B4E2E8F24F2002B9AC9 /* BitkitWidgetExtension */;
};
Expand Down
1 change: 1 addition & 0 deletions Bitkit/AppScene.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct AppScene: View {
@StateObject private var transferTracking: TransferTrackingManager
@StateObject private var channelDetails = ChannelDetailsViewModel.shared
@StateObject private var migrations = MigrationsService.shared
@StateObject private var languageManager = LanguageManager.shared
@StateObject private var pubkyProfile = PubkyProfileManager()
@StateObject private var contactsManager = ContactsManager()
@State private var keyboardManager = KeyboardManager()
Expand Down
166 changes: 17 additions & 149 deletions Bitkit/Components/Widgets/WeatherWidget.swift
Original file line number Diff line number Diff line change
@@ -1,78 +1,13 @@
import SwiftUI

/// Options for configuring the WeatherWidget
struct WeatherWidgetOptions: Codable, Equatable {
var showStatus: Bool = true
var showText: Bool = true
var showMedian: Bool = true
var showNextBlockFee: Bool = true
}

/// Fee condition enum matching the React Native implementation
enum FeeCondition: String, Codable {
case good
case average
case poor

var title: String {
switch self {
case .good:
return t("widgets__weather__condition__good__title")
case .average:
return t("widgets__weather__condition__average__title")
case .poor:
return t("widgets__weather__condition__poor__title")
}
}

var description: String {
switch self {
case .good:
return t("widgets__weather__condition__good__description")
case .average:
return t("widgets__weather__condition__average__description")
case .poor:
return t("widgets__weather__condition__poor__description")
}
}

var icon: String {
switch self {
case .good:
return "☀️"
case .average:
return "⛅"
case .poor:
return "⛈️"
}
}
}

/// Weather widget data model
struct WeatherData: Codable {
let condition: FeeCondition
let currentFee: String
let nextBlockFee: Int
}

/// A widget that displays Bitcoin fee weather information
struct WeatherWidget: View {
/// Configuration options for the widget
var options: WeatherWidgetOptions = .init()

/// Flag indicating if the widget is in editing mode
var isEditing: Bool = false

/// Callback to signal when editing should end
var onEditingEnd: (() -> Void)?

/// View model for handling weather data
@StateObject private var viewModel = WeatherViewModel.shared

/// Currency view model for currency conversion
@EnvironmentObject private var currency: CurrencyViewModel

/// Initialize the widget
init(
options: WeatherWidgetOptions = WeatherWidgetOptions(),
isEditing: Bool = false,
Expand All @@ -89,96 +24,29 @@ struct WeatherWidget: View {
isEditing: isEditing,
onEditingEnd: onEditingEnd
) {
VStack(spacing: 0) {
if viewModel.isLoading {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil {
WidgetContentBuilder.errorView(t("widgets__weather__error"))
} else if let data = viewModel.weatherData {
VStack(spacing: 16) {
// Status condition with icon
if options.showStatus {
HStack(spacing: 16) {
WeatherTitleText(data.condition.title)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity, alignment: .leading)

Text(data.condition.icon)
.font(.system(size: 52))
.frame(width: 52, height: 52)
}
}

// Description text
if options.showText {
BodyMText(data.condition.description, textColor: .textPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
}

// Fee information rows
if options.showMedian || options.showNextBlockFee {
VStack(spacing: 8) {
if options.showMedian {
HStack(spacing: 0) {
HStack {
BodySSBText(t("widgets__weather__current_fee"), textColor: .textSecondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)

HStack {
BodySSBText(data.currentFee)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(minHeight: 20)
}

if options.showNextBlockFee {
HStack(spacing: 0) {
HStack {
BodySSBText(t("widgets__weather__next_block"), textColor: .textSecondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)

HStack {
BodySSBText("\(data.nextBlockFee) ₿/vByte")
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(minHeight: 20)
}
}
}
}
}
}
content
}
.onAppear {
// Inject currency dependency into view model
.task {
viewModel.setCurrencyViewModel(currency)
// Start data updates
viewModel.startUpdates()
}
}
}

struct WeatherTitleText: View {
let text: String

init(_ text: String) {
self.text = text
}

var body: some View {
Text(text)
.font(Fonts.bold(size: 22))
.foregroundColor(.textPrimary)
.kerning(0)
.environment(\._lineHeightMultiple, 0.85)
@ViewBuilder
private var content: some View {
if viewModel.isLoading && viewModel.weatherData == nil {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil && viewModel.weatherData == nil {
WidgetContentBuilder.errorView(t("widgets__weather__error"))
} else if let data = viewModel.weatherData {
WeatherWidgetWideContent(
data: data,
metric: options.selectedMetric,
conditionTitle: t(data.condition.titleKey),
conditionDescription: t(data.condition.descriptionKey),
metricLabel: t(options.selectedMetric.labelKey)
)
}
}
}

Expand Down
138 changes: 138 additions & 0 deletions Bitkit/Components/Widgets/WeatherWidgetContent.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import SwiftUI
import WidgetKit

// MARK: - Display metric helpers

extension WeatherDisplayMetric {
var labelKey: String {
switch self {
case .fiatFee, .satsFee:
return "widgets__weather__current_fee"
case .nextBlockFee:
return "widgets__weather__next_block"
}
}

func value(from data: CachedWeather) -> String {
switch self {
case .fiatFee:
return data.currentFeeFiat
case .satsFee:
return "₿ \(data.currentFeeSats)"
case .nextBlockFee:
return "\(data.nextBlockFee) ₿/VBYTE"
}
}

var fallbackPreviewValue: String {
switch self {
case .fiatFee: return "$ 0.52"
case .satsFee: return "₿ 520"
case .nextBlockFee: return "6 ₿/VBYTE"
}
}
}

// MARK: - Shared metric block

struct WeatherFeeMetric: View {
let label: String
let value: String
var valueColor: Color = .greenAccent
var valueSize: CGFloat = 30
/// Use a smaller label (`FootnoteText`) so the caption doesn't dominate the value
/// in the space-constrained compact widget.
var compactLabel: Bool = false

var body: some View {
VStack(alignment: .leading, spacing: 4) {
labelView
Text(value)
.font(Fonts.bold(size: valueSize))
.foregroundColor(valueColor)
.kerning(-1)
.lineLimit(1)
.minimumScaleFactor(0.9)
.widgetAccentable()
}
}

@ViewBuilder
private var labelView: some View {
if compactLabel {
FootnoteText(label, textColor: .white64)
.textCase(.uppercase)
.lineLimit(1)
.minimumScaleFactor(0.7)
} else {
CaptionMText(label, textColor: .white64)
.textCase(.uppercase)
.lineLimit(1)
}
}
}

struct WeatherWidgetWideContent: View {
let data: CachedWeather
let metric: WeatherDisplayMetric
let conditionTitle: String
let conditionDescription: String
let metricLabel: String

var body: some View {
HStack(alignment: .top, spacing: 16) {
VStack(alignment: .leading, spacing: 16) {
VStack(alignment: .leading, spacing: 4) {
SubtitleText(conditionTitle, textColor: .white)
BodySText(conditionDescription, textColor: .white80)
.lineLimit(2)
.fixedSize(horizontal: false, vertical: true)
}

WeatherFeeMetric(
label: metricLabel,
value: metric.value(from: data),
valueColor: data.condition.valueColor
)
}
.frame(maxWidth: .infinity, alignment: .leading)

Text(data.condition.icon)
.font(.system(size: 82))
.frame(width: 82, height: 82)
.widgetAccentable()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}

struct WeatherWidgetCompactContent: View {
let data: CachedWeather
let metric: WeatherDisplayMetric
let conditionTitle: String
let metricLabel: String

var body: some View {
VStack(alignment: .leading, spacing: 8) {
VStack(alignment: .leading) {
Text(data.condition.icon)
.font(.system(size: 58))
.minimumScaleFactor(0.85)
.widgetAccentable()
SubtitleText(conditionTitle, textColor: .white)
.lineLimit(1)
.minimumScaleFactor(0.6)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)

WeatherFeeMetric(
label: metricLabel,
value: metric.value(from: data),
valueColor: data.condition.valueColor,
valueSize: 28,
compactLabel: true
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
}
}
2 changes: 2 additions & 0 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ struct MainNavView: View {
BlocksWidgetPreviewView()
case .facts:
FactsWidgetPreviewView()
case .weather:
WeatherWidgetPreviewView()
default:
WidgetDetailView(id: widgetType)
}
Expand Down
Loading
Loading