Initial commit: Pezkuwi Wallet Android

Security hardened release:
- Code obfuscation enabled (minifyEnabled=true, shrinkResources=true)
- Sensitive files excluded (google-services.json, keystores)
- Branch.io key moved to BuildConfig placeholder
- Updated dependencies: OkHttp 4.12.0, Gson 2.10.1, BouncyCastle 1.77
- Comprehensive ProGuard rules for crypto wallet
- Navigation 2.7.7, Lifecycle 2.7.0, ConstraintLayout 2.1.4
This commit is contained in:
2026-02-12 05:19:41 +03:00
commit a294aa1a6b
7687 changed files with 441811 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+32
View File
@@ -0,0 +1,32 @@
apply plugin: 'kotlin-parcelize'
android {
namespace 'io.novafoundation.nova.feature_banners_impl'
defaultConfig {
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation coroutinesDep
implementation project(":common")
implementation project(':feature-banners-api')
implementation cardViewDep
implementation recyclerViewDep
implementation materialDep
implementation androidDep
implementation daggerDep
ksp daggerCompiler
implementation androidDep
testImplementation project(':test-shared')
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_banners_impl.data
class BannerResponse(
val id: String,
val background: String,
val image: String,
val clipsToBounds: Boolean,
val action: String?
)
class BannerLocalisationResponse(
val title: String,
val details: String
)
@@ -0,0 +1,20 @@
package io.novafoundation.nova.feature_banners_impl.data
import io.novafoundation.nova.core.model.Language
import retrofit2.http.GET
import retrofit2.http.Url
interface BannersApi {
companion object {
fun getLocalisationLink(url: String, language: Language): String {
return "$url/${language.iso639Code}.json"
}
}
@GET
suspend fun getBanners(@Url url: String): List<BannerResponse>
@GET
suspend fun getBannersLocalisation(@Url url: String): Map<String, BannerLocalisationResponse>
}
@@ -0,0 +1,82 @@
package io.novafoundation.nova.feature_banners_impl.data
import retrofit2.HttpException
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.resources.LanguagesHolder
import io.novafoundation.nova.common.utils.scopeAsync
import io.novafoundation.nova.feature_banners_api.domain.PromotionBanner
import io.novafoundation.nova.core.model.Language
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface BannersRepository {
suspend fun getBanners(url: String, localisationUrl: String): List<PromotionBanner>
fun closeBanner(id: String)
fun observeClosedBannerIds(): Flow<Set<String>>
}
class RealBannersRepository(
private val preferences: Preferences,
private val bannersApi: BannersApi,
private val languagesHolder: LanguagesHolder
) : BannersRepository {
companion object {
private const val PREFS_CLOSED_BANNERS = "closed_banners"
}
override suspend fun getBanners(url: String, localisationUrl: String): List<PromotionBanner> {
val language = preferences.getCurrentLanguage()!!
val bannersDeferred = scopeAsync { bannersApi.getBanners(url) }
val localisationDeferred = scopeAsync { getLocalisation(localisationUrl, language) }
val banners = bannersDeferred.await()
val localisation = localisationDeferred.await()
return mapBanners(banners, localisation)
}
override fun observeClosedBannerIds(): Flow<Set<String>> {
return preferences.stringSetFlow(PREFS_CLOSED_BANNERS)
.map { it.orEmpty() }
}
private fun mapBanners(
banners: List<BannerResponse>,
localisation: Map<String, BannerLocalisationResponse>
) = banners.mapNotNull {
val localisationBanner = localisation[it.id] ?: return@mapNotNull null
PromotionBanner(
id = it.id,
title = localisationBanner.title,
details = localisationBanner.details,
backgroundUrl = it.background,
imageUrl = it.image,
clipToBounds = it.clipsToBounds,
actionLink = it.action
)
}
private suspend fun getLocalisation(url: String, language: Language): Map<String, BannerLocalisationResponse> {
try {
val localisationUrl = BannersApi.getLocalisationLink(url, language)
return bannersApi.getBannersLocalisation(localisationUrl)
} catch (e: HttpException) {
val fallbackLanguage = languagesHolder.getDefaultLanguage()
if (e.code() == 404 && language != fallbackLanguage) {
val fallbackUrl = BannersApi.getLocalisationLink(url, fallbackLanguage)
return bannersApi.getBannersLocalisation(fallbackUrl)
}
throw e
}
}
override fun closeBanner(id: String) {
val closedBannersId = preferences.getStringSet(PREFS_CLOSED_BANNERS)
preferences.putStringSet(PREFS_CLOSED_BANNERS, closedBannersId + id)
}
}
@@ -0,0 +1,31 @@
package io.novafoundation.nova.feature_banners_impl.di
import dagger.Component
import io.novafoundation.nova.common.di.CommonApi
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.feature_banners_api.di.BannersFeatureApi
@Component(
dependencies = [
BannersFeatureDependencies::class,
],
modules = [
BannersFeatureModule::class
]
)
@FeatureScope
interface BannersFeatureComponent : BannersFeatureApi {
@Component.Factory
interface Factory {
fun create(deps: BannersFeatureDependencies): BannersFeatureComponent
}
@Component(
dependencies = [
CommonApi::class
]
)
interface BannersFeatureDependenciesComponent : BannersFeatureDependencies
}
@@ -0,0 +1,23 @@
package io.novafoundation.nova.feature_banners_impl.di
import android.content.Context
import coil.ImageLoader
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.resources.LanguagesHolder
import io.novafoundation.nova.common.utils.sequrity.AutomaticInteractionGate
interface BannersFeatureDependencies {
val imageLoader: ImageLoader
val context: Context
val preferences: Preferences
val languagesHolder: LanguagesHolder
val networkApiCreator: NetworkApiCreator
val automaticInteractionGate: AutomaticInteractionGate
}
@@ -0,0 +1,22 @@
package io.novafoundation.nova.feature_banners_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 javax.inject.Inject
@ApplicationScope
class BannersFeatureHolder @Inject constructor(
featureContainer: FeatureContainer
) : FeatureApiHolder(featureContainer) {
override fun initializeDependencies(): Any {
val accountFeatureDependencies = DaggerBannersFeatureComponent_BannersFeatureDependenciesComponent.builder()
.commonApi(commonApi())
.build()
return DaggerBannersFeatureComponent.factory()
.create(deps = accountFeatureDependencies)
}
}
@@ -0,0 +1,63 @@
package io.novafoundation.nova.feature_banners_impl.di
import android.content.Context
import coil.ImageLoader
import dagger.Module
import dagger.Provides
import io.novafoundation.nova.common.data.network.NetworkApiCreator
import io.novafoundation.nova.common.data.storage.Preferences
import io.novafoundation.nova.common.di.scope.FeatureScope
import io.novafoundation.nova.common.resources.LanguagesHolder
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_impl.data.BannersApi
import io.novafoundation.nova.feature_banners_impl.data.BannersRepository
import io.novafoundation.nova.feature_banners_impl.data.RealBannersRepository
import io.novafoundation.nova.feature_banners_impl.domain.PromotionBannersInteractor
import io.novafoundation.nova.feature_banners_impl.domain.RealPromotionBannersInteractor
import io.novafoundation.nova.feature_banners_impl.presentation.banner.RealPromotionBannersMixinFactory
import io.novafoundation.nova.feature_banners_impl.presentation.banner.source.RealBannersSourceFactory
@Module()
class BannersFeatureModule {
@Provides
@FeatureScope
fun provideBannersApi(networkApiCreator: NetworkApiCreator): BannersApi {
return networkApiCreator.create(BannersApi::class.java)
}
@Provides
@FeatureScope
fun provideBannersRepository(
preferences: Preferences,
bannersApi: BannersApi,
languagesHolder: LanguagesHolder
): BannersRepository {
return RealBannersRepository(preferences, bannersApi, languagesHolder)
}
@Provides
@FeatureScope
fun provideBannersInteractor(
repository: BannersRepository
): PromotionBannersInteractor {
return RealPromotionBannersInteractor(repository)
}
@Provides
@FeatureScope
fun sourceFactory(promotionBannersInteractor: PromotionBannersInteractor): BannersSourceFactory {
return RealBannersSourceFactory(promotionBannersInteractor)
}
@Provides
@FeatureScope
fun providePromotionBannersMixinFactory(
promotionBannersInteractor: PromotionBannersInteractor,
imageLoader: ImageLoader,
context: Context
): PromotionBannersMixinFactory {
return RealPromotionBannersMixinFactory(imageLoader, context, promotionBannersInteractor)
}
}
@@ -0,0 +1,30 @@
package io.novafoundation.nova.feature_banners_impl.domain
import io.novafoundation.nova.feature_banners_api.domain.PromotionBanner
import io.novafoundation.nova.feature_banners_impl.data.BannersRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
interface PromotionBannersInteractor {
suspend fun observeBanners(url: String, localisationUrl: String): Flow<List<PromotionBanner>>
fun closeBanner(id: String)
}
class RealPromotionBannersInteractor(
private val bannersRepository: BannersRepository,
) : PromotionBannersInteractor {
override suspend fun observeBanners(url: String, localisationUrl: String): Flow<List<PromotionBanner>> {
val banners = bannersRepository.getBanners(url, localisationUrl)
return bannersRepository.observeClosedBannerIds()
.map { closedIds ->
banners.filter { it.id !in closedIds }
}
}
override fun closeBanner(id: String) {
bannersRepository.closeBanner(id)
}
}
@@ -0,0 +1,96 @@
package io.novafoundation.nova.feature_banners_impl.presentation.banner
import android.content.Context
import android.graphics.drawable.Drawable
import coil.ImageLoader
import coil.request.ImageRequest
import io.novafoundation.nova.common.utils.launchDeepLink
import io.novafoundation.nova.feature_banners_api.domain.PromotionBanner
import io.novafoundation.nova.common.utils.scopeAsync
import io.novafoundation.nova.common.utils.shareInBackground
import io.novafoundation.nova.common.utils.withSafeLoading
import io.novafoundation.nova.feature_banners_api.presentation.BannerPageModel
import io.novafoundation.nova.feature_banners_api.presentation.ClipableImage
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixin
import io.novafoundation.nova.feature_banners_api.presentation.PromotionBannersMixinFactory
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSource
import io.novafoundation.nova.feature_banners_impl.domain.PromotionBannersInteractor
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.map
class RealPromotionBannersMixinFactory(
private val imageLoader: ImageLoader,
private val context: Context,
private val promotionBannersInteractor: PromotionBannersInteractor
) : PromotionBannersMixinFactory {
override fun create(source: BannersSource, coroutineScope: CoroutineScope): PromotionBannersMixin {
return RealPromotionBannersMixin(
promotionBannersInteractor,
imageLoader,
context,
source,
coroutineScope
)
}
}
class RealPromotionBannersMixin(
private val promotionBannersInteractor: PromotionBannersInteractor,
private val imageLoader: ImageLoader,
private val context: Context,
private val bannersSource: BannersSource,
coroutineScope: CoroutineScope
) : PromotionBannersMixin, CoroutineScope by coroutineScope {
override val bannersFlow = bannersSource.observeBanners()
.map { banners ->
val resources = loadResources(banners)
banners.map { mapBanner(it, resources) }
}.withSafeLoading()
.shareInBackground()
override fun closeBanner(banner: BannerPageModel) {
promotionBannersInteractor.closeBanner(banner.id)
}
override fun startBannerAction(page: BannerPageModel) {
val url = page.actionUrl ?: return
context.launchDeepLink(url)
}
private suspend fun loadResources(banners: List<PromotionBanner>): Map<String, Drawable> {
val imagesSet = buildSet {
addAll(banners.map { it.imageUrl })
addAll(banners.map { it.backgroundUrl })
}
val loadingImagesResult = imagesSet.associateWith {
val imageRequest = ImageRequest.Builder(context)
.data(it)
.build()
scopeAsync { imageLoader.execute(imageRequest) }
}
return loadingImagesResult.mapValues { (_, value) -> value.await().drawable!! }
}
private fun mapBanner(
banner: PromotionBanner,
resources: Map<String, Drawable>
): BannerPageModel {
return BannerPageModel(
id = banner.id,
title = banner.title,
subtitle = banner.details,
image = ClipableImage(
resources.getValue(banner.imageUrl),
banner.clipToBounds
),
background = resources.getValue(banner.backgroundUrl),
actionUrl = banner.actionLink
)
}
}
@@ -0,0 +1,18 @@
package io.novafoundation.nova.feature_banners_impl.presentation.banner.source
import io.novafoundation.nova.common.utils.flowOfAll
import io.novafoundation.nova.feature_banners_api.domain.PromotionBanner
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSource
import io.novafoundation.nova.feature_banners_impl.domain.PromotionBannersInteractor
import kotlinx.coroutines.flow.Flow
class RealBannersSource(
private val bannersUrl: String,
private val localisationUrl: String,
private val bannersInteractor: PromotionBannersInteractor
) : BannersSource {
override fun observeBanners(): Flow<List<PromotionBanner>> {
return flowOfAll { bannersInteractor.observeBanners(bannersUrl, localisationUrl) }
}
}
@@ -0,0 +1,14 @@
package io.novafoundation.nova.feature_banners_impl.presentation.banner.source
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSource
import io.novafoundation.nova.feature_banners_api.presentation.source.BannersSourceFactory
import io.novafoundation.nova.feature_banners_impl.domain.PromotionBannersInteractor
class RealBannersSourceFactory(
private val bannersInteractor: PromotionBannersInteractor
) : BannersSourceFactory {
override fun create(bannersUrl: String, localisationUrl: String): BannersSource {
return RealBannersSource(bannersUrl, localisationUrl, bannersInteractor)
}
}