Skip to content

Commit 5790a29

Browse files
author
Noman R
committed
feat: watch-only security screen with informative description and learn more link
1 parent c429548 commit 5790a29

File tree

9 files changed

+233
-79
lines changed

9 files changed

+233
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
99
- Asset account details and list screens to view account-specific asset transactions and balances
1010
- Load more transactions functionality in asset account details screen
1111
- Asset account details screen with menu actions (rename, watch-only, archive) for individual accounts
12+
- Watch-only security screen with informative description and learn more link
1213

1314
#### Fixed
1415

common/src/commonMain/composeResources/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,7 @@
750750
<string name="id_log_in_to_your_multisig_shield">Log in to your Multisig Shield watch-only wallet with a username and password. You can create your watch-only credentials after logging into your Multisig Shield wallet.</string>
751751
<string name="id_log_in_using_mnemonic">Log in using mnemonic</string>
752752
<string name="id_log_in_via_watchonly_to_receive">Log in via Watch-only to receive funds and check balance.</string>
753+
<string name="id_watchonly_description">In a watch-only wallet, your private keys remain offline for maximum security. This allows you to safely monitor your balance, history and create addresses on the go. To send, transactions are signed with your offline keys. Learn more.</string>
753754
<string name="id_log_out">Log out</string>
754755
<string name="id_logged_in_wallets">Logged in wallets</string>
755756
<string name="id_logging_in">Logging in…</string>

common/src/commonMain/kotlin/com/blockstream/common/Constants.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,6 @@ object Urls {
5858
"https://help.blockstream.com/hc/en-us/articles/4406185830041-Why-is-my-Blockstream-Jade-not-connecting-over-Bluetooth-"
5959
const val LEDGER_SUPPORTED_ASSETS = "https://docs.blockstream.com/green/hww/hww-index.html#ledger-supported-assets"
6060
const val BLUETOOTH_PERMISSIONS = "https://developer.android.com/guide/topics/connectivity/bluetooth/permissions"
61+
const val SECURITY_WATCH_ONLY = "https://blkstrm.com/watchonly"
62+
6163
}

compose/src/commonMain/kotlin/com/blockstream/compose/extensions/Text.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import androidx.compose.ui.geometry.Size
1212
import androidx.compose.ui.graphics.Color
1313
import androidx.compose.ui.graphics.drawscope.withTransform
1414
import androidx.compose.ui.text.AnnotatedString
15+
import androidx.compose.ui.text.LinkAnnotation
1516
import androidx.compose.ui.text.SpanStyle
1617
import androidx.compose.ui.text.TextLayoutResult
18+
import androidx.compose.ui.text.TextLinkStyles
1719
import androidx.compose.ui.text.TextStyle
1820
import androidx.compose.ui.text.buildAnnotatedString
1921
import androidx.compose.ui.text.drawText
@@ -27,6 +29,40 @@ import com.blockstream.compose.theme.md_theme_primary
2729
import com.blockstream.compose.theme.textHigh
2830
import kotlin.math.sqrt
2931

32+
@Composable
33+
fun linkText(
34+
text: String,
35+
linkTexts: List<Pair<String, (() -> Unit)>>,
36+
baseColor: Color = textHigh,
37+
color: Color = md_theme_primary
38+
): AnnotatedString {
39+
return remember(text, linkTexts) {
40+
buildAnnotatedString {
41+
withStyle(style = SpanStyle(color = baseColor)) {
42+
append(text)
43+
}
44+
45+
linkTexts.onEachIndexed { index, coloredText ->
46+
val start = text.lowercase().indexOf(coloredText.first.lowercase())
47+
if (start != -1) {
48+
49+
addLink(
50+
LinkAnnotation.Clickable(
51+
tag = "RichTextLink",
52+
styles = TextLinkStyles(style = SpanStyle(color = color)),
53+
linkInteractionListener = {
54+
coloredText.second.invoke()
55+
}
56+
),
57+
start = start,
58+
end = start + coloredText.first.length
59+
)
60+
}
61+
}
62+
}
63+
}
64+
}
65+
3066
@Composable
3167
fun colorText(
3268
text: String,

compose/src/commonMain/kotlin/com/blockstream/compose/screens/HomeScreen.kt

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import androidx.compose.foundation.layout.height
1212
import androidx.compose.foundation.layout.padding
1313
import androidx.compose.foundation.layout.size
1414
import androidx.compose.foundation.rememberScrollState
15-
import androidx.compose.foundation.text.ClickableText
1615
import androidx.compose.foundation.verticalScroll
1716
import androidx.compose.material3.Text
1817
import androidx.compose.runtime.Composable
@@ -46,7 +45,7 @@ import com.blockstream.compose.components.GreenButtonType
4645
import com.blockstream.compose.components.Promo
4746
import com.blockstream.compose.components.Rive
4847
import com.blockstream.compose.components.RiveAnimation
49-
import com.blockstream.compose.extensions.colorText
48+
import com.blockstream.compose.extensions.linkText
5049
import com.blockstream.compose.theme.bodyLarge
5150
import com.blockstream.compose.theme.bodyMedium
5251
import com.blockstream.compose.theme.displayLarge
@@ -139,30 +138,23 @@ fun HomeScreen(
139138
viewModel.postEvent(HomeViewModel.LocalEvents.ConnectJade)
140139
}
141140

142-
val annotatedString = colorText(
141+
val annotatedString = linkText(
143142
text = stringResource(Res.string.id_by_using_blockstream_app_you_agree),
144-
coloredTexts = listOf(
145-
Res.string.id_terms_of_service, Res.string.id_privacy_policy
146-
).map { stringResource(it) })
143+
linkTexts = listOf(
144+
stringResource(Res.string.id_terms_of_service) to {
145+
viewModel.postEvent(HomeViewModel.LocalEvents.ClickTermsOfService())
146+
}, stringResource(Res.string.id_privacy_policy) to {
147+
viewModel.postEvent(HomeViewModel.LocalEvents.ClickPrivacyPolicy())
148+
}
149+
)
150+
)
147151

148-
ClickableText(
152+
Text(
149153
text = annotatedString,
150154
modifier = Modifier.padding(top = 14.dp)
151155
.padding(horizontal = 16.dp),
152-
style = bodyMedium.copy(textAlign = TextAlign.Center),
153-
onClick = {
154-
annotatedString.getStringAnnotations(
155-
"Index", start = it, end = it
156-
).firstOrNull()?.item?.toIntOrNull()?.also { index ->
157-
(if (index == 0) {
158-
HomeViewModel.LocalEvents.ClickTermsOfService()
159-
} else {
160-
HomeViewModel.LocalEvents.ClickPrivacyPolicy()
161-
}).also { event ->
162-
viewModel.postEvent(event)
163-
}
164-
}
165-
})
156+
style = bodyMedium.copy(textAlign = TextAlign.Center)
157+
)
166158
}
167159
}
168160

compose/src/commonMain/kotlin/com/blockstream/compose/screens/overview/SecurityScreen.kt

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ import androidx.compose.runtime.setValue
1919
import androidx.compose.ui.Alignment
2020
import androidx.compose.ui.Modifier
2121
import androidx.compose.ui.graphics.vector.ImageVector
22+
import androidx.compose.ui.text.SpanStyle
2223
import androidx.compose.ui.unit.dp
24+
import androidx.compose.ui.unit.sp
2325
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2426
import blockstream_green.common.generated.resources.Res
2527
import blockstream_green.common.generated.resources.id_biometrics
@@ -28,16 +30,20 @@ import blockstream_green.common.generated.resources.id_connect_hardware_wallet
2830
import blockstream_green.common.generated.resources.id_firmware_update
2931
import blockstream_green.common.generated.resources.id_genuine_check
3032
import blockstream_green.common.generated.resources.id_hardware
33+
import blockstream_green.common.generated.resources.id_learn_more
3134
import blockstream_green.common.generated.resources.id_mobile
3235
import blockstream_green.common.generated.resources.id_pin
3336
import blockstream_green.common.generated.resources.id_recovery
3437
import blockstream_green.common.generated.resources.id_recovery_phrase
3538
import blockstream_green.common.generated.resources.id_security_level_
3639
import blockstream_green.common.generated.resources.id_unlock_method
40+
import blockstream_green.common.generated.resources.id_watchonly
41+
import blockstream_green.common.generated.resources.id_watchonly_description
3742
import blockstream_green.common.generated.resources.id_your_device
3843
import blockstream_green.common.generated.resources.id_your_jade
3944
import com.adamglin.PhosphorIcons
4045
import com.adamglin.phosphoricons.Regular
46+
import com.adamglin.phosphoricons.regular.Binoculars
4147
import com.adamglin.phosphoricons.regular.CaretRight
4248
import com.adamglin.phosphoricons.regular.Cpu
4349
import com.adamglin.phosphoricons.regular.Fingerprint
@@ -46,12 +52,14 @@ import com.adamglin.phosphoricons.regular.Password
4652
import com.adamglin.phosphoricons.regular.PlugsConnected
4753
import com.adamglin.phosphoricons.regular.SealCheck
4854
import com.adamglin.phosphoricons.regular.ShieldChevron
55+
import com.blockstream.common.Urls
4956
import com.blockstream.common.data.AlertType
5057
import com.blockstream.common.data.CredentialType
5158
import com.blockstream.common.data.GreenWallet
5259
import com.blockstream.common.data.MenuEntry
5360
import com.blockstream.common.data.MenuEntryList
5461
import com.blockstream.common.data.SetupArgs
62+
import com.blockstream.common.events.Events
5563
import com.blockstream.common.models.overview.SecurityViewModel
5664
import com.blockstream.common.models.overview.SecurityViewModel.LocalSideEffects
5765
import com.blockstream.common.models.overview.SecurityViewModelAbstract
@@ -67,14 +75,20 @@ import com.blockstream.compose.components.GreenCard
6775
import com.blockstream.compose.components.ListHeader
6876
import com.blockstream.compose.components.OnProgressStyle
6977
import com.blockstream.compose.components.Promo
78+
import com.blockstream.compose.screens.overview.components.WatchOnlyWalletDescription
79+
import com.blockstream.compose.theme.bodyMedium
7080
import com.blockstream.compose.theme.displaySmall
7181
import com.blockstream.compose.theme.green
7282
import com.blockstream.compose.theme.labelLarge
83+
import com.blockstream.compose.theme.md_theme_primary
84+
import com.blockstream.compose.theme.titleMedium
7385
import com.blockstream.compose.theme.titleSmall
7486
import com.blockstream.compose.theme.whiteMedium
7587
import com.blockstream.compose.utils.SetupScreen
7688
import com.blockstream.ui.components.GreenColumn
7789
import com.blockstream.ui.components.GreenRow
90+
import com.blockstream.ui.components.RichSpan
91+
import com.blockstream.ui.components.RichText
7892
import com.blockstream.ui.navigation.LocalInnerPadding
7993
import com.blockstream.ui.navigation.getResult
8094
import com.blockstream.ui.utils.bottom
@@ -139,35 +153,37 @@ fun SecurityScreen(viewModel: SecurityViewModelAbstract) {
139153
.plus(PaddingValues(bottom = 80.dp + 24.dp))
140154
) {
141155

142-
item {
143-
GreenColumn(
144-
horizontalAlignment = Alignment.CenterHorizontally,
145-
modifier = Modifier.fillMaxWidth()
146-
) {
147-
Icon(
148-
imageVector = PhosphorIcons.Regular.ShieldChevron,
149-
contentDescription = null,
150-
modifier = Modifier.size(32.dp)
151-
)
152-
Text(
153-
text = stringResource(
154-
Res.string.id_security_level_,
155-
if (isHardware) "II" else "I"
156-
), color = whiteMedium
157-
)
158-
Text(
159-
text = stringResource(if (isHardware) Res.string.id_hardware else Res.string.id_mobile),
160-
style = displaySmall
161-
)
156+
if (!isWatchOnly) {
157+
item {
158+
GreenColumn(
159+
horizontalAlignment = Alignment.CenterHorizontally,
160+
modifier = Modifier.fillMaxWidth()
161+
) {
162+
Icon(
163+
imageVector = PhosphorIcons.Regular.ShieldChevron,
164+
contentDescription = null,
165+
modifier = Modifier.size(32.dp)
166+
)
167+
Text(
168+
text = stringResource(
169+
Res.string.id_security_level_,
170+
if (isHardware) "II" else "I"
171+
), color = whiteMedium
172+
)
173+
Text(
174+
text = stringResource(if (isHardware) Res.string.id_hardware else Res.string.id_mobile),
175+
style = displaySmall
176+
)
162177

163-
if (!isHardware) {
164-
GreenButton(
165-
text = stringResource(Res.string.id_compare_security_levels),
166-
type = GreenButtonType.OUTLINE,
167-
size = GreenButtonSize.BIG,
168-
color = GreenButtonColor.GREENER
169-
) {
170-
viewModel.postEvent(NavigateDestinations.SecurityLevel(greenWallet = viewModel.greenWallet))
178+
if (!isHardware) {
179+
GreenButton(
180+
text = stringResource(Res.string.id_compare_security_levels),
181+
type = GreenButtonType.OUTLINE,
182+
size = GreenButtonSize.BIG,
183+
color = GreenButtonColor.GREENER
184+
) {
185+
viewModel.postEvent(NavigateDestinations.SecurityLevel(greenWallet = viewModel.greenWallet))
186+
}
171187
}
172188
}
173189
}
@@ -237,7 +253,26 @@ fun SecurityScreen(viewModel: SecurityViewModelAbstract) {
237253
}
238254

239255
} else if (isWatchOnly) {
256+
item {
257+
GreenColumn(
258+
horizontalAlignment = Alignment.CenterHorizontally,
259+
modifier = Modifier.fillMaxWidth()
260+
) {
261+
Icon(
262+
imageVector = PhosphorIcons.Regular.Binoculars,
263+
contentDescription = null,
264+
modifier = Modifier.size(32.dp)
265+
)
266+
267+
Text(
268+
text = stringResource(Res.string.id_watchonly),
269+
style = titleMedium,
270+
modifier = Modifier.padding(top = 8.dp)
271+
)
240272

273+
WatchOnlyWalletDescription { viewModel.postEvent(Events.OpenBrowser(Urls.SECURITY_WATCH_ONLY)) }
274+
}
275+
}
241276
} else {
242277

243278
if (showRecoveryConfirmation) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.blockstream.compose.screens.overview.components
2+
3+
import androidx.compose.foundation.layout.padding
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.Modifier
6+
import androidx.compose.ui.text.SpanStyle
7+
import androidx.compose.ui.unit.dp
8+
import androidx.compose.ui.unit.sp
9+
import blockstream_green.common.generated.resources.Res
10+
import blockstream_green.common.generated.resources.id_learn_more
11+
import blockstream_green.common.generated.resources.id_watchonly_description
12+
import com.blockstream.compose.theme.bodyMedium
13+
import com.blockstream.compose.theme.md_theme_primary
14+
import com.blockstream.compose.theme.whiteMedium
15+
import com.blockstream.ui.components.RichSpan
16+
import com.blockstream.ui.components.RichText
17+
import org.jetbrains.compose.resources.stringResource
18+
19+
@Composable
20+
fun WatchOnlyWalletDescription(onClickLearnMore: () -> Unit) {
21+
val description = stringResource(Res.string.id_watchonly_description)
22+
val learnMore = stringResource(Res.string.id_learn_more)
23+
24+
RichText(
25+
text = description,
26+
spans = listOf(
27+
RichSpan(
28+
text = learnMore, style = SpanStyle(color = md_theme_primary), onClick = onClickLearnMore
29+
)
30+
),
31+
defaultStyle = bodyMedium.copy(color = whiteMedium, lineHeight = 24.sp),
32+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp)
33+
)
34+
}

0 commit comments

Comments
 (0)