Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
61db06c
feat: port price widgets related screens to figma V61
jvsena42 May 6, 2026
28a550f
fix: spacing and alignment
jvsena42 May 6, 2026
10be29e
feat: hide menu button from nabigation bar
jvsena42 May 6, 2026
90680fe
fix: padding
jvsena42 May 6, 2026
4dfd9a8
fix: remove systemLarge widget option
jvsena42 May 6, 2026
15dc374
fix: collect results in input order instead of completion order
jvsena42 May 6, 2026
fc2aaa6
Merge remote-tracking branch 'origin/feat/os-widgets' into feat/price…
jvsena42 May 6, 2026
600b422
fix: pr comments
jvsena42 May 6, 2026
027da41
fix: pr comments
jvsena42 May 6, 2026
a172129
refactor: simplify doc
jvsena42 May 6, 2026
73eba60
refactor: replace onApper with task
jvsena42 May 6, 2026
d07317f
refactor: replace onChange with task id
jvsena42 May 6, 2026
8b2ec97
refactor: simplify comments
jvsena42 May 6, 2026
6586143
refactor: simplyfy comments
jvsena42 May 6, 2026
fc0dc8e
refactor: simplify comments
jvsena42 May 6, 2026
d01c196
refactor: simplify comments
jvsena42 May 6, 2026
770511e
refactor: simplify comments
jvsena42 May 6, 2026
5f0841f
refactor: remove multi-pair legacy code
jvsena42 May 6, 2026
51c2f41
fix: fallback to os widget options after remove in-app
jvsena42 May 6, 2026
afb421a
fix: make chart height adaptable
jvsena42 May 6, 2026
fdafc5b
Merge branch 'feat/os-widgets' into feat/price-widget-v61
jvsena42 May 6, 2026
5be6014
feat: set backgroud color Gray7
jvsena42 May 6, 2026
b0d05be
Merge branch 'feat/price-widget-v61' of github.com:synonymdev/bitkit-…
jvsena42 May 6, 2026
d60c84c
feat: migrate news widget to design v61 and port OS widget
jvsena42 May 7, 2026
ab5b8d5
fix: push source text to bottom
jvsena42 May 7, 2026
66f82ff
refactor: extract articles url to a shared files
jvsena42 May 7, 2026
78b4dcd
feat: open browser on widget click
jvsena42 May 7, 2026
12fe6e6
doc: changelog entry
jvsena42 May 7, 2026
667e544
fix: small and medium sizes displaying different random url
jvsena42 May 7, 2026
40b0ad7
chore: remove schedule file
jvsena42 May 7, 2026
2e3f81c
fix: replace onAppear with task
jvsena42 May 7, 2026
a25a221
fix: use stable dafe format identifier
jvsena42 May 7, 2026
22dc3e1
Merge branch 'feat/os-widgets' into feat/price-widget-v61
jvsena42 May 7, 2026
6e3ab90
fix: reuse existing text component and remove scale factor
jvsena42 May 7, 2026
b2df71f
fix: display white32 checkmark for unselected item
jvsena42 May 7, 2026
7994460
fix: vertical padding anchored to checkbox image
jvsena42 May 7, 2026
200d2f8
fix: remove the gray bg and custom bg from Navigation bar
jvsena42 May 7, 2026
870d5e8
fix: try to fetch real data for preview
jvsena42 May 7, 2026
d2f4120
refactor: make string keys generic to be reused in the furue implemen…
jvsena42 May 7, 2026
1c8350a
fix: make prevew frame height adaptable
jvsena42 May 7, 2026
779e215
fix: solve conflicts and apply downstream changes
jvsena42 May 7, 2026
ddfd42b
fix: display checkmark for title
jvsena42 May 7, 2026
88843b6
fix: remove app group fallback
jvsena42 May 7, 2026
955e293
Merge remote-tracking branch 'origin/feat/price-widget-v61' into feat…
jvsena42 May 7, 2026
9b221d0
test: widget test ids adjustment
piotr-iohk May 8, 2026
0d98ff5
test: widget test ids adjustment
piotr-iohk May 8, 2026
c1c5929
fixes
pwltr May 14, 2026
f72a5a4
Merge remote-tracking branch 'origin/feat/price-widget-v61' into feat…
pwltr May 14, 2026
f957422
fixes
pwltr May 14, 2026
7d637b6
fixes
pwltr May 14, 2026
5293f75
Merge remote-tracking branch 'origin/feat/os-widgets' into feat/headl…
pwltr May 14, 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
3 changes: 3 additions & 0 deletions Bitkit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,11 @@
Fonts/InterTight-Regular.ttf,
Constants/WidgetEnv.swift,
Fonts/InterTight-SemiBold.ttf,
Models/NewsWidgetData.swift,
Models/NewsWidgetOptions.swift,
Models/PriceWidgetData.swift,
Models/PriceWidgetOptions.swift,
Services/Widgets/NewsHomeScreenWidgetOptionsStore.swift,
Services/Widgets/PriceHomeScreenWidgetOptionsStore.swift,
Styles/Colors.swift,
Styles/Fonts.swift,
Expand Down
116 changes: 77 additions & 39 deletions Bitkit/Components/Widgets/NewsWidget.swift
Original file line number Diff line number Diff line change
@@ -1,27 +1,13 @@
import SwiftUI

/// Options for configuring the NewsWidget
struct NewsWidgetOptions: Codable, Equatable {
var showDate: Bool = true
var showTitle: Bool = true
var showSource: Bool = true
}

/// A widget that displays a news article
/// A widget that displays a news article.
struct NewsWidget: View {
/// Configuration options for the widget
var options: NewsWidgetOptions = .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 news data
@StateObject private var viewModel = NewsViewModel.shared

/// Initialize the widget
init(
options: NewsWidgetOptions = NewsWidgetOptions(),
isEditing: Bool = false,
Expand All @@ -38,40 +24,92 @@ struct NewsWidget: View {
isEditing: isEditing,
onEditingEnd: onEditingEnd
) {
VStack(spacing: 0) {
if viewModel.isLoading {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil {
WidgetContentBuilder.errorView(t("widgets__news__error"))
} else if let data = viewModel.widgetData {
if options.showDate {
BodyMText(data.timeAgo, textColor: .textPrimary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.bottom, 16)
content
.contentShape(Rectangle())
.onTapGesture {
if !isEditing, let data = viewModel.widgetData, let url = URL(string: data.link) {
UIApplication.shared.open(url)
}
}
}
.task {
viewModel.startUpdates()
}
}

if options.showTitle {
TitleText(data.title)
.lineLimit(2)
.frame(maxWidth: .infinity, alignment: .leading)
}
@ViewBuilder
private var content: some View {
if viewModel.isLoading && viewModel.widgetData == nil {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil {
WidgetContentBuilder.errorView(t("widgets__news__error"))
} else if let data = viewModel.widgetData {
NewsWidgetWideContent(data: data, options: options)
}
}
}

// MARK: - Wide layout (in-app + 343-wide carousel page)

struct NewsWidgetWideContent: View {
let data: WidgetData
let options: NewsWidgetOptions

var body: some View {
VStack(alignment: .leading, spacing: 16) {
if options.showTitle {
TitleText(data.title)
.lineLimit(4)
.frame(maxWidth: .infinity, alignment: .leading)
}

if options.showSource || options.showDate {
HStack(alignment: .center, spacing: 8) {
if options.showSource {
WidgetContentBuilder.sourceRow(source: data.publisher)
BodySSBText(data.publisher, textColor: .brandAccent)
.lineLimit(1)
}
Spacer(minLength: 0)
if options.showDate {
BodySSBText(data.timeAgo, textColor: .textSecondary)
.lineLimit(1)
}
}
.frame(maxWidth: .infinity)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}

// MARK: - Compact layout (small carousel preview + 163×192 OS widget)

struct NewsWidgetCompactContent: View {
let data: WidgetData
let options: NewsWidgetOptions

var body: some View {
VStack(alignment: .leading, spacing: 0) {
if options.showTitle {
TitleText(data.title)
.lineLimit(4)
.frame(maxWidth: .infinity, alignment: .leading)
}
.contentShape(Rectangle())
.onTapGesture {
if !isEditing, let data = viewModel.widgetData, let url = URL(string: data.link) {
UIApplication.shared.open(url)

Spacer(minLength: 8)

if options.showDate {
HStack {
Spacer(minLength: 0)
BodySSBText(data.timeAgo, textColor: .textSecondary)
.lineLimit(1)
}
}
}
.onAppear {
viewModel.startUpdates()
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Color.gray6)
.cornerRadius(16)
}
}

Expand Down
2 changes: 2 additions & 0 deletions Bitkit/Constants/WidgetEnv.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ import Foundation
/// because it depends on framework types that aren't linked into the widget extension.
enum WidgetEnv {
static let priceFeedBaseUrl = "https://feeds.synonym.to/price-feed/api"
static let newsFeedBaseUrl = "https://feeds.synonym.to/news-feed/api"
static let newsFeedArticlesUrl = "\(newsFeedBaseUrl)/articles"
}
13 changes: 11 additions & 2 deletions Bitkit/MainNavView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ struct MainNavView: View {
Task {
Logger.info("Received deeplink: \(url.absoluteString)")

// Web URLs from widgets (e.g. news article tap) bypass payment handling
if let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" {
await UIApplication.shared.open(url)
return
}

if let callback = PubkyRingAuthCallback.parse(url: url) {
let handlingResult = await pubkyProfile.handleAuthCallback(callback)

Expand Down Expand Up @@ -432,9 +438,12 @@ struct MainNavView: View {
case .widgetsIntro: WidgetsIntroView()
case .widgetsList: WidgetsListView()
case let .widgetDetail(widgetType):
if widgetType == .price {
switch widgetType {
case .price:
PriceWidgetPreviewView()
} else {
case .news:
NewsWidgetPreviewView()
default:
WidgetDetailView(id: widgetType)
}
case let .widgetEdit(widgetType): WidgetEditView(id: widgetType)
Expand Down
40 changes: 40 additions & 0 deletions Bitkit/Models/NewsWidgetData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import Foundation

/// Persistable representation of a news article shared between the main app and the widget extension via App Group.
struct CachedNewsArticle: Codable, Equatable {
let title: String
let publisher: String
let link: String
let publishedDate: String
let publishedEpoch: Int
}

/// Cache reader/writer used by both the main app and the widget extension.
enum NewsWidgetCache {
static let appGroupSuiteName = "group.bitkit"
private static let topArticlesKey = "news_widget_top_articles_v1"
private static let legacyStandardKey = "news_widget_cache"

private static func defaults() -> UserDefaults {
UserDefaults(suiteName: appGroupSuiteName) ?? .standard
}

static func saveTop(_ articles: [CachedNewsArticle]) {
guard let encoded = try? JSONEncoder().encode(articles) else { return }
defaults().set(encoded, forKey: topArticlesKey)
}

static func loadTop() -> [CachedNewsArticle] {
guard let data = defaults().data(forKey: topArticlesKey),
let decoded = try? JSONDecoder().decode([CachedNewsArticle].self, from: data)
else {
return []
}
return decoded
}

/// One-time cleanup of the pre-App-Group single-`WidgetData` cache.
static func legacyDropStandardSuiteCache() {
UserDefaults.standard.removeObject(forKey: legacyStandardKey)
}
}
14 changes: 14 additions & 0 deletions Bitkit/Models/NewsWidgetOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Foundation

/// Options for configuring the in-app and home-screen news widgets (shared via App Group).
struct NewsWidgetOptions: Codable, Equatable {
var showDate: Bool = true
var showTitle: Bool = true
var showSource: Bool = true

init(showDate: Bool = true, showTitle: Bool = true, showSource: Bool = true) {
self.showDate = showDate
self.showTitle = showTitle
self.showSource = showSource
}
}
2 changes: 1 addition & 1 deletion Bitkit/Resources/Localization/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1382,7 +1382,6 @@
"widgets__widget__edit" = "Widget Feed";
"widgets__widget__edit_default" = "Default";
"widgets__widget__edit_custom" = "Custom";
"widgets__widget__edit_description" = "Please select which fields you want to display in the {name} widget.";
"widgets__widget__source" = "Source";
"widgets__add" = "Add Widget";
"widgets__list__button" = "Enable In Settings";
Expand All @@ -1404,6 +1403,7 @@
"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";
"widgets__news__content_header" = "Content";
"widgets__blocks__name" = "Bitcoin Blocks";
"widgets__blocks__description" = "Examine various statistics on newly mined Bitcoin Blocks.";
"widgets__blocks__error" = "Couldn\'t get blocks data";
Expand Down
36 changes: 36 additions & 0 deletions Bitkit/Services/Widgets/NewsHomeScreenWidgetOptionsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import WidgetKit

/// Mirrors in-app news widget options into the App Group so the WidgetKit extension can read them,
/// and centralizes the WidgetKit reload trigger for the news home-screen widget.
enum NewsHomeScreenWidgetOptionsStore {
/// WidgetKit `kind` for the home-screen news widget (must match `BitkitNewsWidget`).
static let newsHomeScreenWidgetKind = "BitkitNewsWidget"

private static let suiteName = "group.bitkit"
private static let key = "home_screen_news_widget_options_v1"

static func save(_ options: NewsWidgetOptions) {
guard let defaults = UserDefaults(suiteName: suiteName),
let data = try? JSONEncoder().encode(options)
else { return }
defaults.set(data, forKey: key)
}

static func load() -> NewsWidgetOptions {
guard let defaults = UserDefaults(suiteName: suiteName),
let data = defaults.data(forKey: key),
let options = try? JSONDecoder().decode(NewsWidgetOptions.self, from: data)
else {
return NewsWidgetOptions()
}
return options
}

/// Call after updating options or cache so the home-screen widget timeline refreshes.
/// No-op when running inside the widget extension itself (`appex`).
static func reloadHomeScreenWidgetIfNeeded() {
guard Bundle.main.bundleURL.pathExtension != "appex" else { return }
WidgetCenter.shared.reloadTimelines(ofKind: newsHomeScreenWidgetKind)
}
}
Loading
Loading