Conversation
app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthViewModel.kt
Show resolved
Hide resolved
|
Starting review |
| name = ffiProfile.name, | ||
| bio = ffiProfile.bio ?: "", | ||
| imageUrl = ffiProfile.image, | ||
| links = ffiProfile.links?.map { PubkyProfileLink(label = it.title, url = it.url) } ?: emptyList(), |
jvsena42
left a comment
There was a problem hiding this comment.
Could add some unit tests at Repo level too
| } | ||
|
|
||
| private val scope = CoroutineScope(bgDispatcher + SupervisorJob()) | ||
| private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) |
|
|
||
| fun copyPublicKey() { | ||
| val pk = pubkyRepo.publicKey.value ?: return | ||
| val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager |
There was a problem hiding this comment.
Use the existing context extension method
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| fun PubkyImage( | ||
| uri: String, | ||
| size: Dp, | ||
| modifier: Modifier = Modifier, | ||
| ) { | ||
| val context = LocalContext.current | ||
| val repo = remember { | ||
| EntryPointAccessors.fromApplication(context, PubkyImageEntryPoint::class.java).pubkyRepo() | ||
| } | ||
| var bitmap by remember(uri) { mutableStateOf(repo.cachedImage(uri)) } | ||
| var hasFailed by remember(uri) { mutableStateOf(false) } |
There was a problem hiding this comment.
could replace with a ViewModel injection
There was a problem hiding this comment.
could be converted to Webp and imported in multiple densities using https://www.appicon.co/#image-sets
There was a problem hiding this comment.
Reviewed up to and including PubkyImage.kt, will resume later.
Test Recording:
https://github.com/user-attachments/assets/fc3f6997-981e-422f-92e4-60769232bd3d
Manually tested and found 2 critical, pls see video above:
- Crash when trying to go to Profile via Drawer.
- Dialog should use
AppAlertDialogwhich comes with theming for our design lang.
Plus a couple nits, strongest views on:
- using viewModel and no business logic in UI layer
- using DataStore instead of SharedPreferences.
Aside from these small things, it works great 🎉
| _authState.update { PubkyAuthState.Authenticated } | ||
| Logger.info("Pubky auth completed for $pk", context = TAG) | ||
| loadProfile() | ||
| }.map {} |
There was a problem hiding this comment.
nit: could return Result<Boolean> to signal if successful
Edit: fixed to show what I meant using the inline code block.
| runCatching { pubkyService.signOut() } | ||
| .onFailure { | ||
| Logger.warn("Server sign out failed, forcing local sign out", it, context = TAG) | ||
| runCatching { pubkyService.forceSignOut() } | ||
| } | ||
| runCatching { keychain.delete(Keychain.Key.PAYKIT_SESSION.name) } | ||
| runCatching { imageCache.clear() } |
There was a problem hiding this comment.
nit: it's better to refactor this to use .recoverCatching{} and .mapCatching {} chaining.
app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt
Outdated
Show resolved
Hide resolved
app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt
Outdated
Show resolved
Hide resolved
|
|
||
| fun copyPublicKey() { | ||
| val pk = pubkyRepo.publicKey.value ?: return | ||
| val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
app/src/main/java/to/bitkit/ui/screens/profile/PubkyRingAuthScreen.kt
Outdated
Show resolved
Hide resolved
| <string name="profile__auth_error_title">Authorization Failed</string> | ||
| <string name="profile__empty_state">Unable to load your profile.</string> | ||
| <string name="profile__intro_description">Set up your portable pubky profile, so your contacts can reach you or pay you anytime, anywhere in the ecosystem.</string> | ||
| <string name="profile__intro_title">Portable\n<accent>pubky profile</accent></string> | ||
| <string name="profile__nav_title">Profile</string> | ||
| <string name="profile__public_key">Public Key</string> | ||
| <string name="profile__qr_scan_label">Scan to add {name}</string> | ||
| <string name="profile__retry_load">Try Again</string> | ||
| <string name="profile__ring_auth_description">Please authorize Bitkit with Pubky Ring, your mobile keychain for the next web.</string> | ||
| <string name="profile__ring_auth_title">Join the\n<accent>pubky web</accent></string> | ||
| <string name="profile__ring_authorize">Authorize</string> | ||
| <string name="profile__ring_download">Download</string> | ||
| <string name="profile__ring_not_installed_description">Pubky Ring is required to authorize your profile. Would you like to download it?</string> | ||
| <string name="profile__ring_not_installed_title">Pubky Ring Not Installed</string> | ||
| <string name="profile__ring_waiting">Waiting for authorization from Pubky Ring…</string> | ||
| <string name="profile__sign_out">Disconnect</string> | ||
| <string name="profile__sign_out_description">This will disconnect your Pubky profile from Bitkit. You can reconnect at any time.</string> | ||
| <string name="profile__sign_out_title">Disconnect Profile</string> | ||
| <string name="profile__your_name">Your Name</string> |
There was a problem hiding this comment.
nit: pls check if some slashtags__ prefixed strings are still used or can be removed.
| override suspend fun readFrom(input: InputStream): PubkyStoreData { | ||
| return try { | ||
| json.decodeFromString(input.readBytes().decodeToString()) | ||
| } catch (e: SerializationException) { | ||
| Logger.error("Failed to deserialize PubkyStoreData: $e") | ||
| defaultValue | ||
| } | ||
| } |
There was a problem hiding this comment.
Three issues in this catch block per CLAUDE.md:
- Use
ResultAPI instead oftry-catch: "ALWAYS use the Result API instead of try-catch" - Exception manually appended to message: "NEVER manually append the Throwable's message or any other props to the string passed as the 1st param of
Logger.*calls" — pass it as theeargument instead - Missing
context = TAG: "ALWAYS pass the TAG as context to Logger calls" — noTAGconstant exists in thisobject
Suggested fix:
| override suspend fun readFrom(input: InputStream): PubkyStoreData { | |
| return try { | |
| json.decodeFromString(input.readBytes().decodeToString()) | |
| } catch (e: SerializationException) { | |
| Logger.error("Failed to deserialize PubkyStoreData: $e") | |
| defaultValue | |
| } | |
| } | |
| override suspend fun readFrom(input: InputStream): PubkyStoreData { | |
| return runCatching { | |
| json.decodeFromString(input.readBytes().decodeToString()) | |
| }.getOrElse { | |
| Logger.error("Failed to deserialize PubkyStoreData", it, context = TAG) | |
| defaultValue | |
| } | |
| } |
Also add a companion TAG constant to this object:
private val TAG = PubkyStoreSerializer::class.simpleNameThere was a problem hiding this comment.
Claude finished @ovitrif's task in 1m 0s —— View job
- Read
PubkyStoreSerializer.ktfor context - Analyze Kotlin semantics for
this::class.simpleNameinobject - Post answer
Answer: this::class.simpleName in a Kotlin object
Technically it works, but private const val TAG = "..." is preferable. Here's why:
What works
In a Kotlin object, this refers to the singleton instance and is accessible in property initializers, so this compiles:
object PubkyStoreSerializer : Serializer<PubkyStoreData> {
private val TAG = this::class.simpleName // compiles ✓
}The catch: nullable String?
KClass.simpleName returns String? (nullable) — it's null for anonymous objects and local classes. So TAG would be typed as String?, which means passing it to Logger.error(..., context = TAG) passes a nullable where a String is expected — this would likely cause a compile error or require a !!/?: "" workaround.
Why const val is better here
private const val TAG = "PubkyStoreSerializer" // String, non-null, compile-time constant ✓- Non-nullable — no workaround needed
- Compile-time constant — inlined at call sites (no runtime overhead)
- Established pattern — matches every other serializer/class in the codebase
Summary
this::class.simpleName is fine for logging in regular classes (where you can store it as String? and pass ?: javaClass.simpleName), but for object declarations the const val approach is strictly better. The file already has the correct implementation.
| pubkyStore.reset() | ||
| _publicKey.update { null } | ||
| _profile.update { null } | ||
| _authState.update { PubkyAuthState.Idle } |
There was a problem hiding this comment.
Race condition with loadProfile()
signOut() does not acquire loadProfileMutex, creating a race condition with loadProfile():
loadProfile()acquires the mutex and suspends insidepubkyService.getProfile(pk)(network I/O)signOut()runs concurrently, clears_publicKey,_profile, and sets auth state toIdleloadProfile()resumes, writes the fetched profile back to_profile(line 180), and callscacheMetadata()— restoring stale data after sign-out
The result: after sign-out, _authState is Idle but _profile and pubkyStore contain the previous user's data, causing the UI to display a stale profile for an unauthenticated user. On next cold start, the cached name/image will be shown again.
bitkit-android/app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Lines 183 to 197 in 1985f9e
Fix: either have signOut() acquire loadProfileMutex before clearing state, or re-check _publicKey.value inside loadProfile() after the network call completes (while still holding the mutex) and skip writing if it is null.
This comment has been minimized.
This comment has been minimized.
ovitrif
left a comment
There was a problem hiding this comment.
Submitted couple more pending comments and resolved the ones where code changes applied. The goal is to reach a state where agent can be asked to scan through non-resolved review comments and address those in a new pass.
| import javax.inject.Singleton | ||
|
|
||
| private val Context.pubkyDataStore: DataStore<PubkyStoreData> by dataStore( | ||
| fileName = "pubky_profile_cache.json", |
There was a problem hiding this comment.
nit: may use more general name like pubky.json or an extension thereof which doesn't force this to profile only. ultimately these are jsons they can support even lists => name can be more generalistic
| try { | ||
| runCatching { | ||
| withContext(bgDispatcher) { | ||
| val ffiProfile = pubkyService.getProfile(pk) | ||
| Logger.debug("Profile loaded — name: ${ffiProfile.name}, image: ${ffiProfile.image}", context = TAG) | ||
| PubkyProfile.fromFfi(pk, ffiProfile) | ||
| } | ||
| }.onSuccess { loadedProfile -> | ||
| _profile.update { loadedProfile } | ||
| cacheMetadata(loadedProfile) | ||
| }.onFailure { | ||
| Logger.error("Failed to load profile", it, context = TAG) | ||
| } | ||
| } finally { | ||
| _isLoadingProfile.update { false } | ||
| loadProfileMutex.unlock() | ||
| } |
There was a problem hiding this comment.
yes, normally onSuccess and onFailure are meant for side effects, for chaining it's, recommended to use mapCatching
Answer:
|
| Context | this::class.simpleName returns |
|---|---|
| Regular class body | "MyClassName" ✓ |
object declaration (e.g. PubkyStoreSerializer) |
"PubkyStoreSerializer" ✓ |
Where it silently breaks ❌
companion object — which is the dominant TAG pattern in this codebase:
class MyViewModel : ViewModel() {
companion object {
private val TAG = this::class.simpleName // returns "Companion", not "MyViewModel" ❌
}
}Inside a companion object, this refers to the companion object singleton itself, so .simpleName returns "Companion" — your logs would show Companion as the tag for every class.
Nullable return type ⚠️
KClass.simpleName returns String? (nullable). It's null for anonymous classes. You'd need ?: "Fallback" everywhere, or use !! which is banned by CLAUDE.md.
Not a compile-time constant ⚠️
val (not const val) means it's evaluated at runtime and can't be used in annotations.
Codebase verdict
Looking at the codebase, every TAG is a private const val TAG = "ClassName" string literal — in companion objects for ViewModels and as top-level private const val for files/objects. Given that the companion object pattern dominates, this::class.simpleName would produce wrong values for the majority of usages.
If you wanted to avoid the string literal, the only safe cross-context alternative would be:
// In companion object — reference the outer class explicitly:
companion object {
private val TAG = MyViewModel::class.simpleName ?: "MyViewModel"
}But this still has the nullable issue and isn't a const. The const val string literal is the cleanest option here and matches all existing patterns in the codebase.
Summary
Integrates Pubky decentralized identity into Bitkit, allowing users to connect their Pubky profile via Pubky Ring authentication. Once connected, the user's profile name and avatar are displayed on the home screen header, and a full profile page shows their bio, links, and a shareable QR code.
What's included
pubkyauth://) with relay-based session exchange, session persistence in Keychain, and automatic restoration on app launchPubkyService— service layer wrappingpaykit(profile/session management) andbitkit-core(auth relay, file fetching)PubkyRepo— manages auth state, session lifecycle, and profile data with all Keychain/disk operations offloaded to background threadsPubkyImagecomponent for loadingpubky://URIs with two-tier (memory + disk) caching