mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-11-04 09:51:11 +00:00
feat(dapps): show the bundled DApps catalog until one can be fetched
The DApps catalog lived only in memory and every read waited on first(), so with the catalog URL unreachable the DApps screen waited forever. feature-dapp-impl now bundles dapps_full.json at build time. On the first sync the repository tries the network once and, if that fails, publishes the bundled catalog so every read returns; the fetched catalog replaces it as soon as it arrives. The download task moves to scripts/bundled-config.gradle and serves both the chain list and the DApps catalog. runtime and feature-dapp-impl unit tests return defaults for android.util.Log, which the fallback calls.
This commit is contained in:
1 parent
ec9a2cf307
commit
5c66cd7f1d
7 files changed
+216
-43
No files matched your search
@@ -1,6 +1,7 @@
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply from: '../tests.gradle'
|
||||
apply from: "../scripts/secrets.gradle"
|
||||
apply from: "../scripts/bundled-config.gradle"
|
||||
|
||||
android {
|
||||
|
||||
@@ -21,6 +22,14 @@ android {
|
||||
}
|
||||
namespace 'io.novafoundation.nova.feature_dapp_impl'
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
// android.util.Log.* throws "Method ... not mocked" in plain JVM unit tests; the first-install
|
||||
// fallback logs the failed fetch it recovers from.
|
||||
returnDefaultValues = true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources.excludes.add("META-INF/NOTICE.md")
|
||||
}
|
||||
@@ -96,3 +105,10 @@ dependencies {
|
||||
testImplementation jUnitDep
|
||||
testImplementation mockitoDep
|
||||
}
|
||||
|
||||
// The DApps catalog a first install shows when none can be fetched (AssetBundledDappCatalog).
|
||||
bundleConfigAsset(
|
||||
"bundleDappsConfig",
|
||||
"https://raw.githubusercontent.com/pezkuwichain/pezkuwi-wallet-utils/master/dapps/dapps_full.json",
|
||||
"bundled-config/dapps.json"
|
||||
) { it instanceof Map && it.dapps instanceof List && !it.dapps.isEmpty() }
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataResponse
|
||||
|
||||
/** The DApps catalog the APK was built with: what is shown until a catalog can be fetched. */
|
||||
fun interface BundledDappCatalog {
|
||||
|
||||
fun load(): DappMetadataResponse?
|
||||
|
||||
companion object {
|
||||
val None = BundledDappCatalog { null }
|
||||
}
|
||||
}
|
||||
|
||||
class AssetBundledDappCatalog(
|
||||
private val context: Context,
|
||||
private val gson: Gson,
|
||||
) : BundledDappCatalog {
|
||||
|
||||
companion object {
|
||||
// Written at build time by the `bundleDappsConfig` task in feature-dapp-impl/build.gradle.
|
||||
const val ASSET_PATH = "bundled-config/dapps.json"
|
||||
}
|
||||
|
||||
override fun load(): DappMetadataResponse? = runCatching {
|
||||
context.assets.open(ASSET_PATH).bufferedReader().use { gson.fromJson(it, DappMetadataResponse::class.java) }
|
||||
}.onFailure {
|
||||
Log.e(LOG_TAG, "No usable bundled DApps catalog", it)
|
||||
}.getOrNull()
|
||||
}
|
||||
+30
-2
@@ -1,6 +1,8 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.LOG_TAG
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
@@ -11,6 +13,8 @@ import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepos
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.mappers.mapDAppMetadataResponseToDAppMetadatas
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataRemote
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataResponse
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -19,7 +23,8 @@ import kotlinx.coroutines.flow.first
|
||||
class RealDAppMetadataRepository(
|
||||
private val dappMetadataApi: DappMetadataApi,
|
||||
private val remoteApiUrl: String,
|
||||
private val browserHostSettingsDao: BrowserHostSettingsDao
|
||||
private val browserHostSettingsDao: BrowserHostSettingsDao,
|
||||
private val bundledCatalog: BundledDappCatalog = BundledDappCatalog.None,
|
||||
) : DAppMetadataRepository {
|
||||
|
||||
private val dappMetadatasFlow = MutableSharedFlow<DappCatalog>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
@@ -29,7 +34,30 @@ class RealDAppMetadataRepository(
|
||||
}
|
||||
|
||||
override suspend fun syncDAppMetadatas() {
|
||||
val response = retryUntilDone { dappMetadataApi.getParachainMetadata(remoteApiUrl) }
|
||||
if (dappMetadatasFlow.replayCache.isEmpty()) {
|
||||
// Every read below waits for a first catalog. Try the network once, and if that fails show the
|
||||
// catalog the APK was built with, so the DApps screen opens; the fetched one replaces it below.
|
||||
val firstTry = try {
|
||||
dappMetadataApi.getParachainMetadata(remoteApiUrl)
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "First DApps catalog fetch failed, showing the bundled catalog", e)
|
||||
null
|
||||
}
|
||||
|
||||
if (firstTry != null) {
|
||||
applyCatalog(firstTry)
|
||||
return
|
||||
}
|
||||
|
||||
bundledCatalog.load()?.let { dappMetadatasFlow.emit(mapDAppMetadataResponseToDAppMetadatas(it)) }
|
||||
}
|
||||
|
||||
applyCatalog(retryUntilDone { dappMetadataApi.getParachainMetadata(remoteApiUrl) })
|
||||
}
|
||||
|
||||
private suspend fun applyCatalog(response: DappMetadataResponse) {
|
||||
val dappMetadatas = mapDAppMetadataResponseToDAppMetadatas(response)
|
||||
syncHostSettings(response.dapps)
|
||||
dappMetadatasFlow.emit(dappMetadatas)
|
||||
|
||||
+8
-2
@@ -1,5 +1,7 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
@@ -8,6 +10,7 @@ import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.BuildConfig
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.AssetBundledDappCatalog
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.RealDAppMetadataRepository
|
||||
|
||||
@Module
|
||||
@@ -23,10 +26,13 @@ class DappMetadataModule {
|
||||
@FeatureScope
|
||||
fun provideDRepository(
|
||||
api: DappMetadataApi,
|
||||
dappHostSettingsDao: BrowserHostSettingsDao
|
||||
dappHostSettingsDao: BrowserHostSettingsDao,
|
||||
context: Context,
|
||||
gson: Gson
|
||||
): DAppMetadataRepository = RealDAppMetadataRepository(
|
||||
dappMetadataApi = api,
|
||||
remoteApiUrl = BuildConfig.DAPP_METADATAS_URL,
|
||||
browserHostSettingsDao = dappHostSettingsDao
|
||||
browserHostSettingsDao = dappHostSettingsDao,
|
||||
bundledCatalog = AssetBundledDappCatalog(context, gson)
|
||||
)
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataRemote
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataResponse
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.yield
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.junit.MockitoJUnitRunner
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(MockitoJUnitRunner::class)
|
||||
class RealDAppMetadataRepositoryTest {
|
||||
|
||||
@Mock
|
||||
lateinit var hostSettingsDao: BrowserHostSettingsDao
|
||||
|
||||
private val bundled = catalogOf("https://bundled.example")
|
||||
private val remote = catalogOf("https://remote.example")
|
||||
|
||||
@Test
|
||||
fun `a first install shows the bundled catalog until one is fetched`() = runBlocking {
|
||||
`when`(hostSettingsDao.getBrowserAllHostSettings()).thenReturn(emptyList())
|
||||
val network = CompletableDeferred<DappMetadataResponse>()
|
||||
var calls = 0
|
||||
val api = object : DappMetadataApi {
|
||||
override suspend fun getParachainMetadata(url: String): DappMetadataResponse {
|
||||
calls++
|
||||
if (calls == 1) throw IOException("unreachable")
|
||||
return network.await()
|
||||
}
|
||||
}
|
||||
val repository = RealDAppMetadataRepository(api, "url", hostSettingsDao) { bundled }
|
||||
|
||||
val sync = launch { repository.syncDAppMetadatas() }
|
||||
while (!repository.isDAppsSynced()) yield()
|
||||
|
||||
assertEquals("https://bundled.example", repository.getDAppCatalog().dApps.single().url)
|
||||
|
||||
network.complete(remote)
|
||||
sync.join()
|
||||
|
||||
assertEquals("https://remote.example", repository.getDAppCatalog().dApps.single().url)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the bundled catalog is not read when the fetch works`() = runBlocking {
|
||||
`when`(hostSettingsDao.getBrowserAllHostSettings()).thenReturn(emptyList())
|
||||
var bundledReads = 0
|
||||
val api = object : DappMetadataApi {
|
||||
override suspend fun getParachainMetadata(url: String) = remote
|
||||
}
|
||||
val repository = RealDAppMetadataRepository(api, "url", hostSettingsDao) { bundledReads++; bundled }
|
||||
|
||||
repository.syncDAppMetadatas()
|
||||
|
||||
assertEquals(0, bundledReads)
|
||||
assertEquals("https://remote.example", repository.getDAppCatalog().dApps.single().url)
|
||||
}
|
||||
|
||||
private fun catalogOf(url: String) = DappMetadataResponse(
|
||||
popular = emptyList(),
|
||||
categories = emptyList(),
|
||||
dapps = listOf(DappMetadataRemote(name = "d", icon = "i", url = url, categories = emptyList(), desktopOnly = null))
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user