Skip to content

Commit 4e83dd9

Browse files
committed
fix: polish pubky profile flow
1 parent 529296a commit 4e83dd9

12 files changed

Lines changed: 336 additions & 114 deletions

File tree

app/src/main/java/to/bitkit/di/HttpModule.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
@file:Suppress("ImportOrdering")
2-
31
package to.bitkit.di
42

53
import dagger.Module

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -173,11 +173,16 @@ internal object Env {
173173
}
174174

175175
val homegateUrl: String
176-
get() = homegateUrlFor(
177-
network = network,
178-
isLocalE2eBackend = isLocalE2eBackend,
179-
e2eHomegateUrl = e2eHomegateUrl,
180-
)
176+
get() {
177+
if (isLocalE2eBackend) {
178+
return e2eHomegateUrl
179+
}
180+
181+
return when (network) {
182+
Network.BITCOIN -> "https://homegate.pubky.app"
183+
else -> "https://homegate.staging.pubky.app"
184+
}
185+
}
181186

182187
val profilePath: String
183188
get() = "/pub/$pubkyDomain/profile.json"
@@ -238,21 +243,6 @@ internal object Env {
238243
// endregion
239244
}
240245

241-
internal fun homegateUrlFor(
242-
network: Network,
243-
isLocalE2eBackend: Boolean,
244-
e2eHomegateUrl: String,
245-
): String {
246-
if (isLocalE2eBackend) {
247-
return e2eHomegateUrl
248-
}
249-
250-
return when (network) {
251-
Network.BITCOIN -> "https://homegate.pubky.app"
252-
else -> "https://homegate.staging.pubky.app"
253-
}
254-
}
255-
256246
@Suppress("ConstPropertyName")
257247
object Defaults {
258248
/** Recommended transaction base fee in sats */

app/src/main/java/to/bitkit/repositories/PubkyRepo.kt

Lines changed: 87 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ class PubkyRepo @Inject constructor(
199199
val pk = pubkyService.importSession(sessionSecret)
200200

201201
runCatching { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) }
202+
runCatching { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) }
202203
keychain.saveString(Keychain.Key.PAYKIT_SESSION.name, sessionSecret)
203204

204205
pk
@@ -342,10 +343,21 @@ class PubkyRepo @Inject constructor(
342343

343344
suspend fun uploadAvatar(imageBytes: ByteArray): Result<String> = runCatching {
344345
withContext(ioDispatcher) {
345-
val secretKeyHex = requireNotNull(keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name)) {
346-
"No secret key available"
346+
val publicKey = requireNotNull(_publicKey.value) {
347+
"No public key available"
348+
}
349+
val secretKeyHex = managedSecretKeyFor(publicKey)
350+
if (secretKeyHex != null) {
351+
return@withContext uploadAvatar(imageBytes, secretKeyHex).getOrThrow()
352+
}
353+
354+
val session = requireNotNull(keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)) {
355+
"No session available"
347356
}
348-
uploadAvatar(imageBytes, secretKeyHex).getOrThrow()
357+
val compressed = compressAvatar(imageBytes)
358+
val path = "${Env.blobsBasePath}${System.currentTimeMillis()}.jpg"
359+
pubkyService.sessionPut(session, path, compressed)
360+
"$PUBKY_SCHEME${publicKey.removePrefix(PUBKY_PREFIX)}$path"
349361
}
350362
}
351363

@@ -441,7 +453,18 @@ class PubkyRepo @Inject constructor(
441453
val session = keychain.loadString(Keychain.Key.PAYKIT_SESSION.name)
442454
?: return@withContext emptyList()
443455

444-
val contactPaths = pubkyService.sessionList(session, Env.contactsBasePath)
456+
val contactPaths = runCatching {
457+
pubkyService.sessionList(session, Env.contactsBasePath)
458+
}.getOrElse {
459+
if (it.isMissingPubkyDirectory()) {
460+
Logger.debug(
461+
"Treating missing contacts directory as empty for '$pk'",
462+
context = TAG,
463+
)
464+
return@withContext emptyList()
465+
}
466+
throw it
467+
}
445468
val strippedOwnerKey = pk.removePrefix(PUBKY_PREFIX)
446469

447470
coroutineScope {
@@ -638,21 +661,20 @@ class PubkyRepo @Inject constructor(
638661

639662
// region Sign out
640663

641-
suspend fun signOut(): Result<Unit> = runCatching {
642-
withContext(ioDispatcher) { pubkyService.signOut() }
643-
}.recoverCatching {
644-
Logger.warn("Server sign out failed, forcing local sign out", it, context = TAG)
645-
withContext(ioDispatcher) { pubkyService.forceSignOut() }
646-
}.also {
647-
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) } }
648-
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) } }
649-
evictPubkyImages()
650-
runCatching { withContext(ioDispatcher) { pubkyStore.reset() } }
651-
_publicKey.update { null }
652-
_profile.update { null }
653-
_contacts.update { emptyList() }
654-
clearPendingImport()
655-
_authState.update { PubkyAuthState.Idle }
664+
suspend fun signOut(): Result<Unit> {
665+
val result = runCatching {
666+
withContext(ioDispatcher) { pubkyService.signOut() }
667+
}.recoverCatching {
668+
Logger.warn("Forcing local sign out after server sign out failed", it, context = TAG)
669+
withContext(ioDispatcher) { pubkyService.forceSignOut() }
670+
}
671+
672+
clearLocalState()
673+
return result
674+
}
675+
676+
suspend fun wipeLocalState() {
677+
clearLocalState()
656678
}
657679

658680
// endregion
@@ -678,8 +700,54 @@ class PubkyRepo @Inject constructor(
678700
}
679701
}
680702

703+
private suspend fun managedSecretKeyFor(publicKey: String): String? = withContext(ioDispatcher) {
704+
val secretKeyHex = keychain.loadString(Keychain.Key.PUBKY_SECRET_KEY.name)
705+
?: return@withContext null
706+
707+
val derivedPublicKey = runCatching {
708+
pubkyService.publicKeyFromSecret(secretKeyHex).ensurePubkyPrefix()
709+
}.onFailure {
710+
Logger.warn("Ignoring invalid managed secret key for '$publicKey'", it, context = TAG)
711+
}.getOrNull()
712+
713+
if (derivedPublicKey == publicKey) {
714+
return@withContext secretKeyHex
715+
}
716+
717+
if (derivedPublicKey != null) {
718+
Logger.warn("Ignoring stale managed secret key for '$publicKey'", context = TAG)
719+
}
720+
runCatching { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) }
721+
null
722+
}
723+
724+
private suspend fun clearLocalState() {
725+
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) } }
726+
runCatching { withContext(ioDispatcher) { keychain.delete(Keychain.Key.PUBKY_SECRET_KEY.name) } }
727+
evictPubkyImages()
728+
runCatching { withContext(ioDispatcher) { pubkyStore.reset() } }
729+
_publicKey.update { null }
730+
_profile.update { null }
731+
_contacts.update { emptyList() }
732+
clearPendingImport()
733+
_sessionRestorationFailed.update { false }
734+
_authState.update { PubkyAuthState.Idle }
735+
}
736+
681737
private fun String.ensurePubkyPrefix(): String =
682738
if (startsWith(PUBKY_PREFIX)) this else "$PUBKY_PREFIX$this"
683739

740+
private fun Throwable.isMissingPubkyDirectory(): Boolean {
741+
val fullMessage = buildString {
742+
append(message.orEmpty())
743+
cause?.message?.takeIf { it.isNotBlank() }?.let {
744+
append(" ")
745+
append(it)
746+
}
747+
}
748+
return fullMessage.contains("directory not found", ignoreCase = true) ||
749+
(fullMessage.contains("404") && fullMessage.contains("not found", ignoreCase = true))
750+
}
751+
684752
// endregion
685753
}

app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column
55
import androidx.compose.foundation.layout.ExperimentalLayoutApi
66
import androidx.compose.foundation.layout.FlowRow
77
import androidx.compose.foundation.layout.fillMaxWidth
8+
import androidx.compose.foundation.layout.imePadding
9+
import androidx.compose.foundation.layout.navigationBarsPadding
810
import androidx.compose.foundation.layout.padding
911
import androidx.compose.material3.ExperimentalMaterial3Api
1012
import androidx.compose.material3.Icon
@@ -24,6 +26,8 @@ import kotlinx.collections.immutable.ImmutableList
2426
import kotlinx.collections.immutable.persistentListOf
2527
import to.bitkit.R
2628
import to.bitkit.ui.scaffold.SheetTopBar
29+
import to.bitkit.ui.shared.modifiers.sheetHeight
30+
import to.bitkit.ui.shared.util.gradientBackground
2731
import to.bitkit.ui.theme.AppThemeSurface
2832
import to.bitkit.ui.theme.Colors
2933

@@ -46,6 +50,7 @@ fun AddLinkSheet(
4650
BottomSheet(
4751
onDismissRequest = onDismiss,
4852
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
53+
modifier = Modifier.imePadding()
4954
) {
5055
if (showSuggestions) {
5156
SuggestionsContent(
@@ -81,7 +86,13 @@ private fun LinkFormContent(
8186
onSave: () -> Unit,
8287
isSaveEnabled: Boolean,
8388
) {
84-
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
89+
Column(
90+
modifier = Modifier
91+
.sheetHeight(isModal = true)
92+
.gradientBackground()
93+
.navigationBarsPadding()
94+
.padding(horizontal = 16.dp),
95+
) {
8596
SheetTopBar(titleText = stringResource(R.string.profile__add_link))
8697
VerticalSpacer(16.dp)
8798
Text13Up(text = stringResource(R.string.profile__add_link_label))
@@ -92,7 +103,7 @@ private fun LinkFormContent(
92103
placeholder = stringResource(R.string.profile__add_link_label_placeholder),
93104
trailingIcon = { SuggestionsButton(onClick = onShowSuggestions) },
94105
singleLine = true,
95-
modifier = Modifier.fillMaxWidth(),
106+
modifier = Modifier.fillMaxWidth()
96107
)
97108
VerticalSpacer(16.dp)
98109
Text13Up(text = stringResource(R.string.profile__add_link_url))
@@ -102,7 +113,7 @@ private fun LinkFormContent(
102113
onValueChange = onUrlChange,
103114
placeholder = stringResource(R.string.profile__add_link_url_placeholder),
104115
singleLine = true,
105-
modifier = Modifier.fillMaxWidth(),
116+
modifier = Modifier.fillMaxWidth()
106117
)
107118
VerticalSpacer(8.dp)
108119
BodyS(
@@ -127,7 +138,13 @@ internal fun SuggestionsContent(
127138
onSelect: (String) -> Unit,
128139
onBack: () -> Unit,
129140
) {
130-
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
141+
Column(
142+
modifier = Modifier
143+
.sheetHeight(isModal = true)
144+
.gradientBackground()
145+
.navigationBarsPadding()
146+
.padding(horizontal = 16.dp),
147+
) {
131148
SheetTopBar(titleText = title, onBack = onBack)
132149
VerticalSpacer(16.dp)
133150
FlowRow(
@@ -150,7 +167,7 @@ private fun SuggestionsButton(onClick: () -> Unit) {
150167
painter = painterResource(R.drawable.ic_lightbulb),
151168
contentDescription = null,
152169
tint = Colors.PubkyGreen,
153-
modifier = Modifier.padding(end = 4.dp),
170+
modifier = Modifier.padding(end = 4.dp)
154171
)
155172
BodySSB(
156173
text = stringResource(R.string.profile__suggestions),

app/src/main/java/to/bitkit/ui/components/AddTagSheet.kt

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package to.bitkit.ui.components
22

33
import androidx.compose.foundation.layout.Column
44
import androidx.compose.foundation.layout.fillMaxWidth
5+
import androidx.compose.foundation.layout.imePadding
6+
import androidx.compose.foundation.layout.navigationBarsPadding
57
import androidx.compose.foundation.layout.padding
68
import androidx.compose.material3.ExperimentalMaterial3Api
79
import androidx.compose.material3.Icon
@@ -20,6 +22,8 @@ import androidx.compose.ui.unit.dp
2022
import kotlinx.collections.immutable.persistentListOf
2123
import to.bitkit.R
2224
import to.bitkit.ui.scaffold.SheetTopBar
25+
import to.bitkit.ui.shared.modifiers.sheetHeight
26+
import to.bitkit.ui.shared.util.gradientBackground
2327
import to.bitkit.ui.theme.AppThemeSurface
2428
import to.bitkit.ui.theme.Colors
2529

@@ -40,6 +44,7 @@ fun AddTagSheet(
4044
BottomSheet(
4145
onDismissRequest = onDismiss,
4246
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
47+
modifier = Modifier.imePadding()
4348
) {
4449
if (showSuggestions) {
4550
SuggestionsContent(
@@ -71,7 +76,13 @@ private fun TagFormContent(
7176
onSave: () -> Unit,
7277
isSaveEnabled: Boolean,
7378
) {
74-
Column(modifier = Modifier.padding(horizontal = 16.dp)) {
79+
Column(
80+
modifier = Modifier
81+
.sheetHeight(isModal = true)
82+
.gradientBackground()
83+
.navigationBarsPadding()
84+
.padding(horizontal = 16.dp),
85+
) {
7586
SheetTopBar(titleText = stringResource(R.string.profile__add_tag))
7687
VerticalSpacer(16.dp)
7788
Text13Up(text = stringResource(R.string.profile__add_tag_label))
@@ -80,22 +91,9 @@ private fun TagFormContent(
8091
value = tag,
8192
onValueChange = onTagChange,
8293
placeholder = stringResource(R.string.profile__add_tag_placeholder),
83-
trailingIcon = {
84-
TextButton(onClick = onShowSuggestions) {
85-
Icon(
86-
painter = painterResource(R.drawable.ic_lightbulb),
87-
contentDescription = null,
88-
tint = Colors.PubkyGreen,
89-
modifier = Modifier.padding(end = 4.dp),
90-
)
91-
BodySSB(
92-
text = stringResource(R.string.profile__suggestions),
93-
color = Colors.PubkyGreen,
94-
)
95-
}
96-
},
94+
trailingIcon = { SuggestionsButton(onClick = onShowSuggestions) },
9795
singleLine = true,
98-
modifier = Modifier.fillMaxWidth(),
96+
modifier = Modifier.fillMaxWidth()
9997
)
10098
VerticalSpacer(24.dp)
10199
PrimaryButton(
@@ -107,6 +105,22 @@ private fun TagFormContent(
107105
}
108106
}
109107

108+
@Composable
109+
private fun SuggestionsButton(onClick: () -> Unit) {
110+
TextButton(onClick = onClick) {
111+
Icon(
112+
painter = painterResource(R.drawable.ic_lightbulb),
113+
contentDescription = null,
114+
tint = Colors.PubkyGreen,
115+
modifier = Modifier.padding(end = 4.dp)
116+
)
117+
BodySSB(
118+
text = stringResource(R.string.profile__suggestions),
119+
color = Colors.PubkyGreen,
120+
)
121+
}
122+
}
123+
110124
@Preview
111125
@Composable
112126
private fun Preview() {

0 commit comments

Comments
 (0)