Skip to content

Commit bad9821

Browse files
authored
Merge pull request #824 from synonymdev/feat/pubky-profile
feat: add profile and contacts fetching from pubky
2 parents cb141bc + 348af4a commit bad9821

92 files changed

Lines changed: 10330 additions & 97 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
- Show loading state on Spending tab when node is not running #875
1818

1919
### Added
20+
- Pubky profile onboarding with contact sync, import, and editing #824
2021
- Lightning Connections empty state with onboarding screen #857
2122
- Unified PIN management screen (enable/disable/change in one place) #857
2223
- Support entry in drawer menu #857

app/build.gradle.kts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ val bcp47Locales = listOf(
4747
"en", "ar", "es-419", "ca", "cs", "de", "el", "es", "es-ES", "fr", "it", "nl", "pl", "pt", "pt-BR", "ru"
4848
)
4949
val e2eBackendEnv = System.getenv("E2E_BACKEND") ?: "local"
50+
val e2eHomegateUrlEnv = System.getenv("E2E_HOMEGATE_URL") ?: "http://127.0.0.1:6288"
5051

5152
android {
5253
namespace = "to.bitkit"
@@ -63,6 +64,7 @@ android {
6364
}
6465
buildConfigField("boolean", "E2E", System.getenv("E2E")?.toBoolean()?.toString() ?: "false")
6566
buildConfigField("String", "E2E_BACKEND", "\"$e2eBackendEnv\"")
67+
buildConfigField("String", "E2E_HOMEGATE_URL", "\"$e2eHomegateUrlEnv\"")
6668
buildConfigField("boolean", "GEO", System.getenv("GEO")?.toBoolean()?.toString() ?: "true")
6769
buildConfigField("String", "LOCALES", "\"${bcp47Locales.joinToString(",")}\"")
6870
}
@@ -237,6 +239,7 @@ dependencies {
237239
implementation(libs.bouncycastle.provider.jdk)
238240
implementation(libs.ldk.node.android) { exclude(group = "net.java.dev.jna", module = "jna") }
239241
implementation(libs.bitkit.core)
242+
implementation(libs.paykit)
240243
implementation(libs.vss.client)
241244
// Firebase
242245
implementation(platform(libs.firebase.bom))
@@ -267,6 +270,9 @@ dependencies {
267270
implementation(libs.charts)
268271
implementation(libs.haze)
269272
implementation(libs.haze.materials)
273+
// Image Loading
274+
implementation(platform(libs.coil.bom))
275+
implementation(libs.coil.compose)
270276
// Compose Navigation
271277
implementation(libs.navigation.compose)
272278
androidTestImplementation(libs.navigation.testing)

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
xmlns:tools="http://schemas.android.com/tools">
44

55
<queries>
6+
<package android:name="to.pubky.ring" />
67
<intent>
78
<action android:name="android.settings.APPLICATION_DETAILS_SETTINGS" />
89
</intent>
10+
<intent>
11+
<action android:name="android.intent.action.VIEW" />
12+
<data android:scheme="pubkyauth" />
13+
</intent>
914
</queries>
1015

1116
<uses-feature
@@ -100,6 +105,7 @@
100105
<data android:scheme="lnurlw" />
101106
<data android:scheme="lnurlc" />
102107
<data android:scheme="lnurlp" />
108+
<data android:scheme="pubkyauth" />
103109
</intent-filter>
104110

105111
<!-- NFC -->

app/src/main/java/to/bitkit/App.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import android.app.Application.ActivityLifecycleCallbacks
77
import android.os.Bundle
88
import androidx.hilt.work.HiltWorkerFactory
99
import androidx.work.Configuration
10+
import coil3.ImageLoader
11+
import coil3.SingletonImageLoader
1012
import dagger.hilt.android.HiltAndroidApp
1113
import to.bitkit.env.Env
1214
import javax.inject.Inject
@@ -16,13 +18,17 @@ internal open class App : Application(), Configuration.Provider {
1618
@Inject
1719
lateinit var workerFactory: HiltWorkerFactory
1820

21+
@Inject
22+
lateinit var imageLoader: ImageLoader
23+
1924
override val workManagerConfiguration
2025
get() = Configuration.Builder()
2126
.setWorkerFactory(workerFactory)
2227
.build()
2328

2429
override fun onCreate() {
2530
super.onCreate()
31+
SingletonImageLoader.setSafe { imageLoader }
2632
currentActivity = CurrentActivity().also { registerActivityLifecycleCallbacks(it) }
2733
Env.initAppStoragePath(filesDir.absolutePath)
2834
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package to.bitkit.data
2+
3+
import coil3.ImageLoader
4+
import coil3.Uri
5+
import coil3.decode.DataSource
6+
import coil3.decode.ImageSource
7+
import coil3.fetch.FetchResult
8+
import coil3.fetch.Fetcher
9+
import coil3.fetch.SourceFetchResult
10+
import coil3.request.Options
11+
import okio.Buffer
12+
import org.json.JSONObject
13+
import to.bitkit.services.PubkyService
14+
import to.bitkit.utils.Logger
15+
16+
private const val TAG = "PubkyImageFetcher"
17+
private const val PUBKY_SCHEME = "pubky://"
18+
19+
class PubkyImageFetcher(
20+
private val uri: String,
21+
private val options: Options,
22+
private val pubkyService: PubkyService,
23+
) : Fetcher {
24+
25+
override suspend fun fetch(): FetchResult {
26+
val data = pubkyService.fetchFile(uri)
27+
val blobData = resolveImageData(data)
28+
val source = ImageSource(Buffer().apply { write(blobData) }, options.fileSystem)
29+
return SourceFetchResult(source, null, dataSource = DataSource.NETWORK)
30+
}
31+
32+
private suspend fun resolveImageData(data: ByteArray): ByteArray = runCatching {
33+
val json = JSONObject(String(data))
34+
val src = json.optString("src", "")
35+
if (src.isNotEmpty() && src.startsWith(PUBKY_SCHEME)) {
36+
Logger.debug("Found file descriptor, fetching blob from '$src'", context = TAG)
37+
pubkyService.fetchFile(src)
38+
} else {
39+
data
40+
}
41+
}.getOrDefault(data)
42+
43+
class Factory(private val pubkyService: PubkyService) : Fetcher.Factory<Uri> {
44+
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher? {
45+
val uri = data.toString()
46+
if (!uri.startsWith(PUBKY_SCHEME)) return null
47+
return PubkyImageFetcher(uri, options, pubkyService)
48+
}
49+
}
50+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package to.bitkit.data
2+
3+
import android.content.Context
4+
import androidx.datastore.core.DataStore
5+
import androidx.datastore.dataStore
6+
import dagger.hilt.android.qualifiers.ApplicationContext
7+
import kotlinx.coroutines.flow.Flow
8+
import kotlinx.serialization.Serializable
9+
import to.bitkit.data.serializers.PubkyStoreSerializer
10+
import javax.inject.Inject
11+
import javax.inject.Singleton
12+
13+
private val Context.pubkyDataStore: DataStore<PubkyStoreData> by dataStore(
14+
fileName = "pubky.json",
15+
serializer = PubkyStoreSerializer,
16+
)
17+
18+
@Singleton
19+
class PubkyStore @Inject constructor(
20+
@ApplicationContext private val context: Context,
21+
) {
22+
private val store = context.pubkyDataStore
23+
24+
val data: Flow<PubkyStoreData> = store.data
25+
26+
suspend fun update(transform: (PubkyStoreData) -> PubkyStoreData) {
27+
store.updateData(transform)
28+
}
29+
30+
suspend fun reset() {
31+
store.updateData { PubkyStoreData() }
32+
}
33+
}
34+
35+
@Serializable
36+
data class PubkyStoreData(
37+
val cachedName: String? = null,
38+
val cachedImageUri: String? = null,
39+
)

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ data class SettingsData(
9999
val hasSeenSavingsIntro: Boolean = false,
100100
val hasSeenShopIntro: Boolean = false,
101101
val hasSeenProfileIntro: Boolean = false,
102+
val hasSeenContactsIntro: Boolean = false,
102103
val quickPayIntroSeen: Boolean = false,
103104
val bgPaymentsIntroSeen: Boolean = false,
104105
val isQuickPayEnabled: Boolean = false,

app/src/main/java/to/bitkit/data/keychain/Keychain.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ class Keychain @Inject constructor(
173173
BIP39_PASSPHRASE,
174174
PIN,
175175
PIN_ATTEMPTS_REMAINING,
176+
PAYKIT_SESSION,
177+
PUBKY_SECRET_KEY,
176178
}
177179
}
178180

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package to.bitkit.data.serializers
2+
3+
import androidx.datastore.core.Serializer
4+
import to.bitkit.data.PubkyStoreData
5+
import to.bitkit.di.json
6+
import to.bitkit.utils.Logger
7+
import java.io.InputStream
8+
import java.io.OutputStream
9+
10+
object PubkyStoreSerializer : Serializer<PubkyStoreData> {
11+
private const val TAG = "PubkyStoreSerializer"
12+
13+
override val defaultValue: PubkyStoreData = PubkyStoreData()
14+
15+
override suspend fun readFrom(input: InputStream): PubkyStoreData {
16+
return runCatching {
17+
json.decodeFromString<PubkyStoreData>(input.readBytes().decodeToString())
18+
}.getOrElse {
19+
Logger.error("Failed to deserialize PubkyStoreData", it, context = TAG)
20+
defaultValue
21+
}
22+
}
23+
24+
override suspend fun writeTo(t: PubkyStoreData, output: OutputStream) {
25+
output.write(json.encodeToString(t).encodeToByteArray())
26+
}
27+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package to.bitkit.di
2+
3+
import android.content.Context
4+
import coil3.ImageLoader
5+
import coil3.disk.DiskCache
6+
import coil3.disk.directory
7+
import coil3.memory.MemoryCache
8+
import coil3.request.crossfade
9+
import dagger.Module
10+
import dagger.Provides
11+
import dagger.hilt.InstallIn
12+
import dagger.hilt.android.qualifiers.ApplicationContext
13+
import dagger.hilt.components.SingletonComponent
14+
import to.bitkit.data.PubkyImageFetcher
15+
import to.bitkit.services.PubkyService
16+
import javax.inject.Singleton
17+
18+
@Module
19+
@InstallIn(SingletonComponent::class)
20+
object ImageModule {
21+
22+
@Provides
23+
@Singleton
24+
fun provideImageLoader(
25+
@ApplicationContext context: Context,
26+
pubkyService: PubkyService,
27+
): ImageLoader = ImageLoader.Builder(context)
28+
.crossfade(true)
29+
.components { add(PubkyImageFetcher.Factory(pubkyService)) }
30+
.memoryCache {
31+
MemoryCache.Builder()
32+
.maxSizePercent(context, percent = 0.15)
33+
.build()
34+
}
35+
.diskCache {
36+
DiskCache.Builder()
37+
.directory(context.cacheDir.resolve("pubky-images"))
38+
.build()
39+
}
40+
.build()
41+
}

0 commit comments

Comments
 (0)