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 Shared
import SwiftUI import SwiftUI
@@ -103,7 +104,7 @@ struct ContentView: View {
onChange: { viewModel.onBtcAmountChanged($0) } onChange: { viewModel.onBtcAmountChanged($0) }
) )
if state.exceedsMaxSupply { if state.exceedsMaxSupply {
Text(IosLocalizationKt.localizedString(resource: MR.strings.shared.exceeds_max_supply)) exceedsMaxSupplyText
.font(.caption) .font(.caption)
.foregroundColor(.red) .foregroundColor(.red)
} }
@@ -167,6 +168,31 @@ struct ContentView: View {
onToggle: { viewModel.onFiatCurrencyToggled($0) } 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 @ViewBuilder
@@ -46,13 +46,22 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import xyz.tyiu.satsprice.CurrencyInfo import xyz.tyiu.satsprice.CurrencyInfo
import xyz.tyiu.satsprice.domain.CurrencyConverter
import xyz.tyiu.satsprice.domain.formatAmount
import xyz.tyiu.satsprice.shared.MR import xyz.tyiu.satsprice.shared.MR
private val SectionColors private val SectionColors
@@ -207,8 +216,43 @@ fun PriceScreen(
) )
if (exceedsMaxSupply) { 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(
text = stringResource(MR.strings.exceeds_max_supply), text = annotatedWarning,
color = MaterialTheme.colorScheme.error, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),