Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
c43ab28
WIP
pwltr Apr 8, 2026
08347c9
WIP
pwltr Apr 8, 2026
9fd90cd
WIP
pwltr May 1, 2026
b17bfa5
Merge branch 'master' into feat/os-widgets
jvsena42 May 5, 2026
23a74cf
feat: OS widgets foundation
jvsena42 May 5, 2026
11477b6
feat: widgets OS foundation
jvsena42 May 5, 2026
e23894b
refactor: extract urls to shared constant
jvsena42 May 5, 2026
95e132a
Merge branch 'master' into feat/os-widgets
jvsena42 May 5, 2026
a683f92
fix: set IPHONEOS_DEPLOYMENT_TARGET to 17.0
jvsena42 May 5, 2026
5a3013b
Merge branch 'master' into feat/os-widgets
jvsena42 May 6, 2026
0252052
feat: set widgetAccentable
jvsena42 May 6, 2026
61cc0ec
Merge branch 'feat/os-widgets' of github.com:synonymdev/bitkit-ios in…
jvsena42 May 6, 2026
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
c3a75ff
fix: pr commens
jvsena42 May 6, 2026
15dc374
fix: collect results in input order instead of completion order
jvsena42 May 6, 2026
5312360
refactor: remove old doc file
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
4e75c6a
Merge branch 'master' into feat/os-widgets
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
c2e2bd3
feat: migrate blocks to v61 and implement OS widget
jvsena42 May 7, 2026
2469d3b
fix: use arrow-up-down for transfer icons
jvsena42 May 7, 2026
442ea60
fix: drop large OS widget support
jvsena42 May 7, 2026
bc590d3
Merge branch 'master' into feat/os-widgets
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
d636551
Merge branch 'feat/headlines-v61' into feat/blocks-v61
jvsena42 May 7, 2026
ab3fde3
feat: price widget v61 (#542)
jvsena42 May 14, 2026
42bc665
feat: redesign headlines widget v61 + OS widget (#546)
jvsena42 May 15, 2026
a2e0e7b
WIP
pwltr May 15, 2026
0cd3101
Merge remote-tracking branch 'origin/feat/os-widgets' into feat/block…
pwltr May 15, 2026
f8f4f6f
fixes
pwltr May 15, 2026
90e68bc
fixes
pwltr May 15, 2026
33431a0
fixes
pwltr May 15, 2026
e3af505
feat: add bitcoin facts widgets
pwltr May 15, 2026
2273408
feat: blocks widget v61 + OS widget (#547)
jvsena42 May 18, 2026
3360699
Merge remote-tracking branch 'origin/feat/os-widgets' into feat/facts…
pwltr May 18, 2026
a27075f
Merge branch 'master' into feat/os-widgets
jvsena42 May 18, 2026
f67211b
Merge pull request #552 from synonymdev/feat/facts-v61
jvsena42 May 18, 2026
bc30a89
fix: restore string
jvsena42 May 18, 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ buildServer.json
# AIs
.ai/
.claude/*.local*
.claude/scheduled_tasks.lock
234 changes: 232 additions & 2 deletions Bitkit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions Bitkit/Assets.xcassets/icons/bitcoin.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "bitcoin.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
174 changes: 78 additions & 96 deletions Bitkit/Components/Widgets/BlocksWidget.swift
Original file line number Diff line number Diff line change
@@ -1,34 +1,16 @@
import SwiftUI

/// Options for configuring the BlocksWidget
struct BlocksWidgetOptions: Codable, Equatable {
var height: Bool = true
var time: Bool = true
var date: Bool = true
var transactionCount: Bool = false
var size: Bool = false
var weight: Bool = false
var difficulty: Bool = false
var hash: Bool = false
var merkleRoot: Bool = false
var showSource: Bool = false
}
// MARK: - Widget

/// A widget that displays Bitcoin block information
/// In-app Bitcoin Blocks widget (v61). Renders the wide layout — used inside the home feed
/// and the wide carousel page on the preview screen.
struct BlocksWidget: View {
/// Configuration options for the widget
var options: BlocksWidgetOptions = .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 block data
@StateObject private var viewModel = BlocksViewModel.shared

/// Initialize the widget
init(
options: BlocksWidgetOptions = BlocksWidgetOptions(),
isEditing: Bool = false,
Expand All @@ -39,96 +21,96 @@ struct BlocksWidget: View {
self.onEditingEnd = onEditingEnd
}

/// Mapping of block data keys to display labels
private let blocksMapping: [String: String] = [
"height": "Block",
"time": "Time",
"date": "Date",
"transactionCount": "Transactions",
"size": "Size",
"weight": "Weight",
"difficulty": "Difficulty",
"hash": "Hash",
"merkleRoot": "Merkle Root",
]

var body: some View {
BaseWidget(
type: .blocks,
isEditing: isEditing,
onEditingEnd: onEditingEnd
) {
VStack(spacing: 0) {
if viewModel.isLoading {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil {
WidgetContentBuilder.errorView(t("widgets__blocks__error"))
} else if let data = viewModel.blockData {
VStack(spacing: 0) {
// Display block data rows based on options
ForEach(getDisplayableData(data), id: \.key) { item in
HStack(spacing: 0) {
HStack {
BodySSBText(item.label, textColor: .textSecondary)
.lineLimit(1)
}
.frame(maxWidth: .infinity, alignment: .leading)

HStack {
BodyMSBText(item.value)
.lineLimit(1)
.truncationMode(.middle)
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.frame(minHeight: 28)
}

if options.showSource {
WidgetContentBuilder.sourceRow(source: "mempool.space")
}
}
}
}
content
}
.onAppear {
.task {
viewModel.startUpdates()
}
}

/// Get displayable data based on current options
private func getDisplayableData(_ data: BlockData) -> [(key: String, label: String, value: String)] {
var items: [(key: String, label: String, value: String)] = []

if options.height {
items.append((key: "height", label: blocksMapping["height"]!, value: data.height))
}
if options.time {
items.append((key: "time", label: blocksMapping["time"]!, value: data.time))
}
if options.date {
items.append((key: "date", label: blocksMapping["date"]!, value: data.date))
@ViewBuilder
private var content: some View {
if viewModel.isLoading && viewModel.blockData == nil {
WidgetContentBuilder.loadingView()
} else if viewModel.error != nil && viewModel.blockData == nil {
WidgetContentBuilder.errorView(t("widgets__blocks__error"))
} else if let data = viewModel.blockData {
BlocksWidgetWideContent(data: data, options: options)
}
if options.transactionCount {
items.append((key: "transactionCount", label: blocksMapping["transactionCount"]!, value: data.transactionCount))
}
if options.size {
items.append((key: "size", label: blocksMapping["size"]!, value: data.size))
}
if options.weight {
items.append((key: "weight", label: blocksMapping["weight"]!, value: data.weight))
}
if options.difficulty {
items.append((key: "difficulty", label: blocksMapping["difficulty"]!, value: data.difficulty))
}
if options.hash {
items.append((key: "hash", label: blocksMapping["hash"]!, value: data.hash))
}
}

// MARK: - Wide layout (in-app + 343-wide carousel page + .systemMedium / .systemLarge OS widget)

struct BlocksWidgetWideContent: View {
let data: CachedBlock
let options: BlocksWidgetOptions

var body: some View {
VStack(alignment: .leading, spacing: 12) {
ForEach(options.enabledFields, id: \.self) { field in
BlocksWidgetWideRow(field: field, value: field.value(from: data))
}
}
if options.merkleRoot {
items.append((key: "merkleRoot", label: blocksMapping["merkleRoot"]!, value: data.merkleRoot))
.frame(maxWidth: .infinity, alignment: .leading)
}
}

private struct BlocksWidgetWideRow: View {
let field: BlocksWidgetField
let value: String

var body: some View {
HStack(alignment: .center, spacing: 8) {
Image(field.iconName)
.resizable()
.renderingMode(.template)
.foregroundColor(.brandAccent)
.frame(width: 20, height: 20)

BodyMText(field.label, textColor: .white80)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)

BodyMSBText(value)
.lineLimit(1)
.truncationMode(.middle)
}
}
}

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

return items
struct BlocksWidgetCompactContent: View {
let data: CachedBlock
let options: BlocksWidgetOptions

var body: some View {
VStack(alignment: .leading, spacing: 16) {
ForEach(options.enabledFields, id: \.self) { field in
HStack(alignment: .center, spacing: 8) {
Image(field.iconName)
.resizable()
.renderingMode(.template)
.foregroundColor(.brandAccent)
.frame(width: 20, height: 20)

BodySSBText(field.value(from: data))
.lineLimit(1)
.truncationMode(.middle)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Color.gray6)
.cornerRadius(16)
}
}

Expand Down
87 changes: 43 additions & 44 deletions Bitkit/Components/Widgets/FactsWidget.swift
Original file line number Diff line number Diff line change
@@ -1,45 +1,17 @@
import SwiftUI

/// Options for configuring the FactsWidget
struct FactsWidgetOptions: Codable, Equatable {
var showSource: Bool = true
}

struct FactsWidget: View {
/// Configuration options for the widget
var options: FactsWidgetOptions = .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 facts data
@StateObject private var viewModel = FactsViewModel.shared

/// Initialize the widget
init(
options: FactsWidgetOptions = FactsWidgetOptions(),
isEditing: Bool = false,
onEditingEnd: (() -> Void)? = nil
) {
self.options = options
self.isEditing = isEditing
self.onEditingEnd = onEditingEnd
}

/// Initialize with a custom view model (for previews)
init(
viewModel: FactsViewModel,
options: FactsWidgetOptions = FactsWidgetOptions(),
isEditing: Bool = false,
onEditingEnd: (() -> Void)? = nil
) {
self.options = options
self.isEditing = isEditing
self.onEditingEnd = onEditingEnd
_viewModel = StateObject(wrappedValue: viewModel)
}

var body: some View {
Expand All @@ -48,30 +20,57 @@ struct FactsWidget: View {
isEditing: isEditing,
onEditingEnd: onEditingEnd
) {
VStack(spacing: 0) {
TitleText(viewModel.fact)
.lineLimit(2)
.frame(maxWidth: .infinity, alignment: .leading)
FactsWidgetWideContent(fact: viewModel.fact)
}
}
}

if options.showSource {
WidgetContentBuilder.sourceRow(source: "synonym.to")
}
}
struct FactsWidgetWideContent: View {
let fact: String

var body: some View {
HStack(alignment: .top, spacing: 32) {
TitleText(fact)
.lineLimit(4)
.frame(maxWidth: .infinity, alignment: .leading)

BitcoinLogo()
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}

struct FactsWidgetCompactContent: View {
let fact: String

var body: some View {
BodyMSBText(fact)
.lineLimit(4)
.frame(maxWidth: .infinity, alignment: .leading)
.minimumScaleFactor(0.85)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.overlay(alignment: .bottomTrailing) {
BitcoinLogo()
}
.padding(16)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.background(Color.gray6)
.cornerRadius(16)
}
}

private struct BitcoinLogo: View {
var body: some View {
Image("bitcoin")
.resizable()
.frame(width: 32, height: 32)
}
}

#Preview {
VStack(spacing: 16) {
FactsWidget()

FactsWidget(
options: FactsWidgetOptions(showSource: false)
)

FactsWidget(
isEditing: true
)
FactsWidget(isEditing: true)
}
.padding()
.background(Color.black)
Expand Down
Loading
Loading