Make the max-supply warning's BTC amount a link on every platform

Tapping "21,000,000 BTC" in the exceeds-max-supply warning now sets the BTC
field to that value, on both Compose (Android/Desktop/Web) and the native
SwiftUI screen (iOS/macOS) — Compose UI isn't rendered on iOS/macOS at all,
so that side needed its own AttributedString-based link.

The link is styled to match the surrounding warning text: Compose's
LinkAnnotation otherwise falls back to the theme's accent color unless every
interaction state's style is set explicitly, and SwiftUI renders `.link`
runs in the accent color regardless of the Text's own foregroundColor
modifier unless it's set directly on the link's AttributedString range.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpjMKWGoiT5aJBzhxvXkwp
This commit is contained in:
2026-09-08 10:30:10 +03:00
co-authored by Claude Sonnet 5
parent b77455bfc8
commit f35fb05e7c
2 changed files with 72 additions and 2 deletions
+27 -1
View File
@@ -1,3 +1,4 @@
import Foundation
import Shared
import SwiftUI
@@ -103,7 +104,7 @@ struct ContentView: View {
onChange: { viewModel.onBtcAmountChanged($0) }
)
if state.exceedsMaxSupply {
Text(IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply))
exceedsMaxSupplyText
.font(.caption)
.foregroundColor(.red)
}
@@ -167,6 +168,31 @@ struct ContentView: View {
onToggle: { viewModel.onFiatCurrencyToggled($0) }
)
}
.environment(\.openURL, OpenURLAction { url in
guard url == Self.maxSupplyLinkURL else { return .systemAction }
viewModel.onBtcAmountChanged(Self.maxSupplyBtcAmount)
return .handled
})
}
// Matches the literal text of MR.strings.exceeds_max_supply so it can be turned into a link;
// falls back to plain text if that ever drifts apart. `maxSupplyBtcAmount` is the sanitized
// digit-only form CurrencyConverter.MAX_BTC_SUPPLY formats to, mirroring the shared Compose UI.
private static let maxSupplyLinkText = "21,000,000 BTC"
private static let maxSupplyBtcAmount = "21000000"
private static let maxSupplyLinkURL = URL(string: "satsprice://set-max-supply")!
private var exceedsMaxSupplyText: Text {
let warning = IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply)
var attributed = AttributedString(warning)
if let range = attributed.range(of: Self.maxSupplyLinkText) {
attributed[range].link = Self.maxSupplyLinkURL
attributed[range].underlineStyle = .single
// SwiftUI renders `.link` runs in the accent color regardless of the Text's own
// .foregroundColor modifier, so it has to be set directly on the link's range.
attributed[range].foregroundColor = .red
}
return Text(attributed)
}
@ViewBuilder
@@ -46,13 +46,22 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import dev.icerock.moko.resources.compose.stringResource
import xyz.tyiu.satsprice.CurrencyInfo
import xyz.tyiu.satsprice.domain.CurrencyConverter
import xyz.tyiu.satsprice.domain.formatAmount
import xyz.tyiu.satsprice.shared.MR
private val SectionColors
@@ -207,8 +216,43 @@ fun PriceScreen(
)
if (exceedsMaxSupply) {
val warning = stringResource(MR.strings.exceeds_max_supply)
// Matches the literal text of MR.strings.exceeds_max_supply so it can be
// turned into a link; falls back to plain text if that ever drifts apart.
val maxSupplyText = "21,000,000 BTC"
val linkStart = warning.indexOf(maxSupplyText)
val annotatedWarning = if (linkStart < 0) {
AnnotatedString(warning)
} else {
// Set explicitly (and identically) for every interaction state, since
// LinkAnnotation otherwise renders in the theme's link/accent color
// rather than inheriting the surrounding warning text's color.
val linkStyle = SpanStyle(
color = MaterialTheme.colorScheme.error,
textDecoration = TextDecoration.Underline,
)
buildAnnotatedString {
append(warning.substring(0, linkStart))
withLink(
LinkAnnotation.Clickable(
tag = "max_supply",
styles = TextLinkStyles(
style = linkStyle,
focusedStyle = linkStyle,
hoveredStyle = linkStyle,
pressedStyle = linkStyle,
),
) {
viewModel.onBtcAmountChanged(formatAmount(CurrencyConverter.MAX_BTC_SUPPLY, 0))
},
) {
append(maxSupplyText)
}
append(warning.substring(linkStart + maxSupplyText.length))
}
}
Text(
text = stringResource(MR.strings.exceeds_max_supply),
text = annotatedWarning,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp),