mirror of
https://github.com/pezkuwichain/pezkuwi-wallet-android.git
synced 2026-04-22 18:27:56 +00:00
Initial commit: Pezkuwi Wallet Android
Complete rebrand of Nova Wallet for Pezkuwichain ecosystem. ## Features - Full Pezkuwichain support (HEZ & PEZ tokens) - Polkadot ecosystem compatibility - Staking, Governance, DeFi, NFTs - XCM cross-chain transfers - Hardware wallet support (Ledger, Polkadot Vault) - WalletConnect v2 - Push notifications ## Languages - English, Turkish, Kurmanci (Kurdish), Spanish, French, German, Russian, Japanese, Chinese, Korean, Portuguese, Vietnamese Based on Nova Wallet by Novasama Technologies GmbH © Dijital Kurdistan Tech Institute 2026
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.mappers
|
||||
|
||||
import io.novafoundation.nova.core_db.model.BrowserHostSettingsLocal
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.BrowserHostSettings
|
||||
|
||||
fun mapBrowserHostSettingsToLocal(settings: BrowserHostSettings): BrowserHostSettingsLocal {
|
||||
return BrowserHostSettingsLocal(
|
||||
settings.hostUrl,
|
||||
settings.isDesktopModeEnabled
|
||||
)
|
||||
}
|
||||
|
||||
fun mapBrowserHostSettingsFromLocal(settings: BrowserHostSettingsLocal): BrowserHostSettings {
|
||||
return BrowserHostSettings(
|
||||
settings.hostUrl,
|
||||
settings.isDesktopModeEnabled
|
||||
)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.mappers
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCatalog
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCategory
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.metadata.DappMetadataResponse
|
||||
|
||||
fun mapDAppMetadataResponseToDAppMetadatas(
|
||||
response: DappMetadataResponse
|
||||
): DappCatalog {
|
||||
val categories = response.categories.map {
|
||||
DappCategory(
|
||||
iconUrl = it.icon,
|
||||
name = it.name,
|
||||
id = it.id
|
||||
)
|
||||
}
|
||||
|
||||
val categoriesAssociatedById = categories.associateBy { it.id }
|
||||
|
||||
val metadata = response.dapps.map {
|
||||
DappMetadata(
|
||||
name = it.name,
|
||||
iconLink = it.icon,
|
||||
url = it.url,
|
||||
baseUrl = Urls.normalizeUrl(it.url),
|
||||
categories = it.categories.mapNotNullTo(mutableSetOf(), categoriesAssociatedById::get),
|
||||
)
|
||||
}
|
||||
|
||||
return DappCatalog(response.popular.map { it.url }, categories, metadata)
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.mappers
|
||||
|
||||
import io.novafoundation.nova.core_db.model.FavouriteDAppLocal
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
|
||||
fun mapFavouriteDAppLocalToFavouriteDApp(favouriteDAppLocal: FavouriteDAppLocal): FavouriteDApp {
|
||||
return with(favouriteDAppLocal) {
|
||||
FavouriteDApp(
|
||||
url = url,
|
||||
label = label,
|
||||
icon = icon,
|
||||
orderingIndex = orderingIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun mapFavouriteDAppToFavouriteDAppLocal(favouriteDApp: FavouriteDApp): FavouriteDAppLocal {
|
||||
return with(favouriteDApp) {
|
||||
FavouriteDAppLocal(
|
||||
url = url,
|
||||
label = label,
|
||||
icon = icon,
|
||||
orderingIndex = orderingIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.model
|
||||
|
||||
data class FavouriteDApp(
|
||||
val url: String,
|
||||
val label: String,
|
||||
val icon: String?,
|
||||
val orderingIndex: Int
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.network.metadata
|
||||
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Url
|
||||
|
||||
interface DappMetadataApi {
|
||||
|
||||
@GET
|
||||
suspend fun getParachainMetadata(@Url url: String): DappMetadataResponse
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.network.metadata
|
||||
|
||||
class DappMetadataResponse(
|
||||
val popular: List<DappPopularRemote>,
|
||||
val categories: List<DappCategoryRemote>,
|
||||
val dapps: List<DappMetadataRemote>
|
||||
)
|
||||
|
||||
class DappMetadataRemote(
|
||||
val name: String,
|
||||
val icon: String,
|
||||
val url: String,
|
||||
val categories: List<String>,
|
||||
val desktopOnly: Boolean?
|
||||
)
|
||||
|
||||
class DappCategoryRemote(
|
||||
val icon: String?,
|
||||
val name: String,
|
||||
val id: String
|
||||
)
|
||||
|
||||
class DappPopularRemote(val url: String)
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.network.phishing
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface PhishingSitesApi {
|
||||
|
||||
@GET("https://raw.githubusercontent.com/polkadot-js/phishing/master/all.json")
|
||||
suspend fun getPhishingSites(): PhishingSitesRemote
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.network.phishing
|
||||
|
||||
class PhishingSitesRemote(
|
||||
val deny: List<String>
|
||||
)
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.phisning
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.core_db.dao.PhishingSitesDao
|
||||
|
||||
interface PhishingDetectingService {
|
||||
|
||||
suspend fun isPhishing(url: String): Boolean
|
||||
}
|
||||
|
||||
class CompoundPhishingDetectingService(
|
||||
private val services: List<PhishingDetectingService>
|
||||
) : PhishingDetectingService {
|
||||
|
||||
override suspend fun isPhishing(url: String): Boolean {
|
||||
return services.any { it.isPhishing(url) }
|
||||
}
|
||||
}
|
||||
|
||||
class BlackListPhishingDetectingService(
|
||||
private val phishingSitesDao: PhishingSitesDao,
|
||||
) : PhishingDetectingService {
|
||||
|
||||
override suspend fun isPhishing(url: String): Boolean {
|
||||
val host = Urls.hostOf(url)
|
||||
val hostSuffixes = extractAllPossibleSubDomains(host)
|
||||
|
||||
return phishingSitesDao.isPhishing(hostSuffixes)
|
||||
}
|
||||
|
||||
private fun extractAllPossibleSubDomains(host: String): List<String> {
|
||||
val separator = "."
|
||||
|
||||
val segments = host.split(separator)
|
||||
|
||||
val suffixes = (2..segments.size).map { suffixLength ->
|
||||
segments.takeLast(suffixLength).joinToString(separator = ".")
|
||||
}
|
||||
|
||||
return suffixes
|
||||
}
|
||||
}
|
||||
|
||||
class DomainListPhishingDetectingService(
|
||||
private val blackListDomains: List<String> // top
|
||||
) : PhishingDetectingService {
|
||||
|
||||
override suspend fun isPhishing(url: String): Boolean {
|
||||
val host = Urls.hostOf(url)
|
||||
val urlTopLevelDomain = extractTopLevelDomain(host)
|
||||
|
||||
return blackListDomains.any { urlTopLevelDomain == it }
|
||||
}
|
||||
|
||||
private fun extractTopLevelDomain(host: String): String {
|
||||
val separator = "."
|
||||
|
||||
val segments = host.split(separator)
|
||||
|
||||
return segments.last()
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.fromJson
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.model.MetamaskChain
|
||||
import javax.inject.Inject
|
||||
|
||||
interface DefaultMetamaskChainRepository {
|
||||
|
||||
fun getDefaultMetamaskChain(): MetamaskChain?
|
||||
|
||||
fun saveDefaultMetamaskChain(chain: MetamaskChain)
|
||||
}
|
||||
|
||||
private const val PREFERENCES_KEY = "RealDefaultMetamaskChainRepository.DefaultMetamaskChain"
|
||||
|
||||
@FeatureScope
|
||||
class RealDefaultMetamaskChainRepository @Inject constructor(
|
||||
private val preferences: Preferences,
|
||||
private val gson: Gson,
|
||||
) : DefaultMetamaskChainRepository {
|
||||
|
||||
override fun getDefaultMetamaskChain(): MetamaskChain? {
|
||||
val raw = preferences.getString(PREFERENCES_KEY) ?: return null
|
||||
return runCatching { gson.fromJson<MetamaskChain>(raw) }.getOrNull()
|
||||
}
|
||||
|
||||
override fun saveDefaultMetamaskChain(chain: MetamaskChain) {
|
||||
val raw = gson.toJson(chain)
|
||||
preferences.putString(PREFERENCES_KEY, raw)
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.mappers.mapFavouriteDAppLocalToFavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.mappers.mapFavouriteDAppToFavouriteDAppLocal
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface FavouritesDAppRepository {
|
||||
|
||||
fun observeFavourites(): Flow<List<FavouriteDApp>>
|
||||
|
||||
suspend fun getFavourites(): List<FavouriteDApp>
|
||||
|
||||
suspend fun addFavourite(favouriteDApp: FavouriteDApp)
|
||||
|
||||
fun observeIsFavourite(url: String): Flow<Boolean>
|
||||
|
||||
suspend fun removeFavourite(dAppUrl: String)
|
||||
|
||||
suspend fun updateFavoriteDapps(favoriteDapps: List<FavouriteDApp>)
|
||||
|
||||
suspend fun getNextOrderingIndex(): Int
|
||||
}
|
||||
|
||||
class DbFavouritesDAppRepository(
|
||||
private val favouriteDAppsDao: FavouriteDAppsDao
|
||||
) : FavouritesDAppRepository {
|
||||
|
||||
override fun observeFavourites(): Flow<List<FavouriteDApp>> {
|
||||
return favouriteDAppsDao.observeFavouriteDApps()
|
||||
.mapList(::mapFavouriteDAppLocalToFavouriteDApp)
|
||||
}
|
||||
|
||||
override suspend fun getFavourites(): List<FavouriteDApp> {
|
||||
return favouriteDAppsDao.getFavouriteDApps()
|
||||
.map(::mapFavouriteDAppLocalToFavouriteDApp)
|
||||
}
|
||||
|
||||
override suspend fun addFavourite(favouriteDApp: FavouriteDApp) {
|
||||
val local = mapFavouriteDAppToFavouriteDAppLocal(favouriteDApp)
|
||||
|
||||
favouriteDAppsDao.insertFavouriteDApp(local)
|
||||
}
|
||||
|
||||
override fun observeIsFavourite(url: String): Flow<Boolean> {
|
||||
return favouriteDAppsDao.observeIsFavourite(url)
|
||||
}
|
||||
|
||||
override suspend fun removeFavourite(dAppUrl: String) {
|
||||
favouriteDAppsDao.deleteFavouriteDApp(dAppUrl)
|
||||
}
|
||||
|
||||
override suspend fun updateFavoriteDapps(favoriteDapps: List<FavouriteDApp>) {
|
||||
val newDapps = favoriteDapps.map { mapFavouriteDAppToFavouriteDAppLocal(it) }
|
||||
val currentDapps = favouriteDAppsDao.getFavouriteDApps()
|
||||
val diff = CollectionDiffer.findDiff(newDapps, currentDapps, false)
|
||||
favouriteDAppsDao.updateFavourites(diff.updated)
|
||||
}
|
||||
|
||||
override suspend fun getNextOrderingIndex(): Int {
|
||||
return try {
|
||||
favouriteDAppsDao.getMaxOrderingIndex() + 1
|
||||
} catch (e: NullPointerException) { // For case we don't have added favorite dapps
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.PhishingSitesDao
|
||||
import io.novafoundation.nova.core_db.model.PhishingSiteLocal
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.phishing.PhishingSitesApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.phisning.PhishingDetectingService
|
||||
|
||||
interface PhishingSitesRepository {
|
||||
|
||||
suspend fun syncPhishingSites()
|
||||
|
||||
suspend fun isPhishing(url: String): Boolean
|
||||
}
|
||||
|
||||
class PhishingSitesRepositoryImpl(
|
||||
private val phishingSitesDao: PhishingSitesDao,
|
||||
private val phishingSitesApi: PhishingSitesApi,
|
||||
private val phishingDetectingService: PhishingDetectingService
|
||||
) : PhishingSitesRepository {
|
||||
|
||||
override suspend fun syncPhishingSites() {
|
||||
val remotePhishingSites = retryUntilDone { phishingSitesApi.getPhishingSites() }
|
||||
val toInsert = remotePhishingSites.deny.map(::PhishingSiteLocal)
|
||||
|
||||
phishingSitesDao.updatePhishingSites(toInsert)
|
||||
}
|
||||
|
||||
override suspend fun isPhishing(url: String): Boolean {
|
||||
return phishingDetectingService.isPhishing(url)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.BrowserHostSettings
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.BrowserHostSettingsRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.mappers.mapBrowserHostSettingsFromLocal
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.mappers.mapBrowserHostSettingsToLocal
|
||||
|
||||
class RealBrowserHostSettingsRepository(
|
||||
private val browserHostSettingsDao: BrowserHostSettingsDao
|
||||
) : BrowserHostSettingsRepository {
|
||||
override suspend fun getBrowserHostSettings(url: String): BrowserHostSettings? {
|
||||
val hostUrl = Urls.normalizeUrl(url)
|
||||
val localSettings = browserHostSettingsDao.getBrowserHostSettings(hostUrl)
|
||||
return localSettings?.let { mapBrowserHostSettingsFromLocal(it) }
|
||||
}
|
||||
|
||||
override suspend fun saveBrowserHostSettings(settings: BrowserHostSettings) {
|
||||
val localSettings = mapBrowserHostSettingsToLocal(settings)
|
||||
browserHostSettingsDao.insertBrowserHostSettings(localSettings)
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository
|
||||
|
||||
import io.novafoundation.nova.common.utils.CollectionDiffer
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.retryUntilDone
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.core_db.model.BrowserHostSettingsLocal
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCatalog
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
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 kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class RealDAppMetadataRepository(
|
||||
private val dappMetadataApi: DappMetadataApi,
|
||||
private val remoteApiUrl: String,
|
||||
private val browserHostSettingsDao: BrowserHostSettingsDao
|
||||
) : DAppMetadataRepository {
|
||||
|
||||
private val dappMetadatasFlow = MutableSharedFlow<DappCatalog>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
|
||||
override suspend fun isDAppsSynced(): Boolean {
|
||||
return dappMetadatasFlow.replayCache.isNotEmpty()
|
||||
}
|
||||
|
||||
override suspend fun syncDAppMetadatas() {
|
||||
val response = retryUntilDone { dappMetadataApi.getParachainMetadata(remoteApiUrl) }
|
||||
val dappMetadatas = mapDAppMetadataResponseToDAppMetadatas(response)
|
||||
syncHostSettings(response.dapps)
|
||||
dappMetadatasFlow.emit(dappMetadatas)
|
||||
}
|
||||
|
||||
override suspend fun syncAndGetDapp(baseUrl: String): DappMetadata? {
|
||||
syncDAppMetadatas()
|
||||
|
||||
return getDAppMetadata(baseUrl)
|
||||
}
|
||||
|
||||
override suspend fun getDAppMetadata(baseUrl: String): DappMetadata? {
|
||||
return dappMetadatasFlow.first()
|
||||
.dApps
|
||||
.find { it.baseUrl == baseUrl }
|
||||
}
|
||||
|
||||
override suspend fun findDAppMetadataByExactUrlMatch(fullUrl: String): DappMetadata? {
|
||||
return dappMetadatasFlow.first()
|
||||
.dApps
|
||||
.find { it.url == fullUrl }
|
||||
}
|
||||
|
||||
override suspend fun findDAppMetadatasByBaseUrlMatch(baseUrl: String): List<DappMetadata> {
|
||||
return dappMetadatasFlow.first()
|
||||
.dApps
|
||||
.filter { it.baseUrl == baseUrl }
|
||||
}
|
||||
|
||||
override suspend fun getDAppCatalog(): DappCatalog {
|
||||
return dappMetadatasFlow.first()
|
||||
}
|
||||
|
||||
override fun observeDAppCatalog(): Flow<DappCatalog> {
|
||||
return dappMetadatasFlow
|
||||
}
|
||||
|
||||
private suspend fun syncHostSettings(dappMetadatas: List<DappMetadataRemote>) {
|
||||
val oldSettings = browserHostSettingsDao.getBrowserAllHostSettings()
|
||||
val newSettings = dappMetadatas
|
||||
.filter { it.desktopOnly != null }
|
||||
.map { BrowserHostSettingsLocal(Urls.normalizeUrl(it.url), it.desktopOnly!!) }
|
||||
val differ = CollectionDiffer.findDiff(newSettings, oldSettings, false)
|
||||
browserHostSettingsDao.insertBrowserHostSettings(differ.added)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository.tabs
|
||||
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.BrowserTabExternalRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.BrowserTab
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.PageSnapshot
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface BrowserTabInternalRepository : BrowserTabExternalRepository {
|
||||
|
||||
suspend fun saveTab(tab: BrowserTab)
|
||||
|
||||
suspend fun removeTab(tabId: String)
|
||||
|
||||
suspend fun savePageSnapshot(tabId: String, snapshot: PageSnapshot)
|
||||
|
||||
fun observeTabs(metaId: Long): Flow<List<BrowserTab>>
|
||||
|
||||
suspend fun changeCurrentUrl(tabId: String, url: String)
|
||||
|
||||
suspend fun changeKnownDAppMetadata(tabId: String, dappIconUrl: String?)
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.data.repository.tabs
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.core_db.dao.BrowserTabsDao
|
||||
import io.novafoundation.nova.core_db.model.BrowserTabLocal
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.SimpleTabModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.BrowserTab
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.PageSnapshot
|
||||
import java.util.Date
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class RealBrowserTabRepository(
|
||||
private val browserTabsDao: BrowserTabsDao
|
||||
) : BrowserTabInternalRepository {
|
||||
|
||||
override suspend fun saveTab(tab: BrowserTab) {
|
||||
browserTabsDao.insertTab(tab.toLocal())
|
||||
}
|
||||
|
||||
override suspend fun removeTab(tabId: String) {
|
||||
browserTabsDao.removeTab(tabId)
|
||||
}
|
||||
|
||||
override fun observeTabsWithNames(metaId: Long): Flow<List<SimpleTabModel>> {
|
||||
return browserTabsDao.observeTabsByMetaId(metaId)
|
||||
.mapList {
|
||||
SimpleTabModel(it.id, it.pageName, it.knownDAppMetadata?.iconLink, it.pageIconPath)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun removeTabsForMetaAccount(metaId: Long): List<String> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
browserTabsDao.removeTabsByMetaId(metaId)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun savePageSnapshot(tabId: String, snapshot: PageSnapshot) {
|
||||
browserTabsDao.updatePageSnapshot(
|
||||
tabId = tabId,
|
||||
pageName = snapshot.pageName,
|
||||
pageIconPath = snapshot.pageIconPath,
|
||||
pagePicturePath = snapshot.pagePicturePath
|
||||
)
|
||||
}
|
||||
|
||||
override fun observeTabs(metaId: Long): Flow<List<BrowserTab>> {
|
||||
return browserTabsDao.observeTabsByMetaId(metaId).mapList { tab ->
|
||||
tab.fromLocal()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun changeCurrentUrl(tabId: String, url: String) {
|
||||
withContext(Dispatchers.Default) { browserTabsDao.updateCurrentUrl(tabId, url) }
|
||||
}
|
||||
|
||||
override suspend fun changeKnownDAppMetadata(tabId: String, dappIconUrl: String?) {
|
||||
withContext(Dispatchers.Default) { browserTabsDao.updateKnownDAppMetadata(tabId, dappIconUrl) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun BrowserTabLocal.fromLocal(): BrowserTab {
|
||||
return BrowserTab(
|
||||
id = id,
|
||||
metaId = metaId,
|
||||
currentUrl = currentUrl,
|
||||
pageSnapshot = PageSnapshot(
|
||||
pageName = pageName,
|
||||
pageIconPath = pageIconPath,
|
||||
pagePicturePath = pagePicturePath
|
||||
),
|
||||
knownDAppMetadata = knownDAppMetadata?.let { BrowserTab.KnownDAppMetadata(it.iconLink) },
|
||||
creationTime = Date(creationTime),
|
||||
)
|
||||
}
|
||||
|
||||
private fun BrowserTab.toLocal(): BrowserTabLocal {
|
||||
return BrowserTabLocal(
|
||||
id = id,
|
||||
metaId = metaId,
|
||||
currentUrl = currentUrl,
|
||||
creationTime = creationTime.time,
|
||||
pageName = pageSnapshot.pageName,
|
||||
pageIconPath = pageSnapshot.pageIconPath,
|
||||
knownDAppMetadata = knownDAppMetadata?.let { BrowserTabLocal.KnownDAppMetadata(it.iconLink) },
|
||||
pagePicturePath = pageSnapshot.pagePicturePath
|
||||
)
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di
|
||||
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import io.novafoundation.nova.common.di.CommonApi
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites.di.AddToFavouritesComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.di.AuthorizedDAppsComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.di.DAppBrowserComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.favorites.di.DAppFavoritesComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.di.MainDAppComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.di.DAppSearchComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.tab.di.BrowserTabsComponent
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
DAppFeatureDependencies::class
|
||||
],
|
||||
modules = [
|
||||
DappFeatureModule::class
|
||||
]
|
||||
)
|
||||
@FeatureScope
|
||||
interface DAppFeatureComponent : DAppFeatureApi {
|
||||
|
||||
fun mainComponentFactory(): MainDAppComponent.Factory
|
||||
|
||||
fun browserComponentFactory(): DAppBrowserComponent.Factory
|
||||
|
||||
fun browserTabsComponentFactory(): BrowserTabsComponent.Factory
|
||||
|
||||
fun dAppSearchComponentFactory(): DAppSearchComponent.Factory
|
||||
|
||||
fun dAppFavoritesComponentFactory(): DAppFavoritesComponent.Factory
|
||||
|
||||
fun addToFavouritesComponentFactory(): AddToFavouritesComponent.Factory
|
||||
|
||||
fun authorizedDAppsComponentFactory(): AuthorizedDAppsComponent.Factory
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance router: DAppRouter,
|
||||
@BindsInstance signCommunicator: ExternalSignCommunicator,
|
||||
@BindsInstance searchCommunicator: DAppSearchCommunicator,
|
||||
deps: DAppFeatureDependencies
|
||||
): DAppFeatureComponent
|
||||
}
|
||||
|
||||
@Component(
|
||||
dependencies = [
|
||||
CommonApi::class,
|
||||
DbApi::class,
|
||||
AccountFeatureApi::class,
|
||||
WalletFeatureApi::class,
|
||||
RuntimeApi::class,
|
||||
CurrencyFeatureApi::class,
|
||||
BannersFeatureApi::class,
|
||||
WalletConnectFeatureApi::class
|
||||
]
|
||||
)
|
||||
interface DAppFeatureDependenciesComponent : DAppFeatureDependencies
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di
|
||||
|
||||
import android.content.Context
|
||||
import coil.ImageLoader
|
||||
import com.google.gson.Gson
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.data.providers.deviceid.DeviceIdProvider
|
||||
import io.novafoundation.nova.common.data.secrets.v2.SecretStoreV2
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ContextManager
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.IntegrityService
|
||||
import io.novafoundation.nova.common.utils.ToastMessageManager
|
||||
import io.novafoundation.nova.common.utils.browser.fileChoosing.WebViewFileChooserFactory
|
||||
import io.novafoundation.nova.common.utils.browser.permissions.WebViewPermissionAskerFactory
|
||||
import io.novafoundation.nova.common.utils.coroutines.RootScope
|
||||
import io.novafoundation.nova.common.utils.permissions.PermissionsAskerFactory
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.core_db.dao.BrowserTabsDao
|
||||
import io.novafoundation.nova.core_db.dao.DappAuthorizationDao
|
||||
import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao
|
||||
import io.novafoundation.nova.core_db.dao.PhishingSitesDao
|
||||
import io.novafoundation.nova.feature_account_api.data.extrinsic.ExtrinsicService
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_currency_api.domain.interfaces.CurrencyRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.TokenRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.domain.interfaces.WalletRepository
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.formatters.amount.AmountFormatter
|
||||
import io.novafoundation.nova.feature_wallet_api.presentation.mixin.fee.FeeLoaderMixin
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.runtime.di.ExtrinsicSerialization
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RuntimeVersionsRepository
|
||||
|
||||
interface DAppFeatureDependencies {
|
||||
|
||||
val amountFormatter: AmountFormatter
|
||||
|
||||
val context: Context
|
||||
|
||||
val browserTabsDao: BrowserTabsDao
|
||||
|
||||
val phishingSitesDao: PhishingSitesDao
|
||||
|
||||
val favouriteDAppsDao: FavouriteDAppsDao
|
||||
|
||||
val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
|
||||
val walletUiUseCase: WalletUiUseCase
|
||||
|
||||
val walletRepository: WalletRepository
|
||||
|
||||
val fileProvider: FileProvider
|
||||
|
||||
val contextManager: ContextManager
|
||||
|
||||
val rootScope: RootScope
|
||||
|
||||
val permissionsAskerFactory: PermissionsAskerFactory
|
||||
|
||||
val bannerSourceFactory: BannersSourceFactory
|
||||
|
||||
val bannersMixinFactory: PromotionBannersMixinFactory
|
||||
|
||||
val webViewPermissionAskerFactory: WebViewPermissionAskerFactory
|
||||
|
||||
val webViewFileChooserFactory: WebViewFileChooserFactory
|
||||
|
||||
val preferences: Preferences
|
||||
|
||||
val walletConnectService: WalletConnectService
|
||||
|
||||
val automaticInteractionGate: AutomaticInteractionGate
|
||||
|
||||
val integrityService: IntegrityService
|
||||
|
||||
val deviceIdProvider: DeviceIdProvider
|
||||
|
||||
fun currencyRepository(): CurrencyRepository
|
||||
|
||||
fun accountRepository(): AccountRepository
|
||||
|
||||
fun resourceManager(): ResourceManager
|
||||
|
||||
fun appLinksProvider(): AppLinksProvider
|
||||
|
||||
fun selectedAccountUseCase(): SelectedAccountUseCase
|
||||
|
||||
fun addressIconGenerator(): AddressIconGenerator
|
||||
|
||||
fun gson(): Gson
|
||||
|
||||
fun chainRegistry(): ChainRegistry
|
||||
|
||||
fun imageLoader(): ImageLoader
|
||||
|
||||
fun feeLoaderMixinFactory(): FeeLoaderMixin.Factory
|
||||
|
||||
fun extrinsicService(): ExtrinsicService
|
||||
|
||||
fun tokenRepository(): TokenRepository
|
||||
|
||||
fun secretStoreV2(): SecretStoreV2
|
||||
|
||||
@ExtrinsicSerialization
|
||||
fun extrinsicGson(): Gson
|
||||
|
||||
fun apiCreator(): NetworkApiCreator
|
||||
|
||||
fun runtimeVersionsRepository(): RuntimeVersionsRepository
|
||||
|
||||
fun dappAuthorizationDao(): DappAuthorizationDao
|
||||
|
||||
fun browserHostSettingsDao(): BrowserHostSettingsDao
|
||||
|
||||
fun toastMessageManager(): ToastMessageManager
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di
|
||||
|
||||
import io.novafoundation.nova.common.di.FeatureApiHolder
|
||||
import io.novafoundation.nova.common.di.FeatureContainer
|
||||
import io.novafoundation.nova.common.di.scope.ApplicationScope
|
||||
import io.novafoundation.nova.core_db.di.DbApi
|
||||
import io.novafoundation.nova.feature_account_api.di.AccountFeatureApi
|
||||
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
|
||||
import io.novafoundation.nova.feature_currency_api.di.CurrencyFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_api.di.WalletFeatureApi
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.di.WalletConnectFeatureApi
|
||||
import io.novafoundation.nova.runtime.di.RuntimeApi
|
||||
import javax.inject.Inject
|
||||
|
||||
@ApplicationScope
|
||||
class DAppFeatureHolder @Inject constructor(
|
||||
featureContainer: FeatureContainer,
|
||||
private val router: DAppRouter,
|
||||
private val signCommunicator: ExternalSignCommunicator,
|
||||
private val searchCommunicator: DAppSearchCommunicator,
|
||||
) : FeatureApiHolder(featureContainer) {
|
||||
|
||||
override fun initializeDependencies(): Any {
|
||||
val dApp = DaggerDAppFeatureComponent_DAppFeatureDependenciesComponent.builder()
|
||||
.commonApi(commonApi())
|
||||
.dbApi(getFeature(DbApi::class.java))
|
||||
.accountFeatureApi(getFeature(AccountFeatureApi::class.java))
|
||||
.walletFeatureApi(getFeature(WalletFeatureApi::class.java))
|
||||
.runtimeApi(getFeature(RuntimeApi::class.java))
|
||||
.bannersFeatureApi(getFeature(BannersFeatureApi::class.java))
|
||||
.currencyFeatureApi(getFeature(CurrencyFeatureApi::class.java))
|
||||
.walletConnectFeatureApi(getFeature(WalletConnectFeatureApi::class.java))
|
||||
.build()
|
||||
|
||||
return DaggerDAppFeatureComponent.factory()
|
||||
.create(router, signCommunicator, searchCommunicator, dApp)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.BrowserTabsModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.DappMetadataModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.FavouritesDAppModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.PhishingSitesModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.Web3Module
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.deeplinks.DeepLinkModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
|
||||
@Module(
|
||||
includes = [
|
||||
Web3Module::class,
|
||||
DappMetadataModule::class,
|
||||
PhishingSitesModule::class,
|
||||
FavouritesDAppModule::class,
|
||||
BrowserTabsModule::class,
|
||||
DeepLinkModule::class
|
||||
]
|
||||
)
|
||||
class DappFeatureModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideCommonInteractor(
|
||||
dAppMetadataRepository: DAppMetadataRepository,
|
||||
favouritesDAppRepository: FavouritesDAppRepository,
|
||||
phishingSitesRepository: PhishingSitesRepository
|
||||
) = DappInteractor(
|
||||
dAppMetadataRepository = dAppMetadataRepository,
|
||||
favouritesDAppRepository = favouritesDAppRepository,
|
||||
phishingSitesRepository = phishingSitesRepository
|
||||
)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.data.providers.deviceid.DeviceIdProvider
|
||||
import io.novafoundation.nova.common.data.storage.Preferences
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.interfaces.FileProvider
|
||||
import io.novafoundation.nova.common.resources.ContextManager
|
||||
import io.novafoundation.nova.common.utils.IntegrityService
|
||||
import io.novafoundation.nova.common.utils.coroutines.RootScope
|
||||
import io.novafoundation.nova.core_db.dao.BrowserTabsDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.BrowserTabExternalRepository
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.tabs.BrowserTabInternalRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.RealPageSnapshotBuilder
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.RealBrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.tabs.RealBrowserTabRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.integrityCheck.IntegrityCheckJSBridgeFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.integrityCheck.IntegrityCheckSessionFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.PageSnapshotBuilder
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.RealTabMemoryRestrictionService
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.TabMemoryRestrictionService
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.BrowserTabSessionFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.CompoundWeb3Injector
|
||||
|
||||
@Module
|
||||
class BrowserTabsModule {
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideBrowserTabStorage(
|
||||
browserTabsDao: BrowserTabsDao,
|
||||
): BrowserTabInternalRepository {
|
||||
return RealBrowserTabRepository(browserTabsDao = browserTabsDao)
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideBrowserTabRepository(
|
||||
repository: BrowserTabInternalRepository,
|
||||
): BrowserTabExternalRepository {
|
||||
return repository
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun providePageSnapshotBuilder(fileProvider: FileProvider, rootScope: RootScope): PageSnapshotBuilder {
|
||||
return RealPageSnapshotBuilder(fileProvider, rootScope)
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideTabMemoryRestrictionService(context: Context): TabMemoryRestrictionService {
|
||||
return RealTabMemoryRestrictionService(context)
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideIntegrityCheckSessionFactory(
|
||||
apiCreator: NetworkApiCreator,
|
||||
preferences: Preferences,
|
||||
integrityService: IntegrityService,
|
||||
deviceIdProvider: DeviceIdProvider
|
||||
) = IntegrityCheckSessionFactory(
|
||||
apiCreator,
|
||||
preferences,
|
||||
integrityService,
|
||||
deviceIdProvider
|
||||
)
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideIntegrityCheckProviderFactory(
|
||||
integrityCheckSessionFactory: IntegrityCheckSessionFactory
|
||||
) = IntegrityCheckJSBridgeFactory(integrityCheckSessionFactory)
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun providePageSessionFactory(
|
||||
compoundWeb3Injector: CompoundWeb3Injector,
|
||||
contextManager: ContextManager,
|
||||
integrityCheckJSBridgeFactory: IntegrityCheckJSBridgeFactory
|
||||
): BrowserTabSessionFactory {
|
||||
return BrowserTabSessionFactory(compoundWeb3Injector, contextManager, integrityCheckJSBridgeFactory)
|
||||
}
|
||||
|
||||
@FeatureScope
|
||||
@Provides
|
||||
fun provideBrowserTabPoolService(
|
||||
accountRepository: AccountRepository,
|
||||
dAppMetadataRepository: DAppMetadataRepository,
|
||||
browserTabInternalRepository: BrowserTabInternalRepository,
|
||||
pageSnapshotBuilder: PageSnapshotBuilder,
|
||||
tabMemoryRestrictionService: TabMemoryRestrictionService,
|
||||
browserTabSessionFactory: BrowserTabSessionFactory,
|
||||
rootScope: RootScope
|
||||
): BrowserTabService {
|
||||
return RealBrowserTabService(
|
||||
browserTabInternalRepository = browserTabInternalRepository,
|
||||
pageSnapshotBuilder = pageSnapshotBuilder,
|
||||
tabMemoryRestrictionService = tabMemoryRestrictionService,
|
||||
browserTabSessionFactory = browserTabSessionFactory,
|
||||
accountRepository = accountRepository,
|
||||
dAppMetadataRepository = dAppMetadataRepository,
|
||||
rootScope = rootScope
|
||||
)
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
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.RealDAppMetadataRepository
|
||||
|
||||
@Module
|
||||
class DappMetadataModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideApi(
|
||||
apiCreator: NetworkApiCreator
|
||||
) = apiCreator.create(DappMetadataApi::class.java)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDRepository(
|
||||
api: DappMetadataApi,
|
||||
dappHostSettingsDao: BrowserHostSettingsDao
|
||||
): DAppMetadataRepository = RealDAppMetadataRepository(
|
||||
dappMetadataApi = api,
|
||||
remoteApiUrl = BuildConfig.DAPP_METADATAS_URL,
|
||||
browserHostSettingsDao = dappHostSettingsDao
|
||||
)
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.FavouriteDAppsDao
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.DbFavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
|
||||
@Module
|
||||
class FavouritesDAppModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideFavouritesDAppRepository(
|
||||
dao: FavouriteDAppsDao
|
||||
): FavouritesDAppRepository = DbFavouritesDAppRepository(dao)
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.data.network.NetworkApiCreator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.core_db.dao.PhishingSitesDao
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.network.phishing.PhishingSitesApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.phisning.BlackListPhishingDetectingService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.phisning.CompoundPhishingDetectingService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.phisning.DomainListPhishingDetectingService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.phisning.PhishingDetectingService
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepositoryImpl
|
||||
|
||||
private val PHISHING_DOMAINS = listOf("top")
|
||||
|
||||
@Module
|
||||
class PhishingSitesModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePhishingDetectingService(
|
||||
phishingSitesDao: PhishingSitesDao
|
||||
): PhishingDetectingService {
|
||||
return CompoundPhishingDetectingService(
|
||||
listOf(
|
||||
BlackListPhishingDetectingService(phishingSitesDao),
|
||||
DomainListPhishingDetectingService(PHISHING_DOMAINS)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePhishingSitesApi(networkApiCreator: NetworkApiCreator): PhishingSitesApi {
|
||||
return networkApiCreator.create(PhishingSitesApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePhishingSitesRepository(
|
||||
api: PhishingSitesApi,
|
||||
phishingSitesDao: PhishingSitesDao,
|
||||
phishingDetectingService: PhishingDetectingService
|
||||
): PhishingSitesRepository = PhishingSitesRepositoryImpl(phishingSitesDao, api, phishingDetectingService)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.core_db.dao.DappAuthorizationDao
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.web3.MetamaskModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.modules.web3.PolkadotJsModule
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.states.MetamaskStateFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.transport.MetamaskInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.transport.MetamaskTransportFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsTransportFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.states.PolkadotJsStateFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.DbWeb3Session
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.ExtensionStoreFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.CompoundWeb3Injector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewScriptInjector
|
||||
|
||||
@Module(includes = [PolkadotJsModule::class, MetamaskModule::class])
|
||||
class Web3Module {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWebViewHolder() = WebViewHolder()
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideScriptInjector(
|
||||
resourceManager: ResourceManager,
|
||||
) = WebViewScriptInjector(resourceManager)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWeb3InjectorPool(
|
||||
polkadotJsInjector: PolkadotJsInjector,
|
||||
metamaskInjector: MetamaskInjector,
|
||||
) = CompoundWeb3Injector(
|
||||
injectors = listOf(
|
||||
polkadotJsInjector,
|
||||
metamaskInjector
|
||||
)
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideWeb3Session(
|
||||
dappAuthorizationDao: DappAuthorizationDao
|
||||
): Web3Session = DbWeb3Session(dappAuthorizationDao)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideExtensionStoreFactory(
|
||||
polkadotJsStateFactory: PolkadotJsStateFactory,
|
||||
polkadotJsTransportFactory: PolkadotJsTransportFactory,
|
||||
metamaskStateFactory: MetamaskStateFactory,
|
||||
metamaskTransportFactory: MetamaskTransportFactory,
|
||||
) = ExtensionStoreFactory(
|
||||
polkadotJsStateFactory = polkadotJsStateFactory,
|
||||
polkadotJsTransportFactory = polkadotJsTransportFactory,
|
||||
metamaskStateFactory = metamaskStateFactory,
|
||||
metamaskTransportFactory = metamaskTransportFactory
|
||||
)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules.deeplinks
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_api.di.deeplinks.DAppDeepLinks
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.deeplink.DAppDeepLinkHandler
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
|
||||
@Module
|
||||
class DeepLinkModule {
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDappDeepLinkHandler(
|
||||
dAppMetadataRepository: DAppMetadataRepository,
|
||||
router: DAppRouter,
|
||||
automaticInteractionGate: AutomaticInteractionGate,
|
||||
browserTabService: BrowserTabService
|
||||
) = DAppDeepLinkHandler(
|
||||
dAppMetadataRepository,
|
||||
router,
|
||||
automaticInteractionGate,
|
||||
browserTabService
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideDeepLinks(dapp: DAppDeepLinkHandler): DAppDeepLinks {
|
||||
return DAppDeepLinks(listOf(dapp))
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules.web3
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_dapp_impl.BuildConfig
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.DefaultMetamaskChainRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.RealDefaultMetamaskChainRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.metamask.MetamaskInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.di.Metamask
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.states.MetamaskStateFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.transport.MetamaskInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.transport.MetamaskResponder
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.transport.MetamaskTransportFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewScriptInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewWeb3JavaScriptInterface
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [MetamaskModule.BindsModule::class])
|
||||
class MetamaskModule {
|
||||
|
||||
@Module
|
||||
interface BindsModule {
|
||||
|
||||
@Binds
|
||||
fun bindDefaultMetamaskChainRepository(implementation: RealDefaultMetamaskChainRepository): DefaultMetamaskChainRepository
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Metamask
|
||||
@FeatureScope
|
||||
fun provideWeb3JavaScriptInterface() = WebViewWeb3JavaScriptInterface()
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInjector(
|
||||
gson: Gson,
|
||||
webViewScriptInjector: WebViewScriptInjector,
|
||||
@Metamask web3JavaScriptInterface: WebViewWeb3JavaScriptInterface,
|
||||
) = MetamaskInjector(
|
||||
isDebug = BuildConfig.DEBUG,
|
||||
gson = gson,
|
||||
jsInterface = web3JavaScriptInterface,
|
||||
webViewScriptInjector = webViewScriptInjector
|
||||
)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideResponder(webViewHolder: WebViewHolder): MetamaskResponder {
|
||||
return MetamaskResponder(webViewHolder)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideTransportFactory(
|
||||
responder: MetamaskResponder,
|
||||
@Metamask web3JavaScriptInterface: WebViewWeb3JavaScriptInterface,
|
||||
gson: Gson
|
||||
): MetamaskTransportFactory {
|
||||
return MetamaskTransportFactory(
|
||||
webViewWeb3JavaScriptInterface = web3JavaScriptInterface,
|
||||
gson = gson,
|
||||
responder = responder,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
chainRegistry: ChainRegistry,
|
||||
defaultMetamaskChainRepository: DefaultMetamaskChainRepository,
|
||||
) = MetamaskInteractor(accountRepository, chainRegistry, defaultMetamaskChainRepository)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideStateFactory(
|
||||
interactor: MetamaskInteractor,
|
||||
commonInteractor: DappInteractor,
|
||||
resourceManager: ResourceManager,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
web3Session: Web3Session,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
): MetamaskStateFactory {
|
||||
return MetamaskStateFactory(
|
||||
interactor = interactor,
|
||||
commonInteractor = commonInteractor,
|
||||
resourceManager = resourceManager,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
web3Session = web3Session,
|
||||
walletUiUseCase = walletUiUseCase
|
||||
)
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.di.modules.web3
|
||||
|
||||
import com.google.gson.Gson
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import io.novafoundation.nova.common.address.AddressIconGenerator
|
||||
import io.novafoundation.nova.common.di.scope.FeatureScope
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.polkadotJs.PolkadotJsExtensionInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsResponder
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.PolkadotJsTransportFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.di.PolkadotJs
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.states.PolkadotJsStateFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewScriptInjector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewWeb3JavaScriptInterface
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RuntimeVersionsRepository
|
||||
|
||||
@Module
|
||||
class PolkadotJsModule {
|
||||
|
||||
@Provides
|
||||
@PolkadotJs
|
||||
@FeatureScope
|
||||
fun provideWeb3JavaScriptInterface() = WebViewWeb3JavaScriptInterface()
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePolkadotJsInjector(
|
||||
webViewScriptInjector: WebViewScriptInjector,
|
||||
@PolkadotJs web3JavaScriptInterface: WebViewWeb3JavaScriptInterface,
|
||||
) = PolkadotJsInjector(web3JavaScriptInterface, webViewScriptInjector)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun provideResponder(webViewHolder: WebViewHolder): PolkadotJsResponder {
|
||||
return PolkadotJsResponder(webViewHolder)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePolkadotJsTransportFactory(
|
||||
web3Responder: PolkadotJsResponder,
|
||||
@PolkadotJs web3JavaScriptInterface: WebViewWeb3JavaScriptInterface,
|
||||
gson: Gson
|
||||
): PolkadotJsTransportFactory {
|
||||
return PolkadotJsTransportFactory(
|
||||
webViewWeb3JavaScriptInterface = web3JavaScriptInterface,
|
||||
gson = gson,
|
||||
web3Responder = web3Responder,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePolkadotJsInteractor(
|
||||
chainRegistry: ChainRegistry,
|
||||
runtimeVersionsRepository: RuntimeVersionsRepository,
|
||||
accountRepository: AccountRepository
|
||||
) = PolkadotJsExtensionInteractor(chainRegistry, accountRepository, runtimeVersionsRepository)
|
||||
|
||||
@Provides
|
||||
@FeatureScope
|
||||
fun providePolkadotJsStateFactory(
|
||||
interactor: PolkadotJsExtensionInteractor,
|
||||
commonInteractor: DappInteractor,
|
||||
resourceManager: ResourceManager,
|
||||
addressIconGenerator: AddressIconGenerator,
|
||||
web3Session: Web3Session,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
): PolkadotJsStateFactory {
|
||||
return PolkadotJsStateFactory(
|
||||
interactor = interactor,
|
||||
commonInteractor = commonInteractor,
|
||||
resourceManager = resourceManager,
|
||||
addressIconGenerator = addressIconGenerator,
|
||||
web3Session = web3Session,
|
||||
walletUiUseCase = walletUiUseCase
|
||||
)
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DApp
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DAppGroupedCatalog
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DAppUrl
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCategory
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.DAppInfo
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.buildUrlToDappMapping
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.createDAppComparator
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.joinAll
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.random.Random
|
||||
|
||||
class DappInteractor(
|
||||
private val dAppMetadataRepository: DAppMetadataRepository,
|
||||
private val favouritesDAppRepository: FavouritesDAppRepository,
|
||||
private val phishingSitesRepository: PhishingSitesRepository,
|
||||
) {
|
||||
|
||||
private val dAppComparator by lazy {
|
||||
createDAppComparator()
|
||||
}
|
||||
|
||||
suspend fun dAppsSync() = withContext(Dispatchers.IO) {
|
||||
val metadataSyncing = runSync { dAppMetadataRepository.syncDAppMetadatas() }
|
||||
val phishingSitesSyncing = runSync { phishingSitesRepository.syncPhishingSites() }
|
||||
|
||||
joinAll(metadataSyncing, phishingSitesSyncing)
|
||||
}
|
||||
|
||||
suspend fun removeDAppFromFavourites(dAppUrl: String) {
|
||||
favouritesDAppRepository.removeFavourite(dAppUrl)
|
||||
}
|
||||
|
||||
suspend fun getFavoriteDApps(): List<FavouriteDApp> {
|
||||
return favouritesDAppRepository.getFavourites().sortDApps()
|
||||
}
|
||||
|
||||
fun observeFavoriteDApps(): Flow<List<FavouriteDApp>> {
|
||||
return favouritesDAppRepository.observeFavourites()
|
||||
.map { it.sortDApps() }
|
||||
}
|
||||
|
||||
fun observeDAppsByCategory(): Flow<DAppGroupedCatalog> {
|
||||
val shufflingSeed = Random.nextInt()
|
||||
|
||||
return combine(
|
||||
dAppMetadataRepository.observeDAppCatalog(),
|
||||
favouritesDAppRepository.observeFavourites()
|
||||
) { dAppCatalog, favourites ->
|
||||
// We use random with seed to shuffle dapps in categories the same way during updates
|
||||
val random = Random(shufflingSeed)
|
||||
|
||||
val categories = dAppCatalog.categories
|
||||
val dapps = dAppCatalog.dApps
|
||||
|
||||
val urlToDAppMapping = buildUrlToDappMapping(dapps, favourites)
|
||||
|
||||
val popular = dAppCatalog.popular.mapNotNull { urlToDAppMapping[it] }
|
||||
val catalog = categories.associateWith { getShuffledDAppsInCategory(it, dapps, urlToDAppMapping, dAppCatalog.popular.toSet(), random) }
|
||||
|
||||
DAppGroupedCatalog(popular, catalog)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getShuffledDAppsInCategory(
|
||||
category: DappCategory,
|
||||
dapps: List<DappMetadata>,
|
||||
urlToDAppMapping: Map<String, DApp>,
|
||||
popular: Set<DAppUrl>,
|
||||
shufflingSeed: Random
|
||||
): List<DApp> {
|
||||
val categoryDApps = dapps.filter { category in it.categories }
|
||||
.map { urlToDAppMapping.getValue(it.url) }
|
||||
|
||||
val popularDAppsInCategory = categoryDApps.filter { it.url in popular }
|
||||
val otherDAppsInCategory = categoryDApps.filterNot { it.url in popular }
|
||||
|
||||
return popularDAppsInCategory.shuffled(shufflingSeed) + otherDAppsInCategory.shuffled(shufflingSeed)
|
||||
}
|
||||
|
||||
suspend fun getDAppInfo(dAppUrl: String): DAppInfo {
|
||||
val baseUrl = Urls.normalizeUrl(dAppUrl)
|
||||
|
||||
return withContext(Dispatchers.Default) {
|
||||
DAppInfo(
|
||||
baseUrl = baseUrl,
|
||||
metadata = dAppMetadataRepository.getDAppMetadata(baseUrl)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun CoroutineScope.runSync(crossinline sync: suspend () -> Unit): Job {
|
||||
return async { runCatching { sync() } }
|
||||
}
|
||||
|
||||
suspend fun updateFavoriteDapps(favoriteDapps: List<FavouriteDApp>) {
|
||||
favouritesDAppRepository.updateFavoriteDapps(favoriteDapps)
|
||||
}
|
||||
|
||||
private fun List<FavouriteDApp>.sortDApps(): List<FavouriteDApp> {
|
||||
return sortedBy { it.orderingIndex }
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.authorizedDApps
|
||||
|
||||
class AuthorizedDApp(
|
||||
val baseUrl: String,
|
||||
val name: String?,
|
||||
val iconLink: String?
|
||||
)
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.authorizedDApps
|
||||
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session.Authorization
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
class AuthorizedDAppsInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val metadataRepository: DAppMetadataRepository,
|
||||
private val web3Session: Web3Session
|
||||
) {
|
||||
|
||||
suspend fun revokeAuthorization(url: String) {
|
||||
val currentAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
web3Session.revokeAuthorization(url, currentAccount.id)
|
||||
}
|
||||
|
||||
fun observeAuthorizedDApps(): Flow<List<AuthorizedDApp>> {
|
||||
return accountRepository.selectedMetaAccountFlow().flatMapLatest { metaAccount ->
|
||||
val catalog = metadataRepository.getDAppCatalog()
|
||||
val dApps = catalog.dApps.associateBy(DappMetadata::baseUrl)
|
||||
|
||||
web3Session.observeAuthorizationsFor(metaAccount.id)
|
||||
.map { authorizations -> authorizations.filter { it.state == Authorization.State.ALLOWED } }
|
||||
.mapList { authorization ->
|
||||
val metadata = dApps[authorization.baseUrl]
|
||||
|
||||
AuthorizedDApp(
|
||||
baseUrl = authorization.baseUrl,
|
||||
name = metadata?.name ?: authorization.dAppTitle,
|
||||
iconLink = metadata?.iconLink
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser
|
||||
|
||||
class BrowserPage(
|
||||
val url: String,
|
||||
val title: String?,
|
||||
val synchronizedWithBrowser: Boolean
|
||||
)
|
||||
|
||||
class BrowserPageAnalyzed(
|
||||
val display: String,
|
||||
val title: String?,
|
||||
val url: String,
|
||||
val synchronizedWithBrowser: Boolean,
|
||||
val isFavourite: Boolean,
|
||||
val security: Security
|
||||
) {
|
||||
|
||||
enum class Security {
|
||||
SECURE, DANGEROUS, UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
val BrowserPageAnalyzed.isSecure
|
||||
get() = security == BrowserPageAnalyzed.Security.SECURE
|
||||
|
||||
val BrowserPageAnalyzed.isDangerous
|
||||
get() = security == BrowserPageAnalyzed.Security.DANGEROUS
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser
|
||||
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
|
||||
class DAppInfo(
|
||||
val baseUrl: String,
|
||||
val metadata: DappMetadata?
|
||||
)
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.isSecure
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.BrowserHostSettings
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.BrowserHostSettingsRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.net.URL
|
||||
|
||||
class DappBrowserInteractor(
|
||||
private val phishingSitesRepository: PhishingSitesRepository,
|
||||
private val favouritesDAppRepository: FavouritesDAppRepository,
|
||||
private val browserHostSettingsRepository: BrowserHostSettingsRepository
|
||||
) {
|
||||
|
||||
suspend fun getHostSettings(url: String): BrowserHostSettings? {
|
||||
return browserHostSettingsRepository.getBrowserHostSettings(url)
|
||||
}
|
||||
|
||||
suspend fun saveHostSettings(settings: BrowserHostSettings) {
|
||||
browserHostSettingsRepository.saveBrowserHostSettings(settings)
|
||||
}
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun observeBrowserPageFor(browserPage: BrowserPage): Flow<BrowserPageAnalyzed> {
|
||||
return favouritesDAppRepository.observeIsFavourite(browserPage.url).map { isFavourite ->
|
||||
runCatching {
|
||||
val security = when {
|
||||
phishingSitesRepository.isPhishing(browserPage.url) -> BrowserPageAnalyzed.Security.DANGEROUS
|
||||
URL(browserPage.url).isSecure -> BrowserPageAnalyzed.Security.SECURE
|
||||
else -> BrowserPageAnalyzed.Security.UNKNOWN
|
||||
}
|
||||
BrowserPageAnalyzed(
|
||||
display = Urls.hostOf(browserPage.url),
|
||||
title = browserPage.title,
|
||||
url = browserPage.url,
|
||||
security = security,
|
||||
isFavourite = isFavourite,
|
||||
synchronizedWithBrowser = browserPage.synchronizedWithBrowser
|
||||
)
|
||||
}.getOrElse {
|
||||
BrowserPageAnalyzed(
|
||||
display = browserPage.url,
|
||||
title = browserPage.title,
|
||||
url = browserPage.url,
|
||||
isFavourite = isFavourite,
|
||||
security = BrowserPageAnalyzed.Security.UNKNOWN,
|
||||
synchronizedWithBrowser = browserPage.synchronizedWithBrowser
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser.addToFavourites
|
||||
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AddToFavouritesInteractor(
|
||||
private val favouritesDAppRepository: FavouritesDAppRepository,
|
||||
private val dAppMetadataRepository: DAppMetadataRepository,
|
||||
) {
|
||||
|
||||
suspend fun addToFavourites(url: String, label: String, icon: String?) = withContext(Dispatchers.Default) {
|
||||
val nextOrderingIndex = favouritesDAppRepository.getNextOrderingIndex()
|
||||
|
||||
val favorite = FavouriteDApp(
|
||||
url = url,
|
||||
label = label,
|
||||
icon = icon,
|
||||
orderingIndex = nextOrderingIndex
|
||||
)
|
||||
favouritesDAppRepository.addFavourite(favorite)
|
||||
}
|
||||
|
||||
suspend fun addToFavourites(favouriteDApp: FavouriteDApp) = withContext(Dispatchers.Default) {
|
||||
favouritesDAppRepository.addFavourite(favouriteDApp)
|
||||
}
|
||||
|
||||
suspend fun resolveFavouriteDAppDisplay(url: String, suppliedLabel: String?) = withContext(Dispatchers.Default) {
|
||||
val dAppMetadataExactMatch = dAppMetadataRepository.findDAppMetadataByExactUrlMatch(url)
|
||||
val dAppMetadataBaseUrlMatches = dAppMetadataRepository.findDAppMetadatasByBaseUrlMatch(baseUrl = Urls.normalizeUrl(url))
|
||||
|
||||
// we don't want to use base url match if there more than one candidate
|
||||
val dAppMetadataBaseUrlSingleMatch = dAppMetadataBaseUrlMatches.singleOrNull()
|
||||
|
||||
FavouriteDApp(
|
||||
url = url,
|
||||
label = dAppMetadataExactMatch?.name ?: suppliedLabel ?: Urls.hostOf(url),
|
||||
icon = dAppMetadataExactMatch?.iconLink ?: dAppMetadataBaseUrlSingleMatch?.iconLink,
|
||||
orderingIndex = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser.metamask
|
||||
|
||||
import android.util.Log
|
||||
import io.novafoundation.nova.core.model.CryptoType
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.addressIn
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.mainEthereumAddress
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.DefaultMetamaskChainRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.model.EthereumAddress
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.metamask.model.MetamaskChain
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.findEvmChainFromHexId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class MetamaskInteractor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val defaultMetamaskChainRepository: DefaultMetamaskChainRepository,
|
||||
) {
|
||||
|
||||
fun getDefaultMetamaskChain(): MetamaskChain {
|
||||
val defaultChain = defaultMetamaskChainRepository.getDefaultMetamaskChain() ?: MetamaskChain.ETHEREUM
|
||||
return defaultChain.also {
|
||||
Log.d("MetamaskInteractor", "Returned default chain: ${defaultChain.chainName}")
|
||||
}
|
||||
}
|
||||
|
||||
fun setDefaultMetamaskChain(chain: MetamaskChain) {
|
||||
Log.d("MetamaskInteractor", "Saved default chain: ${chain.chainName}")
|
||||
defaultMetamaskChainRepository.saveDefaultMetamaskChain(chain)
|
||||
}
|
||||
|
||||
suspend fun getAddresses(ethereumChainId: String): List<EthereumAddress> = withContext(Dispatchers.Default) {
|
||||
val selectedAccount = accountRepository.getSelectedMetaAccount()
|
||||
val maybeChain = chainRegistry.findEvmChainFromHexId(ethereumChainId)
|
||||
|
||||
val chainsById = chainRegistry.chainsById.first()
|
||||
|
||||
val selectedAddress = maybeChain?.let { selectedAccount.addressIn(it) }
|
||||
|
||||
val mainAddress = selectedAccount.mainEthereumAddress()
|
||||
|
||||
val chainAccountAddresses = selectedAccount.chainAccounts
|
||||
.mapNotNull { (chainId, chainAccount) ->
|
||||
val chain = chainsById[chainId]
|
||||
|
||||
chain?.addressOf(chainAccount.accountId)?.takeIf {
|
||||
chain.isEthereumBased && chainAccount.cryptoType == CryptoType.ECDSA
|
||||
}
|
||||
}
|
||||
|
||||
buildList {
|
||||
selectedAddress?.let { add(it) }
|
||||
mainAddress?.let { add(it) }
|
||||
addAll(chainAccountAddresses)
|
||||
}.distinct()
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.browser.polkadotJs
|
||||
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.MetaAccount
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.defaultSubstrateAddress
|
||||
import io.novafoundation.nova.feature_account_api.domain.model.substrateFrom
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.model.InjectedAccount
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.polkadotJs.model.InjectedMetadataKnown
|
||||
import io.novafoundation.nova.runtime.ext.addressOf
|
||||
import io.novafoundation.nova.runtime.ext.genesisHash
|
||||
import io.novafoundation.nova.runtime.ext.toEthereumAddress
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chain.model.Chain
|
||||
import io.novafoundation.nova.runtime.multiNetwork.runtime.repository.RuntimeVersionsRepository
|
||||
import io.novasama.substrate_sdk_android.encrypt.MultiChainEncryption
|
||||
import io.novasama.substrate_sdk_android.extensions.requireHexPrefix
|
||||
|
||||
class PolkadotJsExtensionInteractor(
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val runtimeVersionsRepository: RuntimeVersionsRepository,
|
||||
) {
|
||||
|
||||
suspend fun getInjectedAccounts(): List<InjectedAccount> {
|
||||
val metaAccount = accountRepository.getSelectedMetaAccount()
|
||||
|
||||
val defaultSubstrateAccount = metaAccount.defaultSubstrateAddress?.let { address ->
|
||||
InjectedAccount(
|
||||
address = address,
|
||||
genesisHash = null,
|
||||
name = metaAccount.name,
|
||||
encryption = metaAccount.substrateCryptoType?.let { MultiChainEncryption.substrateFrom(it) }
|
||||
)
|
||||
}
|
||||
|
||||
val defaultEthereumAccount = metaAccount.ethereumAddress?.let { adddressBytes ->
|
||||
InjectedAccount(
|
||||
address = adddressBytes.toEthereumAddress(),
|
||||
genesisHash = null,
|
||||
name = metaAccount.name,
|
||||
encryption = MultiChainEncryption.Ethereum
|
||||
)
|
||||
}
|
||||
|
||||
val customAccounts = metaAccount.chainAccounts.mapNotNull { (chainId, chainAccount) ->
|
||||
val chain = chainRegistry.getChain(chainId)
|
||||
// Ignore non-substrate chains since they don't have chainId=genesisHash
|
||||
val genesisHash = chain.genesisHash?.requireHexPrefix() ?: return@mapNotNull null
|
||||
|
||||
InjectedAccount(
|
||||
address = chain.addressOf(chainAccount.accountId),
|
||||
genesisHash = genesisHash,
|
||||
name = "${metaAccount.name} (${chain.name})",
|
||||
encryption = chainAccount.multiChainEncryption(chain)
|
||||
)
|
||||
}
|
||||
|
||||
return buildList {
|
||||
defaultSubstrateAccount?.let(::add)
|
||||
defaultEthereumAccount?.let(::add)
|
||||
addAll(customAccounts)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getKnownInjectedMetadatas(): List<InjectedMetadataKnown> {
|
||||
return runtimeVersionsRepository.getAllRuntimeVersions().map {
|
||||
InjectedMetadataKnown(
|
||||
genesisHash = it.chainId.requireHexPrefix(),
|
||||
specVersion = it.specVersion
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MetaAccount.ChainAccount.multiChainEncryption(chain: Chain): MultiChainEncryption? {
|
||||
return if (chain.isEthereumBased) {
|
||||
MultiChainEncryption.Ethereum
|
||||
} else {
|
||||
cryptoType?.let { MultiChainEncryption.substrateFrom(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.common
|
||||
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DApp
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappMetadata
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapDappCategoriesToDescription
|
||||
|
||||
fun createDAppComparator() = compareByDescending<DApp> { it.isFavourite }
|
||||
.thenBy { it.favoriteIndex }
|
||||
.thenBy { it.name }
|
||||
|
||||
// Build mapping in O(Metadatas + Favourites) in case of HashMap. It allows constant time access later
|
||||
internal fun buildUrlToDappMapping(
|
||||
dAppMetadatas: Collection<DappMetadata>,
|
||||
favourites: Collection<FavouriteDApp>
|
||||
): Map<String, DApp> {
|
||||
val favouritesByUrl = favourites.associateBy { it.url }
|
||||
|
||||
return buildMap {
|
||||
val fromFavourites = favouritesByUrl.mapValues { favouriteToDApp(it.value) }
|
||||
putAll(fromFavourites)
|
||||
|
||||
// overlapping metadata urls will override favourites in the map and thus use metadata for display
|
||||
val fromMetadatas = dAppMetadatas.associateBy(
|
||||
keySelector = { it.url },
|
||||
valueTransform = { dAppMetadataToDApp(it, favoriteModel = favouritesByUrl[it.url]) }
|
||||
)
|
||||
putAll(fromMetadatas)
|
||||
}
|
||||
}
|
||||
|
||||
fun dappToFavorite(dapp: DApp, orderingIndex: Int): FavouriteDApp {
|
||||
return FavouriteDApp(
|
||||
label = dapp.name,
|
||||
icon = dapp.iconLink,
|
||||
url = dapp.url,
|
||||
orderingIndex = orderingIndex
|
||||
)
|
||||
}
|
||||
|
||||
fun favouriteToDApp(favouriteDApp: FavouriteDApp): DApp {
|
||||
return DApp(
|
||||
name = favouriteDApp.label,
|
||||
description = favouriteDApp.url,
|
||||
iconLink = favouriteDApp.icon,
|
||||
url = favouriteDApp.url,
|
||||
isFavourite = true,
|
||||
favoriteIndex = favouriteDApp.orderingIndex
|
||||
)
|
||||
}
|
||||
|
||||
private fun dAppMetadataToDApp(metadata: DappMetadata, favoriteModel: FavouriteDApp?): DApp {
|
||||
return DApp(
|
||||
name = metadata.name,
|
||||
description = mapDappCategoriesToDescription(metadata.categories),
|
||||
iconLink = metadata.iconLink,
|
||||
url = metadata.url,
|
||||
isFavourite = favoriteModel != null,
|
||||
favoriteIndex = favoriteModel?.orderingIndex
|
||||
)
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.search
|
||||
|
||||
enum class DappSearchGroup {
|
||||
DAPPS, SEARCH
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.search
|
||||
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DApp
|
||||
|
||||
sealed interface DappSearchResult {
|
||||
|
||||
val isTrustedByNova: Boolean
|
||||
|
||||
class Url(val url: String, override val isTrustedByNova: Boolean) : DappSearchResult
|
||||
|
||||
class Search(val query: String, val searchUrl: String) : DappSearchResult {
|
||||
override val isTrustedByNova: Boolean = false
|
||||
}
|
||||
|
||||
class Dapp(val dapp: DApp) : DappSearchResult {
|
||||
override val isTrustedByNova: Boolean = true
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.domain.search
|
||||
|
||||
import io.novafoundation.nova.common.list.GroupedList
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DApp
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCategory
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.buildUrlToDappMapping
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.createDAppComparator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SearchDappInteractor(
|
||||
private val dAppMetadataRepository: DAppMetadataRepository,
|
||||
private val favouritesDAppRepository: FavouritesDAppRepository,
|
||||
) {
|
||||
|
||||
fun categories(): Flow<List<DappCategory>> {
|
||||
return dAppMetadataRepository.observeDAppCatalog()
|
||||
.map { it.categories }
|
||||
}
|
||||
|
||||
suspend fun searchDapps(query: String, categoryId: String?): GroupedList<DappSearchGroup, DappSearchResult> = withContext(Dispatchers.Default) {
|
||||
val dapps = getDapps(categoryId)
|
||||
|
||||
val dappsGroupContent = dapps
|
||||
.filter { query.isEmpty() || query.lowercase() in it.name.lowercase() }
|
||||
.sortedWith(createDAppComparator())
|
||||
.map(DappSearchResult::Dapp)
|
||||
|
||||
val searchGroupContent = when {
|
||||
query.isEmpty() -> null
|
||||
Urls.isValidWebUrl(query) -> {
|
||||
val searchUrl = Urls.ensureHttpsProtocol(query)
|
||||
val searchUrlDomain = Urls.domainOf(searchUrl)
|
||||
val trusting = dapps.any { Urls.domainOf(it.url) == searchUrlDomain }
|
||||
DappSearchResult.Url(searchUrl, trusting)
|
||||
}
|
||||
|
||||
else -> DappSearchResult.Search(query, searchUrlFor(query))
|
||||
}
|
||||
|
||||
buildMap {
|
||||
searchGroupContent?.let {
|
||||
put(DappSearchGroup.SEARCH, listOf(searchGroupContent))
|
||||
}
|
||||
|
||||
if (dappsGroupContent.isNotEmpty()) {
|
||||
put(DappSearchGroup.DAPPS, dappsGroupContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchUrlFor(query: String): String = "https://duckduckgo.com/?q=$query"
|
||||
|
||||
private suspend fun getDapps(categoryId: String?): Collection<DApp> {
|
||||
val dApps = dAppMetadataRepository.getDAppCatalog()
|
||||
.dApps
|
||||
.filter { dapp -> categoryId == null || dapp.categories.any { it.id == categoryId } }
|
||||
.associateBy { it.url }
|
||||
val favouriteDApps = favouritesDAppRepository.getFavourites()
|
||||
.filter { categoryId == null || it.url in dApps.keys }
|
||||
|
||||
val dAppByUrlMapping = buildUrlToDappMapping(dApps.values, favouriteDApps)
|
||||
return dAppByUrlMapping.values
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation
|
||||
|
||||
import androidx.navigation.fragment.FragmentNavigator
|
||||
import io.novafoundation.nova.common.navigation.ReturnableRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
|
||||
interface DAppRouter : ReturnableRouter {
|
||||
|
||||
fun openChangeAccount()
|
||||
|
||||
fun openDAppBrowser(payload: DAppBrowserPayload, extras: FragmentNavigator.Extras? = null)
|
||||
|
||||
fun openDappSearch()
|
||||
|
||||
fun openDappSearchWithCategory(categoryId: String?)
|
||||
|
||||
fun finishDappSearch()
|
||||
|
||||
fun openAddToFavourites(payload: AddToFavouritesPayload)
|
||||
|
||||
fun openAuthorizedDApps()
|
||||
|
||||
fun openTabs()
|
||||
|
||||
fun closeTabsScreen()
|
||||
|
||||
fun openDAppFavorites()
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.utils.hideKeyboard
|
||||
import io.novafoundation.nova.common.utils.keyboard.showSoftKeyboard
|
||||
import io.novafoundation.nova.common.utils.moveCursorToTheEnd
|
||||
import io.novafoundation.nova.common.utils.postToSelf
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentAddToFavouritesBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val PAYLOAD_KEY = "DAppSignExtrinsicFragment.Payload"
|
||||
|
||||
class AddToFavouritesFragment : BaseFragment<AddToFavouritesViewModel, FragmentAddToFavouritesBinding>() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun getBundle(payload: AddToFavouritesPayload) = bundleOf(PAYLOAD_KEY to payload)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentAddToFavouritesBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
override fun initViews() {
|
||||
binder.addToFavouritesToolbar.setHomeButtonListener {
|
||||
viewModel.backClicked()
|
||||
}
|
||||
|
||||
binder.addToFavouritesToolbar.setRightActionClickListener { viewModel.saveClicked() }
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.addToFavouritesComponentFactory()
|
||||
.create(this, argument(PAYLOAD_KEY))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun subscribe(viewModel: AddToFavouritesViewModel) {
|
||||
binder.addToFavouritesTitleInput.bindTo(viewModel.labelFlow, lifecycleScope)
|
||||
binder.addToFavouritesAddressInput.bindTo(viewModel.urlFlow, lifecycleScope)
|
||||
|
||||
viewModel.iconLink.observe {
|
||||
binder.addToFavouritesIcon.showDAppIcon(it, imageLoader)
|
||||
}
|
||||
|
||||
viewModel.focusOnAddressFieldEvent.observeEvent {
|
||||
binder.addToFavouritesTitleInput.postToSelf {
|
||||
showSoftKeyboard()
|
||||
|
||||
moveCursorToTheEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.sendEvent
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.addToFavourites.AddToFavouritesInteractor
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AddToFavouritesViewModel(
|
||||
private val interactor: AddToFavouritesInteractor,
|
||||
private val payload: AddToFavouritesPayload,
|
||||
private val router: DAppRouter,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val urlFlow = MutableStateFlow(payload.url)
|
||||
val labelFlow = singleReplaySharedFlow<String>()
|
||||
|
||||
val iconLink = singleReplaySharedFlow<String?>()
|
||||
|
||||
private val _focusOnUrlFieldEvent = MutableLiveData<Event<Unit>>()
|
||||
val focusOnAddressFieldEvent: LiveData<Event<Unit>> = _focusOnUrlFieldEvent
|
||||
|
||||
init {
|
||||
setInitialValues()
|
||||
}
|
||||
|
||||
fun saveClicked() = launch {
|
||||
interactor.addToFavourites(urlFlow.value, labelFlow.first(), iconLink.first())
|
||||
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun setInitialValues() = launch {
|
||||
val resolvedDAppDisplay = interactor.resolveFavouriteDAppDisplay(url = payload.url, suppliedLabel = payload.label)
|
||||
|
||||
labelFlow.emit(resolvedDAppDisplay.label)
|
||||
iconLink.emit(resolvedDAppDisplay.icon)
|
||||
|
||||
_focusOnUrlFieldEvent.sendEvent()
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites.AddToFavouritesFragment
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AddToFavouritesModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AddToFavouritesComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: AddToFavouritesPayload,
|
||||
): AddToFavouritesComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AddToFavouritesFragment)
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.addToFavourites.AddToFavouritesInteractor
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.addToFavourites.AddToFavouritesViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AddToFavouritesModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
favouritesDAppRepository: FavouritesDAppRepository,
|
||||
dAppMetadataRepository: DAppMetadataRepository
|
||||
) = AddToFavouritesInteractor(favouritesDAppRepository, dAppMetadataRepository)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): AddToFavouritesViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(AddToFavouritesViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AddToFavouritesViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: DAppRouter,
|
||||
interactor: AddToFavouritesInteractor,
|
||||
payload: AddToFavouritesPayload
|
||||
): ViewModel {
|
||||
return AddToFavouritesViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
payload = payload
|
||||
)
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import io.novafoundation.nova.common.list.BaseListAdapter
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.model.AuthorizedDAppModel
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.view.DAppView
|
||||
|
||||
class AuthorizedDAppAdapter(
|
||||
private val handler: Handler
|
||||
) : BaseListAdapter<AuthorizedDAppModel, AuthorizedDAppViewHolder>(DiffCallback) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun onRevokeClicked(item: AuthorizedDAppModel)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AuthorizedDAppViewHolder {
|
||||
return AuthorizedDAppViewHolder(DAppView.createUsingMathParentWidth(parent.context), handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: AuthorizedDAppViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<AuthorizedDAppModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: AuthorizedDAppModel, newItem: AuthorizedDAppModel): Boolean {
|
||||
return oldItem.url == newItem.url
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AuthorizedDAppModel, newItem: AuthorizedDAppModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class AuthorizedDAppViewHolder(
|
||||
private val dAppView: DAppView,
|
||||
private val itemHandler: AuthorizedDAppAdapter.Handler,
|
||||
) : BaseViewHolder(dAppView) {
|
||||
|
||||
init {
|
||||
dAppView.setActionResource(R.drawable.ic_close)
|
||||
dAppView.setActionTintRes(R.color.icon_secondary)
|
||||
}
|
||||
|
||||
fun bind(item: AuthorizedDAppModel) = with(dAppView) {
|
||||
this.setTitle(item.title)
|
||||
this.showTitle(item.title != null)
|
||||
this.setSubtitle(item.url)
|
||||
this.setIconUrl(item.iconLink)
|
||||
|
||||
setOnActionClickListener { itemHandler.onRevokeClicked(item) }
|
||||
}
|
||||
|
||||
override fun unbind() {
|
||||
dAppView.clearIcon()
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.common.view.dialog.warningDialog
|
||||
import io.novafoundation.nova.feature_account_api.view.showWallet
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentAuthorizedDappsBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.model.AuthorizedDAppModel
|
||||
|
||||
class AuthorizedDAppsFragment : BaseFragment<AuthorizedDAppsViewModel, FragmentAuthorizedDappsBinding>(), AuthorizedDAppAdapter.Handler {
|
||||
|
||||
override fun createBinding() = FragmentAuthorizedDappsBinding.inflate(layoutInflater)
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
AuthorizedDAppAdapter(this)
|
||||
}
|
||||
|
||||
private val placeholderViews by lazy(LazyThreadSafetyMode.NONE) {
|
||||
listOf(binder.authorizedPlaceholderSpacerTop, binder.authorizedPlaceholder, binder.authorizedPlaceholderSpacerBottom)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.authorizedDAppsToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
|
||||
binder.authorizedDAppsList.setHasFixedSize(true)
|
||||
binder.authorizedDAppsList.adapter = adapter
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.authorizedDAppsComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: AuthorizedDAppsViewModel) {
|
||||
viewModel.authorizedDApps.observe {
|
||||
val showPlaceholder = it.isEmpty()
|
||||
|
||||
binder.authorizedDAppsList.setVisible(!showPlaceholder)
|
||||
placeholderViews.forEach { view -> view.setVisible(showPlaceholder) }
|
||||
|
||||
adapter.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.walletUi.observe {
|
||||
binder.authorizedDAppsWallet.showWallet(it)
|
||||
}
|
||||
|
||||
viewModel.revokeAuthorizationConfirmation.awaitableActionLiveData.observeEvent {
|
||||
warningDialog(
|
||||
context = requireContext(),
|
||||
onPositiveClick = { it.onSuccess(Unit) },
|
||||
onNegativeClick = it.onCancel,
|
||||
positiveTextRes = R.string.common_remove
|
||||
) {
|
||||
setTitle(R.string.dapp_authorized_remove_title)
|
||||
setMessage(getString(R.string.dapp_authorized_remove_description, it.payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRevokeClicked(item: AuthorizedDAppModel) {
|
||||
viewModel.revokeClicked(item)
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.authorizedDApps.AuthorizedDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.authorizedDApps.AuthorizedDAppsInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.model.AuthorizedDAppModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
typealias RevokeAuthorizationPayload = String // dApp name
|
||||
|
||||
class AuthorizedDAppsViewModel(
|
||||
private val interactor: AuthorizedDAppsInteractor,
|
||||
private val router: DAppRouter,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val revokeAuthorizationConfirmation = actionAwaitableMixinFactory.confirmingAction<RevokeAuthorizationPayload>()
|
||||
|
||||
val walletUi = walletUiUseCase.selectedWalletUiFlow(showAddressIcon = true)
|
||||
.shareInBackground()
|
||||
|
||||
val authorizedDApps = interactor.observeAuthorizedDApps()
|
||||
.mapList(::mapAuthorizedDAppToModel)
|
||||
.shareInBackground()
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun revokeClicked(item: AuthorizedDAppModel) = launch {
|
||||
val dAppTitle = item.title ?: item.url
|
||||
revokeAuthorizationConfirmation.awaitAction(dAppTitle)
|
||||
|
||||
interactor.revokeAuthorization(item.url)
|
||||
}
|
||||
|
||||
private fun mapAuthorizedDAppToModel(
|
||||
authorizedDApp: AuthorizedDApp
|
||||
): AuthorizedDAppModel {
|
||||
return AuthorizedDAppModel(
|
||||
title = authorizedDApp.name,
|
||||
url = authorizedDApp.baseUrl,
|
||||
iconLink = authorizedDApp.iconLink
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.AuthorizedDAppsFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
AuthorizedDAppsModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface AuthorizedDAppsComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
): AuthorizedDAppsComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: AuthorizedDAppsFragment)
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.AccountRepository
|
||||
import io.novafoundation.nova.feature_account_api.presenatation.account.wallet.WalletUiUseCase
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.authorizedDApps.AuthorizedDAppsInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.AuthorizedDAppsViewModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class AuthorizedDAppsModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
accountRepository: AccountRepository,
|
||||
metadataRepository: DAppMetadataRepository,
|
||||
web3Session: Web3Session
|
||||
) = AuthorizedDAppsInteractor(
|
||||
accountRepository = accountRepository,
|
||||
metadataRepository = metadataRepository,
|
||||
web3Session = web3Session
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): AuthorizedDAppsViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(AuthorizedDAppsViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(AuthorizedDAppsViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: DAppRouter,
|
||||
interactor: AuthorizedDAppsInteractor,
|
||||
walletUiUseCase: WalletUiUseCase,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory
|
||||
): ViewModel {
|
||||
return AuthorizedDAppsViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
walletUiUseCase = walletUiUseCase,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.authorizedDApps.model
|
||||
|
||||
data class AuthorizedDAppModel(
|
||||
val title: String?,
|
||||
val url: String,
|
||||
val iconLink: String?
|
||||
)
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main
|
||||
|
||||
sealed class BrowserCommand {
|
||||
|
||||
object Reload : BrowserCommand()
|
||||
|
||||
object GoBack : BrowserCommand()
|
||||
|
||||
class OpenUrl(val url: String) : BrowserCommand()
|
||||
|
||||
class ChangeDesktopMode(val enabled: Boolean) : BrowserCommand()
|
||||
}
|
||||
+338
@@ -0,0 +1,338 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.transition.TransitionInflater
|
||||
import android.view.View
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.core.app.SharedElementCallback
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.transition.addListener
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.insets.applyNavigationBarInsets
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.makeGone
|
||||
import io.novafoundation.nova.common.utils.makeVisible
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentDappBrowserBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.isSecure
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.DappPendingConfirmation.Action
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.sheets.AcknowledgePhishingBottomSheet
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.options.OptionsBottomSheetDialog
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.favourites.setupRemoveFavouritesConfirmation
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.BrowserTabSession
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.Web3ChromeClient
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.CompoundWeb3Injector
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.Web3WebViewClient
|
||||
import io.novafoundation.nova.common.utils.browser.fileChoosing.WebViewFileChooser
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.WebViewHolder
|
||||
import io.novafoundation.nova.common.utils.browser.permissions.WebViewPermissionAsker
|
||||
import io.novafoundation.nova.common.utils.webView.WebViewRequestInterceptor
|
||||
import io.novafoundation.nova.common.view.dialog.infoDialog
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.SessionCallback
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.externalSign.AuthorizeDappBottomSheet
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val OVERFLOW_TABS_COUNT = 100
|
||||
|
||||
const val DAPP_SHARED_ELEMENT_ID_IMAGE_TAB = "DAPP_SHARED_ELEMENT_ID_IMAGE_TAB"
|
||||
|
||||
class DAppBrowserFragment : BaseFragment<DAppBrowserViewModel, FragmentDappBrowserBinding>(), OptionsBottomSheetDialog.Callback, SessionCallback {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PAYLOAD = "DAppBrowserFragment.Payload"
|
||||
|
||||
fun getBundle(payload: DAppBrowserPayload) = bundleOf(PAYLOAD to payload)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentDappBrowserBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var compoundWeb3Injector: CompoundWeb3Injector
|
||||
|
||||
@Inject
|
||||
lateinit var webViewHolder: WebViewHolder
|
||||
|
||||
@Inject
|
||||
lateinit var fileChooser: WebViewFileChooser
|
||||
|
||||
@Inject
|
||||
lateinit var permissionAsker: WebViewPermissionAsker
|
||||
|
||||
@Inject
|
||||
lateinit var webViewRequestInterceptor: WebViewRequestInterceptor
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private var webViewClient: Web3WebViewClient? = null
|
||||
|
||||
var backCallback: OnBackPressedCallback? = null
|
||||
|
||||
private val dappBrowserWebView: WebView?
|
||||
get() {
|
||||
return binder.dappBrowserWebViewContainer.getChildAt(0) as? WebView
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
WebView.enableSlowWholeDocumentDraw()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
sharedElementEnterTransition = TransitionInflater.from(requireContext())
|
||||
.inflateTransition(android.R.transition.move).apply {
|
||||
addListener(
|
||||
onStart = { binder.dappBrowserWebViewContainer.makeGone() }, // Hide WebView during transition animation
|
||||
onEnd = {
|
||||
binder.dappBrowserWebViewContainer.makeVisible()
|
||||
binder.dappBrowserTransitionImage.animate()
|
||||
.setDuration(300)
|
||||
.alpha(0f)
|
||||
.withEndAction { binder.dappBrowserTransitionImage.makeGone() }
|
||||
.start()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.dappBrowserAddressBarGroup.applyStatusBarInsets()
|
||||
binder.dappBrowserBottomNavigation.applyNavigationBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.dappBrowserHide.setOnClickListener { viewModel.closeClicked() }
|
||||
|
||||
binder.dappBrowserBack.setOnClickListener { backClicked() }
|
||||
|
||||
binder.dappBrowserAddressBar.setOnClickListener {
|
||||
viewModel.openSearch()
|
||||
}
|
||||
|
||||
binder.dappBrowserForward.setOnClickListener { forwardClicked() }
|
||||
binder.dappBrowserTabs.setOnClickListener { viewModel.openTabs() }
|
||||
binder.dappBrowserRefresh.setOnClickListener { refreshClicked() }
|
||||
binder.dappBrowserFavorite.setOnClickListener { viewModel.onFavoriteClick() }
|
||||
binder.dappBrowserMore.setOnClickListener { moreClicked() }
|
||||
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
|
||||
|
||||
binder.dappBrowserTransitionImage.transitionName = DAPP_SHARED_ELEMENT_ID_IMAGE_TAB
|
||||
|
||||
setEnterSharedElementCallback(object : SharedElementCallback() {
|
||||
override fun onSharedElementStart(
|
||||
sharedElementNames: MutableList<String>?,
|
||||
sharedElements: MutableList<View>?,
|
||||
sharedElementSnapshots: MutableList<View>?
|
||||
) {
|
||||
val sharedView = sharedElements?.firstOrNull { it.transitionName == DAPP_SHARED_ELEMENT_ID_IMAGE_TAB }
|
||||
val sharedImageView = sharedView as? ImageView
|
||||
binder.dappBrowserTransitionImage.setImageDrawable(sharedImageView?.drawable) // Set image from shared element
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
binder.dappBrowserWebViewContainer.removeAllViews()
|
||||
viewModel.detachCurrentSession()
|
||||
super.onDestroyView()
|
||||
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
viewModel.makePageSnapshot()
|
||||
|
||||
detachBackCallback()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
attachBackCallback()
|
||||
}
|
||||
|
||||
override fun onHiddenChanged(hidden: Boolean) {
|
||||
if (hidden) {
|
||||
detachBackCallback()
|
||||
} else {
|
||||
attachBackCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.browserComponentFactory()
|
||||
.create(this, argument(PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun subscribe(viewModel: DAppBrowserViewModel) {
|
||||
setupRemoveFavouritesConfirmation(viewModel.removeFromFavouritesConfirmation)
|
||||
|
||||
viewModel.currentTabFlow.observe { currentTab ->
|
||||
attachSession(currentTab.browserTabSession)
|
||||
}
|
||||
|
||||
viewModel.desktopModeChangedModel.observe {
|
||||
webViewClient?.desktopMode = it.desktopModeEnabled
|
||||
}
|
||||
|
||||
viewModel.showConfirmationSheet.observeEvent {
|
||||
when (it.action) {
|
||||
is Action.Authorize -> {
|
||||
showConfirmAuthorizeSheet(it as DappPendingConfirmation<Action.Authorize>)
|
||||
}
|
||||
|
||||
Action.AcknowledgePhishingAlert -> {
|
||||
AcknowledgePhishingBottomSheet(requireContext(), it)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.browserCommandEvent.observeEvent {
|
||||
when (it) {
|
||||
BrowserCommand.Reload -> dappBrowserWebView?.reload()
|
||||
BrowserCommand.GoBack -> backClicked()
|
||||
is BrowserCommand.OpenUrl -> dappBrowserWebView?.loadUrl(it.url)
|
||||
is BrowserCommand.ChangeDesktopMode -> {
|
||||
webViewClient?.desktopMode = it.enabled
|
||||
dappBrowserWebView?.reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.openBrowserOptionsEvent.observeEvent {
|
||||
val optionsBottomSheet = OptionsBottomSheetDialog(requireContext(), this, it)
|
||||
optionsBottomSheet.show()
|
||||
}
|
||||
|
||||
viewModel.currentPageAnalyzed.observe {
|
||||
binder.dappBrowserAddressBar.setAddress(it.display)
|
||||
binder.dappBrowserAddressBar.showSecure(it.isSecure)
|
||||
binder.dappBrowserFavorite.setImageResource(favoriteIcon(it.isFavourite))
|
||||
|
||||
updateButtonsState()
|
||||
}
|
||||
|
||||
viewModel.tabsCountFlow.observe {
|
||||
if (it >= OVERFLOW_TABS_COUNT) {
|
||||
binder.dappBrowserTabsIcon.makeVisible()
|
||||
binder.dappBrowserTabsContent.text = null
|
||||
} else {
|
||||
binder.dappBrowserTabsIcon.makeGone()
|
||||
binder.dappBrowserTabsContent.text = it.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun attachSession(session: BrowserTabSession) {
|
||||
clearProgress()
|
||||
session.attachToHost(createChromeClient(), this)
|
||||
webViewHolder.set(session.webView)
|
||||
webViewClient = session.webViewClient
|
||||
|
||||
binder.dappBrowserWebViewContainer.removeAllViews()
|
||||
binder.dappBrowserWebViewContainer.addView(session.webView)
|
||||
}
|
||||
|
||||
private fun clearProgress() {
|
||||
binder.dappBrowserProgress.makeGone()
|
||||
binder.dappBrowserProgress.progress = 0
|
||||
}
|
||||
|
||||
private fun createChromeClient() = Web3ChromeClient(permissionAsker, fileChooser, viewModel.viewModelScope, binder.dappBrowserProgress)
|
||||
|
||||
private fun updateButtonsState() {
|
||||
binder.dappBrowserForward.isEnabled = dappBrowserWebView?.canGoForward() ?: false
|
||||
binder.dappBrowserBack.isEnabled = dappBrowserWebView?.canGoBack() ?: false
|
||||
}
|
||||
|
||||
private fun showConfirmAuthorizeSheet(pendingConfirmation: DappPendingConfirmation<Action.Authorize>) {
|
||||
AuthorizeDappBottomSheet(
|
||||
context = requireContext(),
|
||||
onConfirm = pendingConfirmation.onConfirm,
|
||||
onDeny = pendingConfirmation.onDeny,
|
||||
payload = pendingConfirmation.action.content,
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun backClicked() {
|
||||
if (dappBrowserWebView?.canGoBack() == true) {
|
||||
dappBrowserWebView?.goBack()
|
||||
} else {
|
||||
viewModel.closeClicked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardClicked() {
|
||||
dappBrowserWebView?.goForward()
|
||||
}
|
||||
|
||||
private fun refreshClicked() {
|
||||
dappBrowserWebView?.reload()
|
||||
}
|
||||
|
||||
private fun attachBackCallback() {
|
||||
if (backCallback == null) {
|
||||
backCallback = object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
backClicked()
|
||||
}
|
||||
}
|
||||
requireActivity().onBackPressedDispatcher.addCallback(backCallback!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun moreClicked() {
|
||||
viewModel.onMoreClicked()
|
||||
}
|
||||
|
||||
private fun detachBackCallback() {
|
||||
backCallback?.remove()
|
||||
backCallback = null
|
||||
}
|
||||
|
||||
override fun onDesktopModeClick() {
|
||||
viewModel.onDesktopClick()
|
||||
}
|
||||
|
||||
override fun onPageStarted(webView: WebView, url: String, favicon: Bitmap?) {
|
||||
compoundWeb3Injector.injectForPage(webView, viewModel.extensionsStore)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
|
||||
return webViewRequestInterceptor.intercept(request)
|
||||
}
|
||||
|
||||
override fun onPageChanged(webView: WebView, url: String?, title: String?) {
|
||||
viewModel.onPageChanged(url, title)
|
||||
}
|
||||
|
||||
override fun onPageError(error: String) {
|
||||
infoDialog(requireContext()) {
|
||||
setTitle(R.string.common_error_general_title)
|
||||
setMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
private fun favoriteIcon(isFavorite: Boolean): Int {
|
||||
return if (isFavorite) {
|
||||
R.drawable.ic_favorite_heart_filled
|
||||
} else {
|
||||
R.drawable.ic_favorite_heart_outline
|
||||
}
|
||||
}
|
||||
}
|
||||
+323
@@ -0,0 +1,323 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.event
|
||||
import io.novafoundation.nova.common.utils.removeHexPrefix
|
||||
import io.novafoundation.nova.common.utils.singleReplaySharedFlow
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.BrowserHostSettings
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.BrowserPage
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.BrowserPageAnalyzed
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.DappBrowserInteractor
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.addToFavorites.AddToFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.options.DAppOptionsPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.favourites.RemoveFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchRequester
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.SearchPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.createAndSelectTab
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.CurrentTabState
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.stateId
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.session.Web3Session.Authorization.State
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.ExtensionStoreFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3ExtensionStateMachine.ExternalEvent
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.Web3StateMachineHost
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.hostApi.ConfirmTxResponse
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignRequester
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.awaitConfirmation
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignPayload
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignRequest
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.ExternalSignWallet
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.SigningDappMetadata
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.signPayload.polkadot.genesisHash
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.externalSign.AuthorizeDappBottomSheet
|
||||
import io.novafoundation.nova.runtime.ext.isDisabled
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
import io.novafoundation.nova.runtime.multiNetwork.chainsById
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
enum class ConfirmationState {
|
||||
ALLOWED, REJECTED, CANCELLED
|
||||
}
|
||||
|
||||
data class DesktopModeChangedEvent(val desktopModeEnabled: Boolean, val url: String)
|
||||
|
||||
class DAppBrowserViewModel(
|
||||
private val router: DAppRouter,
|
||||
private val signRequester: ExternalSignRequester,
|
||||
private val extensionStoreFactory: ExtensionStoreFactory,
|
||||
private val dAppInteractor: DappInteractor,
|
||||
private val interactor: DappBrowserInteractor,
|
||||
private val dAppSearchRequester: DAppSearchRequester,
|
||||
private val payload: DAppBrowserPayload,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val chainRegistry: ChainRegistry,
|
||||
private val browserTabService: BrowserTabService
|
||||
) : BaseViewModel(), Web3StateMachineHost {
|
||||
|
||||
val removeFromFavouritesConfirmation = actionAwaitableMixinFactory.confirmingAction<RemoveFavouritesPayload>()
|
||||
|
||||
private val _showConfirmationDialog = MutableLiveData<Event<DappPendingConfirmation<*>>>()
|
||||
val showConfirmationSheet = _showConfirmationDialog
|
||||
|
||||
override val selectedAccount = selectedAccountUseCase.selectedMetaAccountFlow()
|
||||
.share()
|
||||
|
||||
private val currentPage = singleReplaySharedFlow<BrowserPage>()
|
||||
|
||||
override val currentPageAnalyzed = currentPage.flatMapLatest {
|
||||
interactor.observeBrowserPageFor(it)
|
||||
}.shareInBackground()
|
||||
|
||||
override val externalEvents = singleReplaySharedFlow<ExternalEvent>()
|
||||
|
||||
private val _browserCommandEvent = MutableLiveData<Event<BrowserCommand>>()
|
||||
val browserCommandEvent: LiveData<Event<BrowserCommand>> = _browserCommandEvent
|
||||
|
||||
private val _openBrowserOptionsEvent = MutableLiveData<Event<DAppOptionsPayload>>()
|
||||
val openBrowserOptionsEvent: LiveData<Event<DAppOptionsPayload>> = _openBrowserOptionsEvent
|
||||
|
||||
val extensionsStore = extensionStoreFactory.create(hostApi = this, coroutineScope = this)
|
||||
|
||||
private val isDesktopModeEnabledFlow = MutableStateFlow(false)
|
||||
|
||||
val desktopModeChangedModel = currentPageAnalyzed
|
||||
.map { currentPage ->
|
||||
val hostSettings = interactor.getHostSettings(currentPage.url)
|
||||
val isDesktopModeEnabled = hostSettings?.isDesktopModeEnabled ?: isDesktopModeEnabledFlow.first()
|
||||
DesktopModeChangedEvent(isDesktopModeEnabled, currentPage.url)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.shareInBackground()
|
||||
|
||||
private val tabsState = browserTabService.tabStateFlow
|
||||
.distinctUntilChangedBy { it.stateId() }
|
||||
.shareInBackground()
|
||||
|
||||
val currentTabFlow = tabsState.map { it.selectedTab }
|
||||
.distinctUntilChangedBy { it.stateId() }
|
||||
.filterIsInstance<CurrentTabState.Selected>()
|
||||
.shareInBackground()
|
||||
|
||||
val tabsCountFlow = tabsState.map { it.tabs.size }
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
dAppSearchRequester.responseFlow
|
||||
.filterIsInstance<DAppSearchCommunicator.Response.NewUrl>()
|
||||
.onEach { forceLoad(it.url) }
|
||||
.launchIn(this)
|
||||
|
||||
watchDangerousWebsites()
|
||||
|
||||
launch {
|
||||
when (payload) {
|
||||
is DAppBrowserPayload.Tab -> browserTabService.selectTab(payload.id)
|
||||
|
||||
is DAppBrowserPayload.Address -> browserTabService.createAndSelectTab(payload.address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun authorizeDApp(payload: AuthorizeDappBottomSheet.Payload): State {
|
||||
val confirmationState = awaitConfirmation(DappPendingConfirmation.Action.Authorize(payload))
|
||||
|
||||
return mapConfirmationStateToAuthorizationState(confirmationState)
|
||||
}
|
||||
|
||||
override suspend fun confirmTx(request: ExternalSignRequest): ConfirmTxResponse {
|
||||
val chainId = request.extractChainId()
|
||||
val chain = chainRegistry.chainsById()[chainId]
|
||||
|
||||
if (chain != null && chain.isDisabled) {
|
||||
return ConfirmTxResponse.ChainIsDisabled(request.id, chain.name)
|
||||
}
|
||||
|
||||
val response = withContext(Dispatchers.Main) {
|
||||
signRequester.awaitConfirmation(mapSignExtrinsicRequestToPayload(request))
|
||||
}
|
||||
|
||||
return when (response) {
|
||||
is ExternalSignCommunicator.Response.Rejected -> ConfirmTxResponse.Rejected(response.requestId)
|
||||
is ExternalSignCommunicator.Response.Signed -> ConfirmTxResponse.Signed(response.requestId, response.signature, response.modifiedTransaction)
|
||||
is ExternalSignCommunicator.Response.SigningFailed -> ConfirmTxResponse.SigningFailed(response.requestId, response.shouldPresent)
|
||||
is ExternalSignCommunicator.Response.Sent -> ConfirmTxResponse.Sent(response.requestId, response.txHash)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reloadPage() {
|
||||
_browserCommandEvent.postValue(BrowserCommand.Reload.event())
|
||||
}
|
||||
|
||||
fun detachCurrentSession() {
|
||||
browserTabService.detachCurrentSession()
|
||||
}
|
||||
|
||||
fun onPageChanged(url: String?, title: String?) {
|
||||
updateCurrentPage(url ?: "", title, synchronizedWithBrowser = true)
|
||||
}
|
||||
|
||||
fun closeClicked() = launch {
|
||||
exitBrowser()
|
||||
}
|
||||
|
||||
fun openSearch() = launch {
|
||||
val currentPage = currentPage.first()
|
||||
|
||||
dAppSearchRequester.openRequest(SearchPayload(initialUrl = currentPage.url, SearchPayload.Request.GO_TO_URL))
|
||||
}
|
||||
|
||||
fun onMoreClicked() {
|
||||
launch {
|
||||
val payload = getCurrentPageOptionsPayload()
|
||||
_openBrowserOptionsEvent.value = Event(payload)
|
||||
}
|
||||
}
|
||||
|
||||
fun onFavoriteClick() {
|
||||
launch {
|
||||
val page = currentPageAnalyzed.first()
|
||||
val currentPageTitle = page.title ?: page.display
|
||||
val isCurrentPageFavorite = page.isFavourite
|
||||
|
||||
if (isCurrentPageFavorite) {
|
||||
removeFromFavouritesConfirmation.awaitAction(currentPageTitle)
|
||||
|
||||
dAppInteractor.removeDAppFromFavourites(page.url)
|
||||
} else {
|
||||
val payload = AddToFavouritesPayload(
|
||||
url = page.url,
|
||||
label = currentPageTitle,
|
||||
iconLink = null
|
||||
)
|
||||
|
||||
router.openAddToFavourites(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDesktopClick() {
|
||||
launch {
|
||||
val desktopModeChangedEvent = desktopModeChangedModel.first()
|
||||
val newDesktopMode = !desktopModeChangedEvent.desktopModeEnabled
|
||||
val settings = BrowserHostSettings(Urls.normalizeUrl(desktopModeChangedEvent.url), newDesktopMode)
|
||||
interactor.saveHostSettings(settings)
|
||||
isDesktopModeEnabledFlow.value = newDesktopMode
|
||||
_browserCommandEvent.postValue(BrowserCommand.ChangeDesktopMode(newDesktopMode).event())
|
||||
}
|
||||
}
|
||||
|
||||
fun openTabs() {
|
||||
router.openTabs()
|
||||
}
|
||||
|
||||
fun makePageSnapshot() {
|
||||
browserTabService.makeCurrentTabSnapshot()
|
||||
}
|
||||
|
||||
private fun watchDangerousWebsites() {
|
||||
currentPageAnalyzed
|
||||
.filter { it.synchronizedWithBrowser && it.security == BrowserPageAnalyzed.Security.DANGEROUS }
|
||||
.distinctUntilChanged()
|
||||
.onEach {
|
||||
externalEvents.emit(ExternalEvent.PhishingDetected)
|
||||
|
||||
awaitConfirmation(DappPendingConfirmation.Action.AcknowledgePhishingAlert)
|
||||
|
||||
exitBrowser()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun forceLoad(url: String) {
|
||||
_browserCommandEvent.value = BrowserCommand.OpenUrl(url).event()
|
||||
|
||||
updateCurrentPage(url, title = null, synchronizedWithBrowser = false)
|
||||
}
|
||||
|
||||
private suspend fun getCurrentPageOptionsPayload(): DAppOptionsPayload {
|
||||
return DAppOptionsPayload(
|
||||
isDesktopModeEnabled = desktopModeChangedModel.first().desktopModeEnabled
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun awaitConfirmation(action: DappPendingConfirmation.Action) = suspendCoroutine<ConfirmationState> {
|
||||
val confirmation = DappPendingConfirmation(
|
||||
onConfirm = { it.resume(ConfirmationState.ALLOWED) },
|
||||
onDeny = { it.resume(ConfirmationState.REJECTED) },
|
||||
onCancel = { it.resume(ConfirmationState.CANCELLED) },
|
||||
action = action
|
||||
)
|
||||
|
||||
_showConfirmationDialog.postValue(confirmation.event())
|
||||
}
|
||||
|
||||
private fun mapConfirmationStateToAuthorizationState(
|
||||
confirmationState: ConfirmationState
|
||||
): State = when (confirmationState) {
|
||||
ConfirmationState.ALLOWED -> State.ALLOWED
|
||||
ConfirmationState.REJECTED -> State.REJECTED
|
||||
ConfirmationState.CANCELLED -> State.NONE
|
||||
}
|
||||
|
||||
private fun exitBrowser() = router.back()
|
||||
|
||||
private fun updateCurrentPage(
|
||||
url: String,
|
||||
title: String?,
|
||||
synchronizedWithBrowser: Boolean
|
||||
) = launch {
|
||||
currentPage.emit(BrowserPage(url, title, synchronizedWithBrowser))
|
||||
}
|
||||
|
||||
private suspend fun mapSignExtrinsicRequestToPayload(request: ExternalSignRequest): ExternalSignPayload {
|
||||
return ExternalSignPayload(
|
||||
signRequest = request,
|
||||
dappMetadata = getDAppSignMetadata(currentPageAnalyzed.first().url),
|
||||
wallet = ExternalSignWallet.Current
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun getDAppSignMetadata(dAppUrl: String): SigningDappMetadata {
|
||||
val dappMetadata = dAppInteractor.getDAppInfo(dAppUrl)
|
||||
|
||||
return SigningDappMetadata(
|
||||
icon = dappMetadata.metadata?.iconLink,
|
||||
name = dappMetadata.metadata?.name,
|
||||
url = dappMetadata.baseUrl,
|
||||
)
|
||||
}
|
||||
|
||||
private fun ExternalSignRequest.extractChainId(): String? {
|
||||
return when (this) {
|
||||
is ExternalSignRequest.Evm -> null
|
||||
is ExternalSignRequest.Polkadot -> payload.genesisHash()?.removeHexPrefix()
|
||||
}
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main
|
||||
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.externalSign.AuthorizeDappBottomSheet
|
||||
|
||||
class DappPendingConfirmation<A : DappPendingConfirmation.Action>(
|
||||
val onConfirm: () -> Unit,
|
||||
val onDeny: () -> Unit,
|
||||
val onCancel: () -> Unit,
|
||||
val action: A
|
||||
) {
|
||||
|
||||
sealed class Action {
|
||||
class Authorize(val content: AuthorizeDappBottomSheet.Payload) : Action()
|
||||
|
||||
object AcknowledgePhishingAlert : Action()
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.deeplink
|
||||
|
||||
import android.net.Uri
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.common.DeepLinkHandlingException.DAppHandlingException
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
|
||||
import io.novafoundation.nova.common.utils.sequrity.awaitInteractionAllowed
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.createAndSelectTab
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.hasSelectedTab
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.CallbackEvent
|
||||
import io.novafoundation.nova.feature_deep_linking.presentation.handling.DeepLinkHandler
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
||||
private const val DAPP_DEEP_LINK_PREFIX = "/open/dapp"
|
||||
|
||||
class DAppDeepLinkHandler(
|
||||
private val dappRepository: DAppMetadataRepository,
|
||||
private val router: DAppRouter,
|
||||
private val automaticInteractionGate: AutomaticInteractionGate,
|
||||
private val browserTabService: BrowserTabService
|
||||
) : DeepLinkHandler {
|
||||
|
||||
override val callbackFlow: Flow<CallbackEvent> = emptyFlow()
|
||||
|
||||
override suspend fun matches(data: Uri): Boolean {
|
||||
val path = data.path ?: return false
|
||||
return path.startsWith(DAPP_DEEP_LINK_PREFIX)
|
||||
}
|
||||
|
||||
override suspend fun handleDeepLink(data: Uri) = runCatching {
|
||||
automaticInteractionGate.awaitInteractionAllowed()
|
||||
|
||||
val url = data.getDappUrl() ?: throw DAppHandlingException.UrlIsInvalid
|
||||
val normalizedUrl = runCatching { Urls.normalizeUrl(url) }.getOrNull() ?: throw DAppHandlingException.UrlIsInvalid
|
||||
|
||||
ensureDAppInCatalog(normalizedUrl)
|
||||
|
||||
if (browserTabService.hasSelectedTab()) {
|
||||
browserTabService.createAndSelectTab(normalizedUrl)
|
||||
} else {
|
||||
router.openDAppBrowser(DAppBrowserPayload.Address(url))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun ensureDAppInCatalog(normalizedUrl: String) {
|
||||
dappRepository.syncAndGetDapp(normalizedUrl)
|
||||
?: throw DAppHandlingException.DomainIsNotMatched(normalizedUrl)
|
||||
}
|
||||
|
||||
private fun Uri.getDappUrl(): String? {
|
||||
return getQueryParameter("url")
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.DAppBrowserFragment
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
DAppBrowserModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface DAppBrowserComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: DAppBrowserPayload
|
||||
): DAppBrowserComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: DAppBrowserFragment)
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ContextManager
|
||||
import io.novafoundation.nova.common.utils.ToastMessageManager
|
||||
import io.novafoundation.nova.core_db.dao.BrowserHostSettingsDao
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.BrowserHostSettingsRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.PhishingSitesRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.RealBrowserHostSettingsRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.browser.DappBrowserInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.DAppBrowserViewModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.states.ExtensionStoreFactory
|
||||
import io.novafoundation.nova.common.utils.browser.fileChoosing.WebViewFileChooserFactory
|
||||
import io.novafoundation.nova.common.utils.browser.permissions.WebViewPermissionAskerFactory
|
||||
import io.novafoundation.nova.common.utils.webView.WebViewRequestInterceptor
|
||||
import io.novafoundation.nova.common.utils.webView.interceptors.CompoundWebViewRequestInterceptor
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.interceptors.WalletConnectPairingInterceptor
|
||||
import io.novafoundation.nova.feature_dapp_impl.web3.webview.interceptors.Web3FallbackInterceptor
|
||||
import io.novafoundation.nova.feature_external_sign_api.model.ExternalSignCommunicator
|
||||
import io.novafoundation.nova.feature_wallet_connect_api.presentation.WalletConnectService
|
||||
import io.novafoundation.nova.runtime.multiNetwork.ChainRegistry
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class DAppBrowserModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideBrowserHostSettingsRepository(
|
||||
browserHostSettingsDao: BrowserHostSettingsDao
|
||||
): BrowserHostSettingsRepository = RealBrowserHostSettingsRepository(browserHostSettingsDao)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
phishingSitesRepository: PhishingSitesRepository,
|
||||
favouritesDAppRepository: FavouritesDAppRepository,
|
||||
browserHostSettingsRepository: BrowserHostSettingsRepository
|
||||
) = DappBrowserInteractor(
|
||||
phishingSitesRepository = phishingSitesRepository,
|
||||
favouritesDAppRepository = favouritesDAppRepository,
|
||||
browserHostSettingsRepository = browserHostSettingsRepository
|
||||
)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideFileChooser(
|
||||
fragment: Fragment,
|
||||
webViewFileChooserFactory: WebViewFileChooserFactory
|
||||
) = webViewFileChooserFactory.create(fragment)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun providePermissionAsker(
|
||||
fragment: Fragment,
|
||||
webViewPermissionAskerFactory: WebViewPermissionAskerFactory
|
||||
) = webViewPermissionAskerFactory.create(fragment)
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideWebViewClientInterceptor(
|
||||
toastMessageManager: ToastMessageManager,
|
||||
contextManager: ContextManager,
|
||||
walletConnectService: WalletConnectService
|
||||
): WebViewRequestInterceptor {
|
||||
return CompoundWebViewRequestInterceptor(
|
||||
WalletConnectPairingInterceptor(walletConnectService),
|
||||
Web3FallbackInterceptor(toastMessageManager, contextManager)
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): DAppBrowserViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(DAppBrowserViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(DAppBrowserViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: DAppRouter,
|
||||
interactor: DappBrowserInteractor,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
signRequester: ExternalSignCommunicator,
|
||||
searchRequester: DAppSearchCommunicator,
|
||||
payload: DAppBrowserPayload,
|
||||
extensionStoreFactory: ExtensionStoreFactory,
|
||||
dAppInteractor: DappInteractor,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
chainRegistry: ChainRegistry,
|
||||
browserTabService: BrowserTabService
|
||||
): ViewModel {
|
||||
return DAppBrowserViewModel(
|
||||
router = router,
|
||||
interactor = interactor,
|
||||
dAppInteractor = dAppInteractor,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
signRequester = signRequester,
|
||||
dAppSearchRequester = searchRequester,
|
||||
payload = payload,
|
||||
extensionStoreFactory = extensionStoreFactory,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory,
|
||||
chainRegistry = chainRegistry,
|
||||
browserTabService = browserTabService
|
||||
)
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.sheets
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import io.novafoundation.nova.common.view.bottomSheet.ActionNotAllowedBottomSheet
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.DappPendingConfirmation
|
||||
|
||||
class AcknowledgePhishingBottomSheet(
|
||||
context: Context,
|
||||
private val confirmation: DappPendingConfirmation<*>,
|
||||
) : ActionNotAllowedBottomSheet(
|
||||
context = context,
|
||||
onSuccess = { confirmation.onConfirm() }
|
||||
) {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
titleView.setText(R.string.dapp_phishing_title)
|
||||
subtitleView.setText(R.string.dapp_phishing_subtitle)
|
||||
|
||||
applySolidIconStyle(R.drawable.ic_warning_filled, tint = null)
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.widget.LinearLayout
|
||||
import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setImageTintRes
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ViewAddressBarBinding
|
||||
|
||||
class AddressBarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : LinearLayout(context, attrs, defStyleAttr), WithContextExtensions {
|
||||
|
||||
private val binder = ViewAddressBarBinding.inflate(inflater(), this)
|
||||
|
||||
override val providedContext: Context = context
|
||||
|
||||
init {
|
||||
orientation = HORIZONTAL
|
||||
gravity = Gravity.CENTER
|
||||
|
||||
background = addRipple(getRoundedCornerDrawable(R.color.dapp_blur_navigation_background, cornerSizeDp = 10), mask = getRippleMask(cornerSizeDp = 10))
|
||||
}
|
||||
|
||||
fun setAddress(address: String) {
|
||||
binder.addressBarUrl.text = address
|
||||
}
|
||||
|
||||
fun showSecureIcon(shouldShow: Boolean) {
|
||||
binder.addressBarIcon.setVisible(shouldShow)
|
||||
}
|
||||
|
||||
fun showSecure(shouldShow: Boolean) {
|
||||
binder.addressBarIcon.setVisible(shouldShow)
|
||||
|
||||
if (shouldShow) {
|
||||
binder.addressBarUrl.setTextColorRes(R.color.text_positive)
|
||||
binder.addressBarIcon.setImageTintRes(R.color.icon_positive)
|
||||
} else {
|
||||
binder.addressBarUrl.setTextColorRes(R.color.text_primary)
|
||||
binder.addressBarIcon.setImageTintRes(R.color.icon_primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.options
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class DAppOptionsPayload(
|
||||
val isDesktopModeEnabled: Boolean
|
||||
) : Parcelable
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.browser.options
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import io.novafoundation.nova.common.databinding.BottomSheeetFixedListBinding
|
||||
import io.novafoundation.nova.common.view.bottomSheet.list.fixed.FixedListBottomSheet
|
||||
import io.novafoundation.nova.common.view.bottomSheet.list.fixed.switcherItem
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
|
||||
class OptionsBottomSheetDialog(
|
||||
context: Context,
|
||||
private val callback: Callback,
|
||||
private val payload: DAppOptionsPayload
|
||||
) : FixedListBottomSheet<BottomSheeetFixedListBinding>(context, viewConfiguration = ViewConfiguration.default(context)) {
|
||||
|
||||
init {
|
||||
setTitle(R.string.dapp_options_title)
|
||||
|
||||
switcherItem(
|
||||
R.drawable.ic_desktop,
|
||||
R.string.dapp_options_desktop_mode,
|
||||
payload.isDesktopModeEnabled,
|
||||
::toggleDesktopMode
|
||||
)
|
||||
}
|
||||
|
||||
private fun toggleDesktopMode(view: View) {
|
||||
callback.onDesktopModeClick()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onDesktopModeClick()
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
interface DAppClickHandler {
|
||||
fun onDAppClicked(item: DappModel)
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSnapHelper
|
||||
import io.novafoundation.nova.common.list.BaseListAdapter
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.recyclerView.WithViewType
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemDappGroupBinding
|
||||
|
||||
class DappCategoryListAdapter(
|
||||
private val handler: DAppClickHandler
|
||||
) : BaseListAdapter<DappCategoryModel, DappCategoryViewHolder>(DappCategoryDiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DappCategoryViewHolder {
|
||||
return DappCategoryViewHolder(ItemDappGroupBinding.inflate(parent.inflater(), parent, false), handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DappCategoryViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return DappCategoryViewHolder.viewType
|
||||
}
|
||||
}
|
||||
|
||||
private object DappCategoryDiffCallback : DiffUtil.ItemCallback<DappCategoryModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: DappCategoryModel, newItem: DappCategoryModel): Boolean {
|
||||
return oldItem.categoryName == newItem.categoryName
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DappCategoryModel, newItem: DappCategoryModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class DappCategoryViewHolder(
|
||||
private val binder: ItemDappGroupBinding,
|
||||
itemHandler: DAppClickHandler,
|
||||
) : BaseViewHolder(binder.root) {
|
||||
|
||||
companion object : WithViewType {
|
||||
override val viewType: Int = R.layout.item_dapp_group
|
||||
}
|
||||
|
||||
private val adapter = DappListAdapter(itemHandler)
|
||||
|
||||
init {
|
||||
binder.dappRecyclerView.layoutManager = GridLayoutManager(itemView.context, 3, GridLayoutManager.HORIZONTAL, false)
|
||||
binder.dappRecyclerView.adapter = adapter
|
||||
binder.dappRecyclerView.itemAnimator = null
|
||||
val snapHelper = LinearSnapHelper()
|
||||
snapHelper.attachToRecyclerView(binder.dappRecyclerView)
|
||||
}
|
||||
|
||||
fun bind(item: DappCategoryModel) = with(binder) {
|
||||
itemDAppCategoryTitle.text = item.categoryName
|
||||
adapter.submitList(item.items)
|
||||
}
|
||||
|
||||
override fun unbind() {}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
data class DappCategoryModel(
|
||||
val categoryName: String,
|
||||
val items: List<DappModel>
|
||||
)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
import android.view.ViewGroup
|
||||
import io.novafoundation.nova.common.list.BaseListAdapter
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.view.DAppView
|
||||
|
||||
class DappListAdapter(
|
||||
private val handler: DAppClickHandler
|
||||
) : BaseListAdapter<DappModel, DappViewHolder>(DappModelDiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DappViewHolder {
|
||||
return DappViewHolder(DAppView.createUsingMathParentWidth(parent.context), handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DappViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
class DappViewHolder(
|
||||
private val dAppView: DAppView,
|
||||
private val itemHandler: DAppClickHandler,
|
||||
) : BaseViewHolder(dAppView) {
|
||||
|
||||
fun bind(item: DappModel) = with(dAppView) {
|
||||
setTitle(item.name)
|
||||
setSubtitle(item.description)
|
||||
setIconUrl(item.iconUrl)
|
||||
setFavoriteIconVisible(item.isFavourite)
|
||||
|
||||
setOnClickListener { itemHandler.onDAppClicked(item) }
|
||||
}
|
||||
|
||||
override fun unbind() {
|
||||
dAppView.clearIcon()
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DApp
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DAppGroupedCatalog
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCategory
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.dappToFavorite
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.common.favouriteToDApp
|
||||
|
||||
data class DappModel(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val iconUrl: String?,
|
||||
val isFavourite: Boolean,
|
||||
val favoriteIndex: Int?,
|
||||
val url: String
|
||||
)
|
||||
|
||||
fun mapDappCategoriesToDescription(categories: Collection<DappCategory>) = categories.joinToString { it.name }
|
||||
|
||||
fun mapDAppCatalogToDAppCategoryModels(resourceManager: ResourceManager, dappCatalog: DAppGroupedCatalog): List<DappCategoryModel> {
|
||||
val popular = mapDappCategoryToDappCategoryModel(resourceManager.getString(R.string.popular_dapps_title), dappCatalog.popular)
|
||||
val categories = dappCatalog.categoriesWithDApps.map { (category, dapps) -> mapDappCategoryToDappCategoryModel(category.name, dapps) }
|
||||
|
||||
return listOf(popular) + categories
|
||||
}
|
||||
|
||||
fun mapDappCategoryToDappCategoryModel(categoryName: String, dApps: List<DApp>) = DappCategoryModel(
|
||||
categoryName = categoryName,
|
||||
items = dApps.map { mapDappToDappModel(it) }
|
||||
)
|
||||
|
||||
fun mapDappToDappModel(dApp: DApp) = with(dApp) {
|
||||
DappModel(
|
||||
name = name,
|
||||
description = description,
|
||||
iconUrl = iconLink,
|
||||
url = url,
|
||||
isFavourite = isFavourite,
|
||||
favoriteIndex = favoriteIndex
|
||||
)
|
||||
}
|
||||
|
||||
fun mapDappModelToDApp(dApp: DappModel) = with(dApp) {
|
||||
DApp(
|
||||
name = name,
|
||||
description = description,
|
||||
iconLink = iconUrl,
|
||||
url = url,
|
||||
isFavourite = isFavourite,
|
||||
favoriteIndex = favoriteIndex
|
||||
)
|
||||
}
|
||||
|
||||
fun mapFavoriteDappToDappModel(favoriteDapp: FavouriteDApp): DappModel {
|
||||
val dapp = favouriteToDApp(favoriteDapp)
|
||||
return mapDappToDappModel(dapp)
|
||||
}
|
||||
|
||||
fun mapDAppModelToFavorite(model: DappModel, orderingIndex: Int): FavouriteDApp {
|
||||
val dapp = mapDappModelToDApp(model)
|
||||
return dappToFavorite(dapp, orderingIndex)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
object DappModelDiffCallback : DiffUtil.ItemCallback<DappModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: DappModel, newItem: DappModel): Boolean {
|
||||
return oldItem.url == newItem.url
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DappModel, newItem: DappModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common
|
||||
|
||||
import io.novafoundation.nova.feature_dapp_api.data.model.DappCategory
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.model.DAppCategoryModel
|
||||
|
||||
fun dappCategoryToUi(dappCategory: DappCategory, isSelected: Boolean): DAppCategoryModel {
|
||||
return DAppCategoryModel(
|
||||
id = dappCategory.id,
|
||||
name = dappCategory.name,
|
||||
selected = isSelected,
|
||||
iconUrl = dappCategory.iconUrl
|
||||
)
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.common.favourites
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseFragmentMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ConfirmationAwaitable
|
||||
import io.novafoundation.nova.common.view.dialog.warningDialog
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
|
||||
typealias RemoveFavouritesPayload = String // dApp title
|
||||
|
||||
fun BaseFragmentMixin<*>.setupRemoveFavouritesConfirmation(awaitableMixin: ConfirmationAwaitable<RemoveFavouritesPayload>) {
|
||||
awaitableMixin.awaitableActionLiveData.observeEvent {
|
||||
warningDialog(
|
||||
context = providedContext,
|
||||
onPositiveClick = { it.onSuccess(Unit) },
|
||||
positiveTextRes = R.string.common_remove,
|
||||
onNegativeClick = it.onCancel
|
||||
) {
|
||||
setTitle(R.string.dapp_favourites_remove_title)
|
||||
|
||||
setMessage(providedContext.getString(R.string.dapp_favourites_remove_description, it.payload))
|
||||
}
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.favorites
|
||||
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.model.FavouriteDApp
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.favourites.RemoveFavouritesPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapDAppModelToFavorite
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapFavoriteDappToDappModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DAppFavoritesViewModel(
|
||||
private val router: DAppRouter,
|
||||
private val interactor: DappInteractor,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
) : BaseViewModel() {
|
||||
|
||||
val removeFavouriteConfirmationAwaitable = actionAwaitableMixinFactory.confirmingAction<RemoveFavouritesPayload>()
|
||||
|
||||
private val favoriteDAppsFlow = MutableStateFlow<List<FavouriteDApp>>(emptyList())
|
||||
|
||||
val favoriteDAppsUIFlow = favoriteDAppsFlow
|
||||
.map { dapps -> dapps.map { mapFavoriteDappToDappModel(it) } }
|
||||
.shareInBackground()
|
||||
|
||||
init {
|
||||
launch {
|
||||
updateDApps()
|
||||
}
|
||||
}
|
||||
|
||||
fun backClicked() {
|
||||
router.back()
|
||||
}
|
||||
|
||||
fun openDApp(dapp: DappModel) {
|
||||
router.openDAppBrowser(DAppBrowserPayload.Address(dapp.url))
|
||||
}
|
||||
|
||||
fun onFavoriteClicked(dapp: DappModel) = launch {
|
||||
removeFavouriteConfirmationAwaitable.awaitAction(dapp.name)
|
||||
|
||||
interactor.removeDAppFromFavourites(dapp.url)
|
||||
|
||||
// Update list, since item was removed
|
||||
updateDApps()
|
||||
}
|
||||
|
||||
fun changeDAppOrdering(newOrdering: List<DappModel>) = launch {
|
||||
val favoriteItems = newOrdering.mapIndexed { index, dappModel ->
|
||||
mapDAppModelToFavorite(dappModel, index)
|
||||
}
|
||||
|
||||
interactor.updateFavoriteDapps(favoriteItems)
|
||||
}
|
||||
|
||||
private suspend fun updateDApps() {
|
||||
val dapps = interactor.getFavoriteDApps()
|
||||
favoriteDAppsFlow.value = dapps
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.favorites
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.recyclerView.dragging.OnItemDragCallback
|
||||
import io.novafoundation.nova.common.utils.recyclerView.dragging.StartDragListener
|
||||
import io.novafoundation.nova.common.utils.recyclerView.dragging.prepareForDragging
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemDappFavoriteDragableBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
import java.util.Collections
|
||||
|
||||
class DappDraggableFavoritesAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val handler: Handler,
|
||||
private val startDragListener: StartDragListener
|
||||
) : RecyclerView.Adapter<DappDraggableFavoritesViewHolder>(), OnItemDragCallback {
|
||||
|
||||
interface Handler {
|
||||
fun onDAppClicked(dapp: DappModel)
|
||||
|
||||
fun onDAppFavoriteClicked(dapp: DappModel)
|
||||
|
||||
fun onItemOrderingChanged(dapps: List<DappModel>)
|
||||
}
|
||||
|
||||
private val dapps = mutableListOf<DappModel>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DappDraggableFavoritesViewHolder {
|
||||
return DappDraggableFavoritesViewHolder(
|
||||
ItemDappFavoriteDragableBinding.inflate(parent.inflater(), parent, false),
|
||||
imageLoader,
|
||||
handler,
|
||||
startDragListener
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dapps.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DappDraggableFavoritesViewHolder, position: Int) {
|
||||
holder.bind(dapps[position])
|
||||
}
|
||||
|
||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||
Collections.swap(dapps, fromPosition, toPosition)
|
||||
notifyItemMoved(toPosition, fromPosition)
|
||||
handler.onItemOrderingChanged(dapps)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun submitList(dapps: List<DappModel>) {
|
||||
this.dapps.clear()
|
||||
this.dapps.addAll(dapps)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
class DappDraggableFavoritesViewHolder(
|
||||
private val binder: ItemDappFavoriteDragableBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: DappDraggableFavoritesAdapter.Handler,
|
||||
private val startDragListener: StartDragListener
|
||||
) : ViewHolder(binder.root) {
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun bind(item: DappModel) = with(itemView) {
|
||||
binder.itemDraggableFavoriteDAppIcon.showDAppIcon(item.iconUrl, imageLoader)
|
||||
binder.itemDraggableFavoriteDAppTitle.text = item.name
|
||||
binder.itemDraggableFavoriteDAppSubtitle.text = item.description
|
||||
|
||||
binder.itemDraggableFavoriteDappDragHandle.prepareForDragging(this@DappDraggableFavoritesViewHolder, startDragListener)
|
||||
|
||||
binder.itemDraggableFavoriteDappFavoriteIcon.setOnClickListener { itemHandler.onDAppFavoriteClicked(item) }
|
||||
setOnClickListener { itemHandler.onDAppClicked(item) }
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.favorites
|
||||
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseBottomSheetFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.recyclerView.dragging.SimpleItemDragHelperCallback
|
||||
import io.novafoundation.nova.common.utils.recyclerView.dragging.StartDragListener
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentFavoritesDappBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.favourites.setupRemoveFavouritesConfirmation
|
||||
import javax.inject.Inject
|
||||
|
||||
class DappFavoritesFragment :
|
||||
BaseBottomSheetFragment<DAppFavoritesViewModel, FragmentFavoritesDappBinding>(),
|
||||
DappDraggableFavoritesAdapter.Handler,
|
||||
StartDragListener {
|
||||
|
||||
override fun createBinding() = FragmentFavoritesDappBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) { DappDraggableFavoritesAdapter(imageLoader, this, this) }
|
||||
|
||||
private val itemDragHelper by lazy(LazyThreadSafetyMode.NONE) { ItemTouchHelper(SimpleItemDragHelperCallback(adapter)) }
|
||||
|
||||
override fun initViews() {
|
||||
binder.favoritesDappToolbar.applyStatusBarInsets()
|
||||
binder.favoritesDappToolbar.setHomeButtonListener { viewModel.backClicked() }
|
||||
binder.favoritesDappList.adapter = adapter
|
||||
itemDragHelper.attachToRecyclerView(binder.favoritesDappList)
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.dAppFavoritesComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: DAppFavoritesViewModel) {
|
||||
setupRemoveFavouritesConfirmation(viewModel.removeFavouriteConfirmationAwaitable)
|
||||
|
||||
viewModel.favoriteDAppsUIFlow.observe {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDAppClicked(dapp: DappModel) {
|
||||
viewModel.openDApp(dapp)
|
||||
}
|
||||
|
||||
override fun onDAppFavoriteClicked(dapp: DappModel) {
|
||||
viewModel.onFavoriteClicked(dapp)
|
||||
}
|
||||
|
||||
override fun onItemOrderingChanged(dapps: List<DappModel>) {
|
||||
viewModel.changeDAppOrdering(dapps)
|
||||
}
|
||||
|
||||
override fun requestDrag(viewHolder: RecyclerView.ViewHolder) {
|
||||
itemDragHelper.startDrag(viewHolder)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.favorites.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.favorites.DappFavoritesFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
DAppFavoritesModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface DAppFavoritesComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): DAppFavoritesComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: DappFavoritesFragment)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.favorites.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.favorites.DAppFavoritesViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class DAppFavoritesModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): DAppFavoritesViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(DAppFavoritesViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(DAppFavoritesViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: DAppRouter,
|
||||
interactor: DappInteractor,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
): ViewModel {
|
||||
return DAppFavoritesViewModel(
|
||||
router,
|
||||
interactor,
|
||||
actionAwaitableMixinFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setVisible
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedWalletModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemDappHeaderBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.model.DAppCategoryModel
|
||||
|
||||
class DAppHeaderAdapter(
|
||||
val imageLoader: ImageLoader,
|
||||
val headerHandler: Handler,
|
||||
val categoriesHandler: DappCategoriesAdapter.Handler
|
||||
) : RecyclerView.Adapter<HeaderHolder>() {
|
||||
|
||||
private var walletModel: SelectedWalletModel? = null
|
||||
private var categories: List<DAppCategoryModel> = emptyList()
|
||||
private var favoritesDApps: List<DappModel> = emptyList()
|
||||
private var showCategoriesShimmering: Boolean = false
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun onWalletClick()
|
||||
|
||||
fun onSearchClick()
|
||||
|
||||
fun onManageClick()
|
||||
|
||||
fun onManageFavoritesClick()
|
||||
|
||||
fun onCategoryClicked(id: String)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderHolder {
|
||||
return HeaderHolder(
|
||||
imageLoader,
|
||||
ItemDappHeaderBinding.inflate(parent.inflater(), parent, false),
|
||||
headerHandler,
|
||||
categoriesHandler
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderHolder, position: Int) {
|
||||
holder.bind(
|
||||
walletModel,
|
||||
categories,
|
||||
favoritesDApps,
|
||||
showCategoriesShimmering
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HeaderHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
} else {
|
||||
payloads.filterIsInstance<Payload>().forEach {
|
||||
when (it) {
|
||||
Payload.WALLET -> holder.bindWallet(walletModel)
|
||||
Payload.CATEGORIES -> holder.bindCategories(categories)
|
||||
Payload.CATEGORIES_SHIMMERING -> holder.bindCategoriesShimmering(showCategoriesShimmering)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
fun setWallet(walletModel: SelectedWalletModel) {
|
||||
this.walletModel = walletModel
|
||||
notifyItemChanged(0, Payload.WALLET)
|
||||
}
|
||||
|
||||
fun setCategories(categories: List<DAppCategoryModel>) {
|
||||
this.categories = categories
|
||||
notifyItemChanged(0, Payload.CATEGORIES)
|
||||
}
|
||||
|
||||
fun showCategoriesShimmering(show: Boolean) {
|
||||
showCategoriesShimmering = show
|
||||
notifyItemChanged(0, Payload.CATEGORIES_SHIMMERING)
|
||||
}
|
||||
}
|
||||
|
||||
class HeaderHolder(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val binder: ItemDappHeaderBinding,
|
||||
headerHandler: DAppHeaderAdapter.Handler,
|
||||
categoriesHandler: DappCategoriesAdapter.Handler
|
||||
) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
private val categoriesAdapter = DappCategoriesAdapter(imageLoader, categoriesHandler)
|
||||
|
||||
init {
|
||||
binder.dappMainSelectedWallet.setOnClickListener { headerHandler.onWalletClick() }
|
||||
binder.dappMainSearch.setOnClickListener { headerHandler.onSearchClick() }
|
||||
binder.dappMainManage.setOnClickListener { headerHandler.onManageClick() }
|
||||
binder.mainDappCategories.adapter = categoriesAdapter
|
||||
}
|
||||
|
||||
fun bind(
|
||||
walletModel: SelectedWalletModel?,
|
||||
categoriesState: List<DAppCategoryModel>,
|
||||
favoritesDApps: List<DappModel>,
|
||||
showCategoriesShimmering: Boolean
|
||||
) {
|
||||
bindWallet(walletModel)
|
||||
bindCategories(categoriesState)
|
||||
bindCategoriesShimmering(showCategoriesShimmering)
|
||||
}
|
||||
|
||||
fun bindWallet(walletModel: SelectedWalletModel?) = with(binder) {
|
||||
walletModel?.let { dappMainSelectedWallet.setModel(walletModel) }
|
||||
}
|
||||
|
||||
fun bindCategories(categoriesState: List<DAppCategoryModel>) = with(binder) {
|
||||
categoriesAdapter.submitList(categoriesState)
|
||||
}
|
||||
|
||||
fun bindCategoriesShimmering(showCategoriesShimmering: Boolean) = with(itemView) {
|
||||
binder.categorizedDappsCategoriesShimmering.setVisible(showCategoriesShimmering, falseState = View.INVISIBLE)
|
||||
binder.mainDappCategories.isInvisible = showCategoriesShimmering
|
||||
}
|
||||
}
|
||||
|
||||
private enum class Payload {
|
||||
WALLET, CATEGORIES, CATEGORIES_SHIMMERING
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.PayloadGenerator
|
||||
import io.novafoundation.nova.common.list.resolvePayload
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.loadOrHide
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemDappCategoryBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.model.DAppCategoryModel
|
||||
|
||||
class DappCategoriesAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val handler: Handler,
|
||||
) : ListAdapter<DAppCategoryModel, DappCategoryViewHolder>(DappDiffCallback) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun onCategoryClicked(id: String)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DappCategoryViewHolder {
|
||||
return DappCategoryViewHolder(ItemDappCategoryBinding.inflate(parent.inflater(), parent, false), imageLoader, handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DappCategoryViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
val item = getItem(position)
|
||||
|
||||
resolvePayload(holder, position, payloads) {
|
||||
when (it) {
|
||||
DAppCategoryModel::selected -> holder.bindSelected(item.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DappCategoryViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
private val dAppCategoryPayloadGenerator = PayloadGenerator(DAppCategoryModel::selected)
|
||||
|
||||
private object DappDiffCallback : DiffUtil.ItemCallback<DAppCategoryModel>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: DAppCategoryModel, newItem: DAppCategoryModel): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DAppCategoryModel, newItem: DAppCategoryModel): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: DAppCategoryModel, newItem: DAppCategoryModel): Any? {
|
||||
return dAppCategoryPayloadGenerator.diff(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
|
||||
class DappCategoryViewHolder(
|
||||
private val binder: ItemDappCategoryBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: DappCategoriesAdapter.Handler,
|
||||
) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
fun bind(item: DAppCategoryModel) = with(binder) {
|
||||
itemDappCategoryIcon.loadOrHide(item.iconUrl, imageLoader)
|
||||
itemDappCategoryText.text = item.name
|
||||
|
||||
bindSelected(item.selected)
|
||||
|
||||
binder.root.setOnClickListener { itemHandler.onCategoryClicked(item.id) }
|
||||
}
|
||||
|
||||
fun bindSelected(isSelected: Boolean) = with(binder) {
|
||||
root.isSelected = isSelected
|
||||
|
||||
// We must set tint to image view programmatically since we can't specify the state for default color in state list
|
||||
if (isSelected) {
|
||||
itemDappCategoryIcon.setColorFilter(ContextCompat.getColor(itemView.context, R.color.icon_primary_on_content))
|
||||
} else {
|
||||
itemDappCategoryIcon.clearColorFilter()
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemFavoriteDappBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DAppClickHandler
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModelDiffCallback
|
||||
import io.novafoundation.nova.feature_external_sign_api.presentation.dapp.showDAppIcon
|
||||
|
||||
class DappFavoritesAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val handler: DAppClickHandler
|
||||
) : ListAdapter<DappModel, FavoriteDappViewHolder>(DappModelDiffCallback) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteDappViewHolder {
|
||||
return FavoriteDappViewHolder(ItemFavoriteDappBinding.inflate(parent.inflater(), parent, false), imageLoader, handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FavoriteDappViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
class FavoriteDappViewHolder(
|
||||
private val binder: ItemFavoriteDappBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: DAppClickHandler,
|
||||
) : ViewHolder(binder.root) {
|
||||
|
||||
fun bind(item: DappModel) = with(binder) {
|
||||
itemFavoriteDAppIcon.showDAppIcon(item.iconUrl, imageLoader)
|
||||
itemFavoriteDAppTitle.text = item.name
|
||||
|
||||
binder.root.setOnClickListener { itemHandler.onDAppClicked(item) }
|
||||
}
|
||||
}
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.ConcatAdapter
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.list.CustomPlaceholderAdapter
|
||||
import io.novafoundation.nova.common.mixin.impl.observeBrowserEvents
|
||||
import io.novafoundation.nova.common.presentation.LoadingState
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.recyclerView.space.SpaceBetween
|
||||
import io.novafoundation.nova.common.utils.recyclerView.space.addSpaceItemDecoration
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannerAdapter
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.bindWithAdapter
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentDappMainBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DAppClickHandler
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappCategoryListAdapter
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappCategoryViewHolder
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainDAppFragment :
|
||||
BaseFragment<MainDAppViewModel, FragmentDappMainBinding>(),
|
||||
DAppClickHandler,
|
||||
DAppHeaderAdapter.Handler,
|
||||
DappCategoriesAdapter.Handler,
|
||||
MainFavoriteDAppsAdapter.Handler {
|
||||
|
||||
override fun createBinding() = FragmentDappMainBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val headerAdapter by lazy(LazyThreadSafetyMode.NONE) { DAppHeaderAdapter(imageLoader, this, this) }
|
||||
|
||||
private val bannerAdapter: PromotionBannerAdapter by lazy(LazyThreadSafetyMode.NONE) { PromotionBannerAdapter(closable = false) }
|
||||
|
||||
private val favoritesAdapter: MainFavoriteDAppsAdapter by lazy(LazyThreadSafetyMode.NONE) { MainFavoriteDAppsAdapter(this, this, imageLoader) }
|
||||
|
||||
private val dappsShimmering by lazy(LazyThreadSafetyMode.NONE) { CustomPlaceholderAdapter(R.layout.layout_dapps_shimmering) }
|
||||
|
||||
private val dappCategoriesListAdapter by lazy(LazyThreadSafetyMode.NONE) { DappCategoryListAdapter(this) }
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.dappRecyclerViewCatalog.applyStatusBarInsets()
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.dappRecyclerViewCatalog.adapter = ConcatAdapter(headerAdapter, bannerAdapter, favoritesAdapter, dappsShimmering, dappCategoriesListAdapter)
|
||||
binder.dappRecyclerViewCatalog.itemAnimator = null
|
||||
setupRecyclerViewSpacing()
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.mainComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: MainDAppViewModel) {
|
||||
observeBrowserEvents(viewModel)
|
||||
viewModel.bannersMixin.bindWithAdapter(bannerAdapter) {
|
||||
binder.dappRecyclerViewCatalog?.invalidateItemDecorations()
|
||||
}
|
||||
|
||||
viewModel.selectedWalletFlow.observe(headerAdapter::setWallet)
|
||||
|
||||
viewModel.shownDAppsStateFlow.observe { state ->
|
||||
when (state) {
|
||||
is LoadingState.Loaded -> {
|
||||
dappsShimmering.show(false)
|
||||
dappCategoriesListAdapter.submitList(state.data)
|
||||
}
|
||||
|
||||
is LoadingState.Loading -> {
|
||||
dappsShimmering.show(true)
|
||||
dappCategoriesListAdapter.submitList(listOf())
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.categoriesStateFlow.observe { state ->
|
||||
headerAdapter.showCategoriesShimmering(state is LoadingState.Loading)
|
||||
if (state is LoadingState.Loaded) {
|
||||
headerAdapter.setCategories(state.data.categories)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.favoriteDAppsUIFlow.observe {
|
||||
favoritesAdapter.show(it.isNotEmpty())
|
||||
favoritesAdapter.setDApps(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCategoryClicked(id: String) {
|
||||
viewModel.openCategory(id)
|
||||
}
|
||||
|
||||
override fun onDAppClicked(item: DappModel) {
|
||||
viewModel.dappClicked(item)
|
||||
}
|
||||
|
||||
override fun onWalletClick() {
|
||||
viewModel.accountIconClicked()
|
||||
}
|
||||
|
||||
override fun onSearchClick() {
|
||||
viewModel.searchClicked()
|
||||
}
|
||||
|
||||
override fun onManageClick() {
|
||||
viewModel.manageClicked()
|
||||
}
|
||||
|
||||
override fun onManageFavoritesClick() {
|
||||
viewModel.openFavorites()
|
||||
}
|
||||
|
||||
private fun setupRecyclerViewSpacing() {
|
||||
binder.dappRecyclerViewCatalog.addSpaceItemDecoration {
|
||||
// Add extra space between items
|
||||
add(SpaceBetween(DappCategoryViewHolder, spaceDp = 8))
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.api.Browserable
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.indexOfFirstOrNull
|
||||
import io.novafoundation.nova.common.utils.withLoading
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.dappsSource
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.dappCategoryToUi
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapDAppCatalogToDAppCategoryModels
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.mapFavoriteDappToDappModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.model.DAppCategoryState
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainDAppViewModel(
|
||||
private val promotionBannersMixinFactory: PromotionBannersMixinFactory,
|
||||
private val bannerSourceFactory: BannersSourceFactory,
|
||||
private val router: DAppRouter,
|
||||
private val selectedAccountUseCase: SelectedAccountUseCase,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val dappInteractor: DappInteractor,
|
||||
private val resourceManager: ResourceManager
|
||||
) : BaseViewModel(), Browserable {
|
||||
|
||||
override val openBrowserEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val selectedWalletFlow = selectedAccountUseCase.selectedWalletModelFlow()
|
||||
.shareInBackground()
|
||||
|
||||
val bannersMixin = promotionBannersMixinFactory.create(bannerSourceFactory.dappsSource(), viewModelScope)
|
||||
|
||||
private val favoriteDAppsFlow = dappInteractor.observeFavoriteDApps()
|
||||
.shareInBackground()
|
||||
|
||||
private val groupedDAppsFlow = dappInteractor.observeDAppsByCategory()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
private val groupedDAppsUiFlow = groupedDAppsFlow
|
||||
.map { mapDAppCatalogToDAppCategoryModels(resourceManager, it) }
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val favoriteDAppsUIFlow = favoriteDAppsFlow
|
||||
.map { dapps -> dapps.map { mapFavoriteDappToDappModel(it) } }
|
||||
.shareInBackground()
|
||||
|
||||
val shownDAppsStateFlow = groupedDAppsUiFlow
|
||||
.filterNotNull()
|
||||
.withLoading()
|
||||
.share()
|
||||
|
||||
val categoriesStateFlow = groupedDAppsFlow
|
||||
.map { catalog -> catalog.categoriesWithDApps.keys.map { dappCategoryToUi(it, isSelected = false) } }
|
||||
.map { categories ->
|
||||
DAppCategoryState(
|
||||
categories = categories,
|
||||
selectedIndex = categories.indexOfFirstOrNull { it.selected }
|
||||
)
|
||||
}
|
||||
.inBackground()
|
||||
.withLoading()
|
||||
.share()
|
||||
|
||||
init {
|
||||
syncDApps()
|
||||
}
|
||||
|
||||
fun openCategory(categoryId: String) {
|
||||
router.openDappSearchWithCategory(categoryId)
|
||||
}
|
||||
|
||||
fun accountIconClicked() {
|
||||
router.openChangeAccount()
|
||||
}
|
||||
|
||||
fun dappClicked(dapp: DappModel) {
|
||||
router.openDAppBrowser(DAppBrowserPayload.Address(dapp.url))
|
||||
}
|
||||
|
||||
fun searchClicked() {
|
||||
router.openDappSearch()
|
||||
}
|
||||
|
||||
fun manageClicked() {
|
||||
router.openAuthorizedDApps()
|
||||
}
|
||||
|
||||
private fun syncDApps() = launch {
|
||||
dappInteractor.dAppsSync()
|
||||
}
|
||||
|
||||
fun openFavorites() {
|
||||
router.openDAppFavorites()
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.list.SingleItemAdapter
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemMainFavoriteDappsBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DAppClickHandler
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.DappModel
|
||||
|
||||
class MainFavoriteDAppsAdapter(
|
||||
private val dappClickHandler: DAppClickHandler,
|
||||
private val handler: Handler,
|
||||
private val imageLoader: ImageLoader
|
||||
) : SingleItemAdapter<FavoriteDAppHolder>(isShownByDefault = true) {
|
||||
|
||||
interface Handler {
|
||||
fun onManageFavoritesClick()
|
||||
}
|
||||
|
||||
private var favoritesDApps: List<DappModel> = emptyList()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FavoriteDAppHolder {
|
||||
val binder = ItemMainFavoriteDappsBinding.inflate(parent.inflater(), parent, false)
|
||||
return FavoriteDAppHolder(binder, imageLoader, dappClickHandler, handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FavoriteDAppHolder, position: Int) {
|
||||
holder.bind(favoritesDApps)
|
||||
}
|
||||
|
||||
fun setDApps(dapps: List<DappModel>) {
|
||||
favoritesDApps = dapps
|
||||
notifyChangedIfShown()
|
||||
}
|
||||
}
|
||||
|
||||
class FavoriteDAppHolder(
|
||||
private val binder: ItemMainFavoriteDappsBinding,
|
||||
imageLoader: ImageLoader,
|
||||
dAppClickHandler: DAppClickHandler,
|
||||
handler: MainFavoriteDAppsAdapter.Handler
|
||||
) : RecyclerView.ViewHolder(binder.root) {
|
||||
|
||||
private val favoritesAdapter = DappFavoritesAdapter(imageLoader, dAppClickHandler)
|
||||
|
||||
init {
|
||||
binder.dAppMainFavoriteDAppList.adapter = favoritesAdapter
|
||||
binder.dAppMainFavoriteDAppsShow.setOnClickListener { handler.onManageFavoritesClick() }
|
||||
}
|
||||
|
||||
fun bind(dapps: List<DappModel>) = with(binder) {
|
||||
dAppMainFavoriteDAppList.isGone = dapps.isEmpty()
|
||||
dAppMainFavoriteDAppTitle.isGone = dapps.isEmpty()
|
||||
dAppMainFavoriteDAppsShow.isGone = dapps.isEmpty()
|
||||
|
||||
favoritesAdapter.submitList(dapps)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.MainDAppFragment
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
MainDAppModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface MainDAppComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment
|
||||
): MainDAppComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: MainDAppFragment)
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
|
||||
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.MainDAppViewModel
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class MainDAppModule {
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): MainDAppViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(MainDAppViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(MainDAppViewModel::class)
|
||||
fun provideViewModel(
|
||||
promotionBannersMixinFactory: PromotionBannersMixinFactory,
|
||||
bannerSourceFactory: BannersSourceFactory,
|
||||
selectedAccountUseCase: SelectedAccountUseCase,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
router: DAppRouter,
|
||||
dappInteractor: DappInteractor,
|
||||
resourceManager: ResourceManager
|
||||
): ViewModel {
|
||||
return MainDAppViewModel(
|
||||
promotionBannersMixinFactory = promotionBannersMixinFactory,
|
||||
bannerSourceFactory = bannerSourceFactory,
|
||||
router = router,
|
||||
selectedAccountUseCase = selectedAccountUseCase,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory,
|
||||
dappInteractor = dappInteractor,
|
||||
resourceManager = resourceManager
|
||||
)
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main.model
|
||||
|
||||
data class DAppCategoryModel(
|
||||
val id: String,
|
||||
val iconUrl: String?,
|
||||
val name: String,
|
||||
val selected: Boolean
|
||||
)
|
||||
|
||||
class DAppCategoryState(
|
||||
val categories: List<DAppCategoryModel>,
|
||||
val selectedIndex: Int?
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.main.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.view.ContextThemeWrapper
|
||||
import androidx.appcompat.widget.AppCompatTextView
|
||||
import io.novafoundation.nova.common.utils.WithContextExtensions
|
||||
import io.novafoundation.nova.common.utils.setDrawableStart
|
||||
import io.novafoundation.nova.common.utils.setTextColorRes
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
|
||||
class TapToSearchView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
) : AppCompatTextView(ContextThemeWrapper(context, R.style.TextAppearance_NovaFoundation_Regular_SubHeadline), attrs, defStyleAttr),
|
||||
WithContextExtensions {
|
||||
|
||||
override val providedContext: Context
|
||||
get() = context
|
||||
|
||||
init {
|
||||
setPaddingRelative(12.dp, 0.dp, 12.dp, 0.dp)
|
||||
|
||||
gravity = android.view.Gravity.CENTER_VERTICAL
|
||||
|
||||
setDrawableStart(
|
||||
drawableRes = R.drawable.ic_search,
|
||||
widthInDp = 16,
|
||||
heightInDp = 16,
|
||||
paddingInDp = 6,
|
||||
tint = R.color.icon_secondary
|
||||
)
|
||||
|
||||
text = context.getString(R.string.dapp_search_hint)
|
||||
setTextColorRes(R.color.hint_text)
|
||||
|
||||
background = addRipple(getRoundedCornerDrawable(R.color.block_background))
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
import android.os.Parcelable
|
||||
import io.novafoundation.nova.common.navigation.InterScreenRequester
|
||||
import io.novafoundation.nova.common.navigation.InterScreenResponder
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator.Response
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
interface DAppSearchRequester : InterScreenRequester<SearchPayload, Response>
|
||||
|
||||
interface DAppSearchResponder : InterScreenResponder<SearchPayload, Response>
|
||||
|
||||
interface DAppSearchCommunicator : DAppSearchRequester, DAppSearchResponder {
|
||||
|
||||
sealed interface Response : Parcelable {
|
||||
|
||||
@Parcelize
|
||||
class NewUrl(val url: String) : Response
|
||||
|
||||
@Parcelize
|
||||
object Cancel : Response
|
||||
}
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.list.headers.TextHeader
|
||||
import io.novafoundation.nova.common.list.toListWithHeaders
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.common.utils.Event
|
||||
import io.novafoundation.nova.common.utils.inBackground
|
||||
import io.novafoundation.nova.common.utils.sendEvent
|
||||
import io.novafoundation.nova.common.utils.withSafeLoading
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.DappSearchGroup
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.DappSearchResult
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.SearchDappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.common.dappCategoryToUi
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.model.DappSearchModel
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class DAppSearchViewModel(
|
||||
private val router: DAppRouter,
|
||||
private val resourceManager: ResourceManager,
|
||||
private val interactor: SearchDappInteractor,
|
||||
private val dappInteractor: DappInteractor,
|
||||
private val payload: SearchPayload,
|
||||
private val dAppSearchResponder: DAppSearchResponder,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val appLinksProvider: AppLinksProvider
|
||||
) : BaseViewModel() {
|
||||
|
||||
val dAppNotInCatalogWarning = actionAwaitableMixinFactory.confirmingAction<DappUnknownWarningModel>()
|
||||
|
||||
val query = MutableStateFlow(payload.initialUrl.orEmpty())
|
||||
|
||||
private val _selectQueryTextEvent = MutableLiveData<Event<Unit>>()
|
||||
val selectQueryTextEvent: LiveData<Event<Unit>> = _selectQueryTextEvent
|
||||
|
||||
private val selectedCategoryId = MutableStateFlow(payload.preselectedCategoryId)
|
||||
|
||||
val categoriesFlow = combine(
|
||||
interactor.categories(),
|
||||
selectedCategoryId
|
||||
) { categories, categoryId ->
|
||||
categories.map { dappCategoryToUi(it, isSelected = it.id == categoryId) }
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.withSafeLoading()
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
val searchResults = combine(query, selectedCategoryId) { query, categoryId ->
|
||||
interactor.searchDapps(query, categoryId)
|
||||
.mapKeys { (searchGroup, _) -> mapSearchGroupToTextHeader(searchGroup) }
|
||||
.mapValues { (_, groupItems) -> groupItems.map(::mapSearchResultToSearchModel) }
|
||||
.toListWithHeaders()
|
||||
}
|
||||
.inBackground()
|
||||
.share()
|
||||
|
||||
init {
|
||||
if (!payload.initialUrl.isNullOrEmpty()) {
|
||||
_selectQueryTextEvent.sendEvent()
|
||||
}
|
||||
|
||||
launch {
|
||||
dappInteractor.dAppsSync()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelClicked() {
|
||||
if (shouldReportResult()) {
|
||||
dAppSearchResponder.respond(DAppSearchCommunicator.Response.Cancel)
|
||||
}
|
||||
|
||||
router.back()
|
||||
}
|
||||
|
||||
private fun mapSearchGroupToTextHeader(searchGroup: DappSearchGroup): TextHeader {
|
||||
val content = when (searchGroup) {
|
||||
DappSearchGroup.DAPPS -> resourceManager.getString(R.string.dapp_dapps)
|
||||
DappSearchGroup.SEARCH -> resourceManager.getString(R.string.common_search)
|
||||
}
|
||||
|
||||
return TextHeader(content)
|
||||
}
|
||||
|
||||
private fun mapSearchResultToSearchModel(searchResult: DappSearchResult): DappSearchModel {
|
||||
return when (searchResult) {
|
||||
is DappSearchResult.Dapp -> DappSearchModel(
|
||||
title = searchResult.dapp.name,
|
||||
description = searchResult.dapp.description,
|
||||
icon = searchResult.dapp.iconLink,
|
||||
searchResult = searchResult,
|
||||
actionIcon = R.drawable.ic_favorite_heart_filled_20.takeIf { searchResult.dapp.isFavourite }
|
||||
)
|
||||
|
||||
is DappSearchResult.Search -> DappSearchModel(
|
||||
title = searchResult.query,
|
||||
searchResult = searchResult,
|
||||
actionIcon = null
|
||||
)
|
||||
|
||||
is DappSearchResult.Url -> DappSearchModel(
|
||||
title = searchResult.url,
|
||||
searchResult = searchResult,
|
||||
actionIcon = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun searchResultClicked(searchResult: DappSearchResult) {
|
||||
launch {
|
||||
val newUrl = when (searchResult) {
|
||||
is DappSearchResult.Dapp -> searchResult.dapp.url
|
||||
is DappSearchResult.Search -> searchResult.searchUrl
|
||||
is DappSearchResult.Url -> searchResult.url
|
||||
}
|
||||
|
||||
if (!searchResult.isTrustedByNova) {
|
||||
dAppNotInCatalogWarning.awaitAction(DappUnknownWarningModel(appLinksProvider.email))
|
||||
}
|
||||
|
||||
when (payload.request) {
|
||||
SearchPayload.Request.GO_TO_URL -> {
|
||||
dAppSearchResponder.respond(DAppSearchCommunicator.Response.NewUrl(newUrl))
|
||||
router.finishDappSearch()
|
||||
}
|
||||
|
||||
SearchPayload.Request.OPEN_NEW_URL -> router.openDAppBrowser(DAppBrowserPayload.Address(newUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldReportResult() = when (payload.request) {
|
||||
SearchPayload.Request.GO_TO_URL -> true
|
||||
|
||||
SearchPayload.Request.OPEN_NEW_URL -> false
|
||||
}
|
||||
|
||||
fun onCategoryClicked(id: String) {
|
||||
if (selectedCategoryId.value == id) {
|
||||
selectedCategoryId.value = null
|
||||
} else {
|
||||
selectedCategoryId.value = id
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
import android.view.View
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.common.domain.isLoaded
|
||||
import io.novafoundation.nova.common.domain.isLoading
|
||||
import io.novafoundation.nova.common.domain.onLoaded
|
||||
import io.novafoundation.nova.common.utils.insets.applyNavigationBarInsets
|
||||
import io.novafoundation.nova.common.utils.insets.applyStatusBarInsets
|
||||
import io.novafoundation.nova.common.utils.bindTo
|
||||
import io.novafoundation.nova.common.utils.insets.ImeInsetsState
|
||||
import io.novafoundation.nova.common.utils.keyboard.hideSoftKeyboard
|
||||
import io.novafoundation.nova.common.utils.keyboard.showSoftKeyboard
|
||||
import io.novafoundation.nova.common.view.dialog.warningDialog
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.R
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentSearchDappBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.DappSearchResult
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.main.DappCategoriesAdapter
|
||||
import javax.inject.Inject
|
||||
|
||||
class DappSearchFragment : BaseFragment<DAppSearchViewModel, FragmentSearchDappBinding>(), SearchDappAdapter.Handler, DappCategoriesAdapter.Handler {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val PAYLOAD = "DappSearchFragment.PAYLOAD"
|
||||
|
||||
fun getBundle(payload: SearchPayload) = bundleOf(
|
||||
PAYLOAD to payload
|
||||
)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentSearchDappBinding.inflate(layoutInflater)
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val categoriesAdapter by lazy(LazyThreadSafetyMode.NONE) { DappCategoriesAdapter(imageLoader, this) }
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) { SearchDappAdapter(this) }
|
||||
|
||||
override fun applyInsets(rootView: View) {
|
||||
binder.searchDappSearch.applyStatusBarInsets()
|
||||
binder.searchDappSearhContainer.applyNavigationBarInsets(consume = false, imeInsets = ImeInsetsState.ENABLE_IF_SUPPORTED)
|
||||
}
|
||||
|
||||
override fun initViews() {
|
||||
binder.searchDappCategories.adapter = categoriesAdapter
|
||||
binder.searchDappList.adapter = adapter
|
||||
binder.searchDappList.setHasFixedSize(true)
|
||||
|
||||
binder.searchDappSearch.cancel.setOnClickListener {
|
||||
viewModel.cancelClicked()
|
||||
|
||||
hideKeyboard()
|
||||
}
|
||||
|
||||
binder.searchDappSearch.searchInput.requestFocus()
|
||||
binder.searchDappSearch.searchInput.content.showSoftKeyboard()
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.dAppSearchComponentFactory()
|
||||
.create(this, argument(PAYLOAD))
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: DAppSearchViewModel) {
|
||||
setupDAppNotInCatalogWarning()
|
||||
binder.searchDappSearch.searchInput.content.bindTo(viewModel.query, lifecycleScope)
|
||||
|
||||
viewModel.searchResults.observe(::submitListPreservingViewPoint)
|
||||
|
||||
viewModel.selectQueryTextEvent.observeEvent {
|
||||
binder.searchDappSearch.searchInput.content.selectAll()
|
||||
}
|
||||
|
||||
viewModel.categoriesFlow.observe {
|
||||
binder.searchDappCategoriesShimmering.isVisible = it.isLoading()
|
||||
binder.searchDappCategories.isVisible = it.isLoaded()
|
||||
it.onLoaded { categoriesAdapter.submitList(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun itemClicked(searchResult: DappSearchResult) {
|
||||
hideKeyboard()
|
||||
|
||||
viewModel.searchResultClicked(searchResult)
|
||||
}
|
||||
|
||||
private fun hideKeyboard() {
|
||||
binder.searchDappSearch.searchInput.hideSoftKeyboard()
|
||||
}
|
||||
|
||||
private fun submitListPreservingViewPoint(data: List<Any?>) {
|
||||
val recyclerViewState = binder.searchDappList.layoutManager!!.onSaveInstanceState()
|
||||
|
||||
adapter.submitList(data) {
|
||||
binder.searchDappList.layoutManager!!.onRestoreInstanceState(recyclerViewState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDAppNotInCatalogWarning() {
|
||||
viewModel.dAppNotInCatalogWarning.awaitableActionLiveData.observeEvent { event ->
|
||||
warningDialog(
|
||||
context = providedContext,
|
||||
onPositiveClick = { event.onCancel() },
|
||||
positiveTextRes = R.string.common_close,
|
||||
negativeTextRes = R.string.dapp_url_warning_open_anyway,
|
||||
onNegativeClick = { event.onSuccess(Unit) },
|
||||
styleRes = R.style.AccentNegativeAlertDialogTheme
|
||||
) {
|
||||
setTitle(R.string.dapp_url_warning_title)
|
||||
|
||||
setMessage(requireContext().getString(R.string.dapp_url_warning_subtitle, event.payload.supportEmail))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCategoryClicked(id: String) {
|
||||
viewModel.onCategoryClicked(id)
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
class DappUnknownWarningModel(
|
||||
val supportEmail: String
|
||||
)
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
import android.view.ViewGroup
|
||||
import io.novafoundation.nova.common.list.BaseGroupedDiffCallback
|
||||
import io.novafoundation.nova.common.list.GroupedListAdapter
|
||||
import io.novafoundation.nova.common.list.GroupedListHolder
|
||||
import io.novafoundation.nova.common.list.PayloadGenerator
|
||||
import io.novafoundation.nova.common.list.headers.TextHeader
|
||||
import io.novafoundation.nova.common.list.resolvePayload
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.DappSearchResult
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.view.DAppView
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemDappSearchCategoryBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.model.DappSearchModel
|
||||
|
||||
class SearchDappAdapter(
|
||||
private val handler: Handler
|
||||
) : GroupedListAdapter<TextHeader, DappSearchModel>(DiffCallback) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun itemClicked(searchResult: DappSearchResult)
|
||||
}
|
||||
|
||||
override fun createGroupViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return CategoryHolder(ItemDappSearchCategoryBinding.inflate(parent.inflater(), parent, false))
|
||||
}
|
||||
|
||||
override fun createChildViewHolder(parent: ViewGroup): GroupedListHolder {
|
||||
return SearchHolder(DAppView.createUsingMathParentWidth(parent.context), handler)
|
||||
}
|
||||
|
||||
override fun bindGroup(holder: GroupedListHolder, group: TextHeader) {
|
||||
(holder as CategoryHolder).bind(group)
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, child: DappSearchModel) {
|
||||
(holder as SearchHolder).bind(child)
|
||||
}
|
||||
|
||||
override fun bindChild(holder: GroupedListHolder, position: Int, child: DappSearchModel, payloads: List<Any>) {
|
||||
resolvePayload(holder, position, payloads) {
|
||||
(holder as SearchHolder).rebind(child) {
|
||||
when (it) {
|
||||
DappSearchModel::title -> bindTitle(child)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : BaseGroupedDiffCallback<TextHeader, DappSearchModel>(TextHeader::class.java) {
|
||||
|
||||
override fun areGroupItemsTheSame(oldItem: TextHeader, newItem: TextHeader): Boolean {
|
||||
return TextHeader.DIFF_CALLBACK.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areGroupContentsTheSame(oldItem: TextHeader, newItem: TextHeader): Boolean {
|
||||
return TextHeader.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areChildItemsTheSame(oldItem: DappSearchModel, newItem: DappSearchModel): Boolean {
|
||||
return when {
|
||||
isSingletonItem(oldItem) && isSingletonItem(newItem) -> true
|
||||
else -> oldItem.title == newItem.title
|
||||
}
|
||||
}
|
||||
|
||||
override fun areChildContentsTheSame(oldItem: DappSearchModel, newItem: DappSearchModel): Boolean {
|
||||
return oldItem.title == newItem.title && oldItem.description == newItem.description && oldItem.icon == newItem.icon
|
||||
}
|
||||
|
||||
override fun getChildChangePayload(oldItem: DappSearchModel, newItem: DappSearchModel): Any? {
|
||||
return SearchDappPayloadGenerator.diff(oldItem, newItem)
|
||||
}
|
||||
|
||||
private fun isSingletonItem(item: DappSearchModel) = when (item.searchResult) {
|
||||
is DappSearchResult.Search -> true
|
||||
is DappSearchResult.Url -> true
|
||||
is DappSearchResult.Dapp -> false
|
||||
}
|
||||
}
|
||||
|
||||
private object SearchDappPayloadGenerator : PayloadGenerator<DappSearchModel>(DappSearchModel::title)
|
||||
|
||||
private class CategoryHolder(private val binder: ItemDappSearchCategoryBinding) : GroupedListHolder(binder.root) {
|
||||
|
||||
fun bind(item: TextHeader) {
|
||||
binder.searchCategory.text = item.content
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchHolder(
|
||||
private val dAppView: DAppView,
|
||||
private val itemHandler: SearchDappAdapter.Handler
|
||||
) : GroupedListHolder(dAppView) {
|
||||
|
||||
override fun unbind() {
|
||||
dAppView.clearIcon()
|
||||
}
|
||||
|
||||
fun bind(item: DappSearchModel) = with(dAppView) {
|
||||
setIconUrl(item.icon)
|
||||
|
||||
bindTitle(item)
|
||||
setSubtitle(item.description)
|
||||
showSubtitle(item.description != null)
|
||||
|
||||
setActionResource(item.actionIcon)
|
||||
|
||||
bindClick(item)
|
||||
}
|
||||
|
||||
fun bindTitle(item: DappSearchModel) {
|
||||
dAppView.setTitle(item.title)
|
||||
}
|
||||
|
||||
fun rebind(item: DappSearchModel, action: SearchHolder.() -> Unit) = with(containerView) {
|
||||
bindClick(item)
|
||||
|
||||
action()
|
||||
}
|
||||
|
||||
private fun bindClick(item: DappSearchModel) = with(dAppView) {
|
||||
setOnClickListener { itemHandler.itemClicked(item.searchResult) }
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SearchPayload(
|
||||
val initialUrl: String?,
|
||||
val request: Request,
|
||||
val preselectedCategoryId: String? = null
|
||||
) : Parcelable {
|
||||
|
||||
enum class Request {
|
||||
GO_TO_URL,
|
||||
OPEN_NEW_URL,
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import dagger.BindsInstance
|
||||
import dagger.Subcomponent
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DappSearchFragment
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.SearchPayload
|
||||
|
||||
@Subcomponent(
|
||||
modules = [
|
||||
DAppSearchModule::class
|
||||
]
|
||||
)
|
||||
@ScreenScope
|
||||
interface DAppSearchComponent {
|
||||
|
||||
@Subcomponent.Factory
|
||||
interface Factory {
|
||||
|
||||
fun create(
|
||||
@BindsInstance fragment: Fragment,
|
||||
@BindsInstance payload: SearchPayload,
|
||||
): DAppSearchComponent
|
||||
}
|
||||
|
||||
fun inject(fragment: DappSearchFragment)
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search.di
|
||||
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.multibindings.IntoMap
|
||||
import io.novafoundation.nova.common.data.network.AppLinksProvider
|
||||
import io.novafoundation.nova.common.di.scope.ScreenScope
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelKey
|
||||
import io.novafoundation.nova.common.di.viewmodel.ViewModelModule
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.resources.ResourceManager
|
||||
import io.novafoundation.nova.feature_dapp_api.data.repository.DAppMetadataRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_impl.data.repository.FavouritesDAppRepository
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.DappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.SearchDappInteractor
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchCommunicator
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.DAppSearchViewModel
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.search.SearchPayload
|
||||
|
||||
@Module(includes = [ViewModelModule::class])
|
||||
class DAppSearchModule {
|
||||
|
||||
@Provides
|
||||
@ScreenScope
|
||||
fun provideInteractor(
|
||||
dAppMetadataRepository: DAppMetadataRepository,
|
||||
favouritesDAppRepository: FavouritesDAppRepository
|
||||
) = SearchDappInteractor(dAppMetadataRepository, favouritesDAppRepository)
|
||||
|
||||
@Provides
|
||||
internal fun provideViewModel(fragment: Fragment, factory: ViewModelProvider.Factory): DAppSearchViewModel {
|
||||
return ViewModelProvider(fragment, factory).get(DAppSearchViewModel::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@IntoMap
|
||||
@ViewModelKey(DAppSearchViewModel::class)
|
||||
fun provideViewModel(
|
||||
router: DAppRouter,
|
||||
resourceManager: ResourceManager,
|
||||
interactor: SearchDappInteractor,
|
||||
dappInteractor: DappInteractor,
|
||||
searchResponder: DAppSearchCommunicator,
|
||||
payload: SearchPayload,
|
||||
actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
appLinksProvider: AppLinksProvider
|
||||
): ViewModel {
|
||||
return DAppSearchViewModel(
|
||||
router = router,
|
||||
resourceManager = resourceManager,
|
||||
interactor = interactor,
|
||||
dAppSearchResponder = searchResponder,
|
||||
payload = payload,
|
||||
actionAwaitableMixinFactory = actionAwaitableMixinFactory,
|
||||
appLinksProvider = appLinksProvider,
|
||||
dappInteractor = dappInteractor
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.search.model
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import io.novafoundation.nova.feature_dapp_impl.domain.search.DappSearchResult
|
||||
|
||||
class DappSearchModel(
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
val icon: String? = null,
|
||||
@DrawableRes val actionIcon: Int?,
|
||||
val searchResult: DappSearchResult
|
||||
)
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.tab
|
||||
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
|
||||
data class BrowserTabRvItem(
|
||||
val tabId: String,
|
||||
val tabName: String?,
|
||||
val icon: Icon?,
|
||||
val tabScreenshotPath: String?,
|
||||
)
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.tab
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import coil.ImageLoader
|
||||
import coil.clear
|
||||
import coil.load
|
||||
import io.novafoundation.nova.common.list.BaseViewHolder
|
||||
import io.novafoundation.nova.common.utils.ImageMonitor
|
||||
import io.novafoundation.nova.common.utils.images.Icon
|
||||
import io.novafoundation.nova.common.utils.images.setIconOrMakeGone
|
||||
import io.novafoundation.nova.common.utils.inflater
|
||||
import io.novafoundation.nova.common.utils.setPathOrStopWatching
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.ItemBrowserTabBinding
|
||||
import java.io.File
|
||||
|
||||
class BrowserTabsAdapter(
|
||||
private val imageLoader: ImageLoader,
|
||||
private val handler: Handler
|
||||
) : ListAdapter<BrowserTabRvItem, BrowserTabViewHolder>(DiffCallback) {
|
||||
|
||||
interface Handler {
|
||||
|
||||
fun tabClicked(item: BrowserTabRvItem, view: View)
|
||||
|
||||
fun tabCloseClicked(item: BrowserTabRvItem)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BrowserTabViewHolder {
|
||||
return BrowserTabViewHolder(ItemBrowserTabBinding.inflate(parent.inflater(), parent, false), imageLoader, handler)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BrowserTabViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
private object DiffCallback : DiffUtil.ItemCallback<BrowserTabRvItem>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: BrowserTabRvItem, newItem: BrowserTabRvItem): Boolean {
|
||||
return oldItem.tabId == newItem.tabId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: BrowserTabRvItem, newItem: BrowserTabRvItem): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
class BrowserTabViewHolder(
|
||||
private val binder: ItemBrowserTabBinding,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val itemHandler: BrowserTabsAdapter.Handler,
|
||||
) : BaseViewHolder(binder.root) {
|
||||
|
||||
private val screenshotImageMonitor = ImageMonitor(
|
||||
imageView = binder.browserTabScreenshot,
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
|
||||
private val tabIconImageMonitor = ImageMonitor(
|
||||
imageView = binder.browserTabFavicon,
|
||||
imageLoader = imageLoader
|
||||
)
|
||||
|
||||
fun bind(item: BrowserTabRvItem) = with(binder) {
|
||||
browserTabCard.setOnClickListener { itemHandler.tabClicked(item, browserTabScreenshot) }
|
||||
browserTabClose.setOnClickListener { itemHandler.tabCloseClicked(item) }
|
||||
browserTabSiteName.text = item.tabName
|
||||
|
||||
browserTabScreenshot.load(item.tabScreenshotPath?.asFile(), imageLoader)
|
||||
screenshotImageMonitor.setPathOrStopWatching(item.tabScreenshotPath)
|
||||
|
||||
browserTabFavicon.setIconOrMakeGone(item.icon, imageLoader)
|
||||
|
||||
if (item.icon is Icon.FromFile) {
|
||||
tabIconImageMonitor.setPathOrStopWatching(item.icon.data.absolutePath)
|
||||
} else {
|
||||
tabIconImageMonitor.stopMonitoring()
|
||||
}
|
||||
}
|
||||
|
||||
override fun unbind() {
|
||||
screenshotImageMonitor.stopMonitoring()
|
||||
tabIconImageMonitor.stopMonitoring()
|
||||
|
||||
with(binder) {
|
||||
browserTabScreenshot.clear()
|
||||
browserTabFavicon.clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.asFile() = File(this)
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.tab
|
||||
|
||||
import android.view.View
|
||||
import androidx.navigation.fragment.FragmentNavigatorExtras
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import coil.ImageLoader
|
||||
import io.novafoundation.nova.common.base.BaseFragment
|
||||
import io.novafoundation.nova.common.di.FeatureUtils
|
||||
import io.novafoundation.nova.feature_dapp_api.di.DAppFeatureApi
|
||||
import io.novafoundation.nova.feature_dapp_impl.databinding.FragmentBrowserTabsBinding
|
||||
import io.novafoundation.nova.feature_dapp_impl.di.DAppFeatureComponent
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.browser.main.DAPP_SHARED_ELEMENT_ID_IMAGE_TAB
|
||||
import javax.inject.Inject
|
||||
|
||||
class BrowserTabsFragment : BaseFragment<BrowserTabsViewModel, FragmentBrowserTabsBinding>(), BrowserTabsAdapter.Handler {
|
||||
|
||||
@Inject
|
||||
lateinit var imageLoader: ImageLoader
|
||||
|
||||
private val adapter by lazy(LazyThreadSafetyMode.NONE) {
|
||||
BrowserTabsAdapter(imageLoader, this)
|
||||
}
|
||||
|
||||
override fun createBinding() = FragmentBrowserTabsBinding.inflate(layoutInflater)
|
||||
|
||||
override fun initViews() {
|
||||
onBackPressed { viewModel.done() }
|
||||
|
||||
binder.browserTabsList.layoutManager = GridLayoutManager(requireContext(), 2)
|
||||
binder.browserTabsList.adapter = adapter
|
||||
|
||||
binder.browserTabsCloseTabs.setOnClickListener { viewModel.closeAllTabs() }
|
||||
binder.browserTabsAddTab.setOnClickListener { viewModel.addTab() }
|
||||
binder.browserTabsDone.setOnClickListener { viewModel.done() }
|
||||
}
|
||||
|
||||
override fun inject() {
|
||||
FeatureUtils.getFeature<DAppFeatureComponent>(this, DAppFeatureApi::class.java)
|
||||
.browserTabsComponentFactory()
|
||||
.create(this)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun subscribe(viewModel: BrowserTabsViewModel) {
|
||||
setupCloseAllDappTabsDialogue(viewModel.closeAllTabsConfirmation)
|
||||
|
||||
viewModel.tabsFlow.observe {
|
||||
adapter.submitList(it)
|
||||
binder.browserTabsList.scrollToPosition(it.size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun tabClicked(item: BrowserTabRvItem, view: View) {
|
||||
view.transitionName = DAPP_SHARED_ELEMENT_ID_IMAGE_TAB
|
||||
|
||||
val extras = FragmentNavigatorExtras(
|
||||
view to DAPP_SHARED_ELEMENT_ID_IMAGE_TAB
|
||||
)
|
||||
viewModel.openTab(item, extras)
|
||||
}
|
||||
|
||||
override fun tabCloseClicked(item: BrowserTabRvItem) {
|
||||
viewModel.closeTab(item.tabId)
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package io.novafoundation.nova.feature_dapp_impl.presentation.tab
|
||||
|
||||
import androidx.navigation.fragment.FragmentNavigator
|
||||
import io.novafoundation.nova.common.base.BaseViewModel
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.ActionAwaitableMixin
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.awaitAction
|
||||
import io.novafoundation.nova.common.mixin.actionAwaitable.confirmingAction
|
||||
import io.novafoundation.nova.common.utils.Urls
|
||||
import io.novafoundation.nova.common.utils.images.asFileIcon
|
||||
import io.novafoundation.nova.common.utils.images.asUrlIcon
|
||||
import io.novafoundation.nova.common.utils.mapList
|
||||
import io.novafoundation.nova.feature_account_api.domain.interfaces.SelectedAccountUseCase
|
||||
import io.novafoundation.nova.feature_dapp_impl.presentation.DAppRouter
|
||||
import io.novafoundation.nova.feature_dapp_api.presentation.browser.main.DAppBrowserPayload
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.BrowserTabService
|
||||
import io.novafoundation.nova.feature_dapp_impl.utils.tabs.models.BrowserTab
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class BrowserTabsViewModel(
|
||||
private val router: DAppRouter,
|
||||
private val browserTabService: BrowserTabService,
|
||||
private val actionAwaitableMixinFactory: ActionAwaitableMixin.Factory,
|
||||
private val accountUseCase: SelectedAccountUseCase
|
||||
) : BaseViewModel() {
|
||||
|
||||
val closeAllTabsConfirmation = actionAwaitableMixinFactory.confirmingAction<Unit>()
|
||||
|
||||
val tabsFlow = browserTabService.tabStateFlow
|
||||
.map { it.tabs }
|
||||
.mapList {
|
||||
mapBrowserTab(it)
|
||||
}.shareInBackground()
|
||||
|
||||
private fun mapBrowserTab(it: BrowserTab) = BrowserTabRvItem(
|
||||
tabId = it.id,
|
||||
tabName = it.pageSnapshot.pageName ?: Urls.domainOf(it.currentUrl),
|
||||
icon = it.knownDAppMetadata?.iconLink?.asUrlIcon() ?: it.pageSnapshot.pageIconPath?.asFileIcon(),
|
||||
tabScreenshotPath = it.pageSnapshot.pagePicturePath
|
||||
)
|
||||
|
||||
fun openTab(tab: BrowserTabRvItem, extras: FragmentNavigator.Extras) = launch {
|
||||
router.openDAppBrowser(DAppBrowserPayload.Tab(tab.tabId), extras)
|
||||
}
|
||||
|
||||
fun closeTab(tabId: String) = launch {
|
||||
browserTabService.removeTab(tabId)
|
||||
}
|
||||
|
||||
fun closeAllTabs() = launch {
|
||||
closeAllTabsConfirmation.awaitAction()
|
||||
|
||||
val metaAccount = accountUseCase.getSelectedMetaAccount()
|
||||
browserTabService.removeTabsForMetaAccount(metaAccount.id)
|
||||
router.closeTabsScreen()
|
||||
}
|
||||
|
||||
fun addTab() {
|
||||
router.openDappSearch()
|
||||
}
|
||||
|
||||
fun done() {
|
||||
router.closeTabsScreen()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user