diff --git a/.cursorrules b/.cursorrules index 139a9e6b0..08ae0d894 100644 --- a/.cursorrules +++ b/.cursorrules @@ -72,8 +72,25 @@ ### Mapper Pattern - Internal mappers: `internal interface FooMapper` + `internal class FooMapperImpl` +- **No DTO suffix on mappers** - Use `InboxSearchMapper`, not `InboxSearchDTOMapper` - Use `with` scope function for cleaner code +### Cache vs Repository Naming +- **In-memory caches are NOT repositories** - Use `InboxInMemoryCache`, not `InboxRepository` +- **Use InMemoryCacheProvider** for in-memory caches, not custom `MutableStateFlow` implementations +- **Repository** = data access with external sources (API, database) +- **Cache** = in-memory storage only + +### DI Module Rules +- **Use method references, not lambdas** - `RefreshCache(manager::refresh)` not `RefreshCache { manager.refresh() }` +- **Don't use @Singleton if state is external** - If cache/state is passed in constructor, no need for @Singleton +- **Keep providers in correct feature modules** - `InboxApiService` belongs in `InboxModule`, not `JointAccountModule` + +### Feature Ownership +- **Methods belong to their feature** - `getInboxMessages` belongs in inbox feature, not joint account +- **Don't mix feature concerns** - Each repository handles only its own feature's API calls +- **Cross-feature dependencies use interfaces** - Features depend on each other via domain interfaces, not implementations + ## Code Quality ### Functions @@ -167,6 +184,10 @@ - [ ] Unit tests for data/domain layers - [ ] Internal classes marked `internal` - [ ] Content descriptions for accessibility +- [ ] In-memory caches use `InMemoryCacheProvider`, not custom StateFlow +- [ ] No DTO suffix on mappers +- [ ] Method references used in DI, not lambdas +- [ ] Methods are in correct feature modules --- diff --git a/app/src/main/kotlin/com/algorand/android/modules/accountdetail/assets/ui/domain/DefaultAccountDetailAccountsItemProcessor.kt b/app/src/main/kotlin/com/algorand/android/modules/accountdetail/assets/ui/domain/DefaultAccountDetailAccountsItemProcessor.kt index 2f95b3284..de5895dac 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/accountdetail/assets/ui/domain/DefaultAccountDetailAccountsItemProcessor.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/accountdetail/assets/ui/domain/DefaultAccountDetailAccountsItemProcessor.kt @@ -29,16 +29,16 @@ import com.algorand.android.ui.common.amount.mapper.AmountRendererTypeMapper import com.algorand.android.utils.formatAsAlgoAmount import com.algorand.android.utils.formatAsAlgoDisplayString import com.algorand.wallet.account.detail.domain.model.AccountType -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequest +import com.algorand.wallet.inbox.asset.domain.usecase.GetAssetInboxRequest import com.algorand.wallet.privacy.domain.model.PrivacyMode import com.algorand.wallet.privacy.domain.usecase.GetPrivacyModeFlow import com.algorand.wallet.remoteconfig.domain.model.FeatureToggle import com.algorand.wallet.remoteconfig.domain.usecase.IsFeatureToggleEnabled -import java.math.BigDecimal -import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import java.math.BigDecimal +import javax.inject.Inject internal class DefaultAccountDetailAccountsItemProcessor @Inject constructor( private val getAccountLiteCacheFlow: GetAccountLiteCacheFlow, diff --git a/app/src/main/kotlin/com/algorand/android/modules/accounts/ui/viewmodel/AccountsPreviewUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/accounts/ui/viewmodel/AccountsPreviewUseCase.kt index ce30388f8..063ca7a55 100644 --- a/app/src/main/kotlin/com/algorand/android/modules/accounts/ui/viewmodel/AccountsPreviewUseCase.kt +++ b/app/src/main/kotlin/com/algorand/android/modules/accounts/ui/viewmodel/AccountsPreviewUseCase.kt @@ -24,8 +24,8 @@ import com.algorand.android.modules.accounts.ui.model.AccountPreview import com.algorand.android.modules.parity.domain.model.SelectedCurrencyDetail import com.algorand.android.modules.peraconnectivitymanager.ui.PeraConnectivityManager import com.algorand.android.utils.CacheResult -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequestCountFlow import com.algorand.wallet.banner.domain.usecase.GetBannerFlow +import com.algorand.wallet.inbox.asset.domain.usecase.GetAssetInboxRequestCountFlow import com.algorand.wallet.privacy.domain.usecase.GetPrivacyModeFlow import com.algorand.wallet.spotbanner.domain.model.SpotBannerFlowData import com.algorand.wallet.spotbanner.domain.usecase.GetSpotBannersFlow diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/di/AssetInboxAllAccountsRepositoryModule.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/di/AssetInboxAllAccountsRepositoryModule.kt deleted file mode 100644 index e8e0a55f7..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/di/AssetInboxAllAccountsRepositoryModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.di - -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.mapper.AssetInboxAllAccountsPreviewMapper -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.mapper.AssetInboxAllAccountsPreviewMapperImpl -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -object AssetInboxAllAccountsRepositoryModule { - - @Provides - @Singleton - fun provideAssetInboxAllAccountsPreviewMapper( - assetInboxAllAccountsPreviewMapperImpl: AssetInboxAllAccountsPreviewMapperImpl - ): AssetInboxAllAccountsPreviewMapper = assetInboxAllAccountsPreviewMapperImpl -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/domain/model/AssetInboxAllAccountsWithAccount.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/domain/model/AssetInboxAllAccountsWithAccount.kt deleted file mode 100644 index c4830fae5..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/domain/model/AssetInboxAllAccountsWithAccount.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.domain.model - -import android.os.Parcelable -import com.algorand.android.models.RecyclerListItem -import com.algorand.android.modules.accountcore.ui.model.AccountDisplayName -import com.algorand.android.modules.accounticon.ui.model.AccountIconDrawablePreview -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AssetInboxAllAccountsWithAccount( - val address: String, - val requestCount: Int, - val accountDisplayName: AccountDisplayName, - val accountAddress: String, - val accountIconDrawablePreview: AccountIconDrawablePreview -) : Parcelable, RecyclerListItem { - override fun areItemsTheSame(other: RecyclerListItem): Boolean { - return other is AssetInboxAllAccountsWithAccount && accountAddress == other.accountAddress - } - - override fun areContentsTheSame(other: RecyclerListItem): Boolean { - return other is AssetInboxAllAccountsWithAccount && this == other - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsFragment.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsFragment.kt deleted file mode 100644 index 021125499..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsFragment.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - * - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.viewModels -import com.algorand.android.R -import com.algorand.android.core.transaction.TransactionSignBaseFragment -import com.algorand.android.customviews.toolbar.buttoncontainer.model.IconButton -import com.algorand.android.databinding.FragmentAssetInboxAllAccountsBinding -import com.algorand.android.models.FragmentConfiguration -import com.algorand.android.models.ToolbarConfiguration -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model.AssetInboxAllAccountsPreview -import com.algorand.android.modules.assetinbox.assetinboxoneaccount.ui.model.AssetInboxOneAccountNavArgs -import com.algorand.android.utils.BaseCustomDividerItemDecoration -import com.algorand.android.utils.addCustomDivider -import com.algorand.android.utils.extensions.collectLatestOnLifecycle -import com.algorand.android.utils.extensions.hide -import com.algorand.android.utils.extensions.show -import com.algorand.android.utils.viewbinding.viewBinding -import dagger.hilt.android.AndroidEntryPoint - -@AndroidEntryPoint -class AssetInboxAllAccountsFragment : - TransactionSignBaseFragment(R.layout.fragment_asset_inbox_all_accounts) { - - private val infoButton by lazy { IconButton(R.drawable.ic_info, onClick = ::onInfoButtonClick) } - - private val toolbarConfiguration = ToolbarConfiguration( - titleResId = R.string.asset_transfer_requests, - startIconClick = ::navBack, - startIconResId = R.drawable.ic_left_arrow, - ) - - override val fragmentConfiguration: FragmentConfiguration = - FragmentConfiguration(toolbarConfiguration = toolbarConfiguration) - - private val binding by viewBinding(FragmentAssetInboxAllAccountsBinding::bind) - - private val assetInboxAllAccountsViewModel: AssetInboxAllAccountsViewModel by viewModels() - - private val inboxAccountSelectionListener = object : InboxAccountSelectionAdapter.Listener { - override fun onAccountItemClick(publicKey: String) { - onAccountClicked(publicKey) - } - } - - private val accountAdapter: InboxAccountSelectionAdapter = - InboxAccountSelectionAdapter(inboxAccountSelectionListener) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setupToolbar() - initObservers() - initUi() - assetInboxAllAccountsViewModel.initializePreview() - } - - private fun initObservers() { - collectLatestOnLifecycle( - flow = assetInboxAllAccountsViewModel.viewStateFlow, - collection = viewStateCollector - ) - } - - private val viewStateCollector: suspend (AssetInboxAllAccountsPreview) -> Unit = { preview -> - initPreview(preview) - } - - private fun initPreview(preview: AssetInboxAllAccountsPreview) { - preview.showError?.consume()?.let { error -> - context?.let { showGlobalError(error.parseError(it), tag = baseActivityTag) } - } - if (preview.isLoading) showLoading() else hideLoading() - if (preview.isEmptyStateVisible) showEmptyState() else hideEmptyState() - accountAdapter.submitList(preview.assetInboxAllAccountsWithAccountList) - } - - private fun initUi() { - binding.accountsRecyclerView.apply { - adapter = accountAdapter - addCustomDivider( - drawableResId = R.drawable.horizontal_divider_80_24dp, - showLast = false, - divider = BaseCustomDividerItemDecoration() - ) - } - } - - private fun setupToolbar() { - getAppToolbar()?.run { - setEndButton(button = infoButton) - } - } - - private fun onAccountClicked(publicKey: String) { - navToAssetInboxOneAccountNavigation(AssetInboxOneAccountNavArgs(publicKey)) - } - - private fun onInfoButtonClick() { - navToAssetInboxInfoNavigation() - } - - private fun navToAssetInboxInfoNavigation() { - nav( - AssetInboxAllAccountsFragmentDirections - .actionAssetInboxAllAccountsFragmentToAssetInboxInfoNavigation() - ) - } - - private fun navToAssetInboxOneAccountNavigation(assetInboxOneAccountNavArgs: AssetInboxOneAccountNavArgs) { - nav( - AssetInboxAllAccountsFragmentDirections - .actionAssetInboxAllAccountsFragmentToAssetInboxOneAccountNavigation( - assetInboxOneAccountNavArgs - ) - ) - } - - private fun showLoading() { - binding.progressbar.root.show() - } - - private fun hideLoading() { - binding.progressbar.root.hide() - } - - private fun showEmptyState() { - binding.emptyStateGroup.show() - } - - private fun hideEmptyState() { - binding.emptyStateGroup.hide() - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsViewModel.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsViewModel.kt deleted file mode 100644 index 9dfeec028..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/AssetInboxAllAccountsViewModel.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - * - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model.AssetInboxAllAccountsPreview -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.usecase.AssetInboxAllAccountsPreviewUseCase -import com.algorand.android.utils.launchIO -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import javax.inject.Inject - -@HiltViewModel -class AssetInboxAllAccountsViewModel @Inject constructor( - private val assetInboxAllAccountsPreviewUseCase: AssetInboxAllAccountsPreviewUseCase -) : ViewModel() { - - private val _viewStateFlow = MutableStateFlow(assetInboxAllAccountsPreviewUseCase.getInitialPreview()) - - val viewStateFlow: StateFlow = _viewStateFlow.asStateFlow() - - fun initializePreview() { - viewModelScope.launchIO { - assetInboxAllAccountsPreviewUseCase.getAssetInboxAllAccountsPreview( - _viewStateFlow.value - ).collectLatest { preview -> - _viewStateFlow.value = preview - } - } - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionAdapter.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionAdapter.kt deleted file mode 100644 index 71f74af94..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionAdapter.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui - -import android.view.ViewGroup -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.algorand.android.models.BaseDiffUtil -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.domain.model.AssetInboxAllAccountsWithAccount - -class InboxAccountSelectionAdapter( - private val listener: Listener -) : ListAdapter(BaseDiffUtil()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): InboxAccountSelectionViewHolder { - return InboxAccountSelectionViewHolder.create(parent).apply { - itemView.setOnClickListener { - if (bindingAdapterPosition != RecyclerView.NO_POSITION) { - listener.onAccountItemClick(getItem(bindingAdapterPosition).accountAddress) - } - } - } - } - - override fun onBindViewHolder(holder: InboxAccountSelectionViewHolder, position: Int) { - holder.bind(getItem(position)) - } - - interface Listener { - fun onAccountItemClick(publicKey: String) {} - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionViewHolder.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionViewHolder.kt deleted file mode 100644 index 4c5065c68..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/InboxAccountSelectionViewHolder.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - * - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.algorand.android.R -import com.algorand.android.databinding.ItemInboxAccountBinding -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.domain.model.AssetInboxAllAccountsWithAccount -import com.algorand.android.utils.AccountIconDrawable - -class InboxAccountSelectionViewHolder( - private val binding: ItemInboxAccountBinding -) : RecyclerView.ViewHolder(binding.root) { - - fun bind(assetInboxAllAccountsWithAccount: AssetInboxAllAccountsWithAccount) { - with(binding) { - with(assetInboxAllAccountsWithAccount) { - accountNameTextView.text = accountDisplayName.primaryDisplayName - incomingAssetCountTextView.text = incomingAssetCountTextView.resources.getQuantityString( - R.plurals.incoming_assets, - requestCount, - requestCount - ) - val accountIconDrawable = AccountIconDrawable.create( - context = accountIconImageView.context, - accountIconDrawablePreview = accountIconDrawablePreview, - sizeResId = R.dimen.spacing_xxxxlarge - ) - accountIconImageView.setImageDrawable(accountIconDrawable) - } - } - } - - companion object { - fun create(parent: ViewGroup): InboxAccountSelectionViewHolder { - val binding = ItemInboxAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return InboxAccountSelectionViewHolder(binding) - } - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapper.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapper.kt deleted file mode 100644 index 0f775a81d..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapper.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.mapper - -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model.AssetInboxAllAccountsPreview -import com.algorand.android.utils.ErrorResource -import com.algorand.android.utils.Event -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest - -interface AssetInboxAllAccountsPreviewMapper { - suspend operator fun invoke( - assetInboxAllAccountsList: List, - addresses: List, - isLoading: Boolean, - isEmptyStateVisible: Boolean, - showError: Event?, - onNavBack: Event?, - ): AssetInboxAllAccountsPreview - - fun getInitialPreview(): AssetInboxAllAccountsPreview -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapperImpl.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapperImpl.kt deleted file mode 100644 index 8d3d221ec..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/mapper/AssetInboxAllAccountsPreviewMapperImpl.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.mapper - -import com.algorand.android.modules.accountcore.ui.usecase.GetAccountDisplayName -import com.algorand.android.modules.accountcore.ui.usecase.GetAccountIconDrawablePreview -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.domain.model.AssetInboxAllAccountsWithAccount -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model.AssetInboxAllAccountsPreview -import com.algorand.android.utils.ErrorResource -import com.algorand.android.utils.Event -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import javax.inject.Inject - -class AssetInboxAllAccountsPreviewMapperImpl @Inject constructor( - private val getAccountDisplayName: GetAccountDisplayName, - private val getAccountIconDrawablePreview: GetAccountIconDrawablePreview, -) : AssetInboxAllAccountsPreviewMapper { - - override suspend fun invoke( - assetInboxAllAccountsList: List, - addresses: List, - isLoading: Boolean, - isEmptyStateVisible: Boolean, - showError: Event?, - onNavBack: Event?, - ): AssetInboxAllAccountsPreview { - return AssetInboxAllAccountsPreview( - isLoading = isLoading, - isEmptyStateVisible = isEmptyStateVisible, - showError = showError, - assetInboxAllAccountsWithAccountList = mapToAssetInboxAllAccountsWithAccount( - assetInboxAllAccountsList, - addresses - ) - ) - } - - override fun getInitialPreview(): AssetInboxAllAccountsPreview { - return AssetInboxAllAccountsPreview( - isLoading = true, - isEmptyStateVisible = false, - showError = null, - assetInboxAllAccountsWithAccountList = emptyList() - ) - } - - private suspend fun mapToAssetInboxAllAccountsWithAccount( - assetInboxAllAccountsList: List, - addresses: List - ): List { - return assetInboxAllAccountsList.mapNotNull { assetInboxAllAccounts -> - if (assetInboxAllAccounts.requestCount <= 0) { - null - } else { - addresses.firstOrNull { it == assetInboxAllAccounts.address }?.let { address -> - AssetInboxAllAccountsWithAccount( - address = assetInboxAllAccounts.address, - requestCount = assetInboxAllAccounts.requestCount, - accountAddress = address, - accountDisplayName = getAccountDisplayName(address), - accountIconDrawablePreview = getAccountIconDrawablePreview(address) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/model/AssetInboxAllAccountsPreview.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/model/AssetInboxAllAccountsPreview.kt deleted file mode 100644 index 0ae1523f5..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/model/AssetInboxAllAccountsPreview.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model - -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.domain.model.AssetInboxAllAccountsWithAccount -import com.algorand.android.utils.ErrorResource -import com.algorand.android.utils.Event - -data class AssetInboxAllAccountsPreview( - val isLoading: Boolean, - val isEmptyStateVisible: Boolean, - val showError: Event?, - val assetInboxAllAccountsWithAccountList: List -) diff --git a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/usecase/AssetInboxAllAccountsPreviewUseCase.kt b/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/usecase/AssetInboxAllAccountsPreviewUseCase.kt deleted file mode 100644 index 0934520fe..000000000 --- a/app/src/main/kotlin/com/algorand/android/modules/assetinbox/assetinboxallaccounts/ui/usecase/AssetInboxAllAccountsPreviewUseCase.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.usecase - -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.mapper.AssetInboxAllAccountsPreviewMapper -import com.algorand.android.modules.assetinbox.assetinboxallaccounts.ui.model.AssetInboxAllAccountsPreview -import com.algorand.android.utils.ErrorResource -import com.algorand.android.utils.Event -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequests -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxValidAddresses -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import javax.inject.Inject - -class AssetInboxAllAccountsPreviewUseCase @Inject constructor( - private val getAssetInboxRequests: GetAssetInboxRequests, - private val assetInboxAllAccountsPreviewMapper: AssetInboxAllAccountsPreviewMapper, - private val getAssetInboxValidAddresses: GetAssetInboxValidAddresses -) { - - fun getInitialPreview(): AssetInboxAllAccountsPreview { - return assetInboxAllAccountsPreviewMapper.getInitialPreview() - } - - fun getAssetInboxAllAccountsPreview( - preview: AssetInboxAllAccountsPreview - ): Flow = flow { - val accountAddresses = getAssetInboxValidAddresses() - if (accountAddresses.isEmpty()) { - emit(createAssetInboxAllAccountsPreview(emptyList(), accountAddresses)) - return@flow - } - getAssetInboxRequests(accountAddresses).use( - onSuccess = { - emit(createAssetInboxAllAccountsPreview(it, accountAddresses)) - }, - onFailed = { exception, _ -> - val errorEvent = Event(ErrorResource.Api(exception.message.orEmpty())) - val newPreview = preview.copy(isLoading = false, showError = errorEvent) - emit(newPreview) - } - ) - } - - private suspend fun createAssetInboxAllAccountsPreview( - assetInboxAllAccountsList: List, - addresses: List, - ): AssetInboxAllAccountsPreview { - return assetInboxAllAccountsPreviewMapper.invoke( - assetInboxAllAccountsList, - addresses, - isEmptyStateVisible = assetInboxAllAccountsList.none { it.requestCount > 0 }, - isLoading = false, - showError = null, - onNavBack = null - ) - } -} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapperImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapperImpl.kt deleted file mode 100644 index ef6aee199..000000000 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapperImpl.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.wallet.asset.assetinbox.data.mapper - -import com.algorand.wallet.asset.assetinbox.data.model.AssetInboxRequestsResponse -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import javax.inject.Inject - -internal class AssetInboxRequestMapperImpl @Inject constructor() : AssetInboxRequestMapper { - - override fun invoke(response: AssetInboxRequestsResponse): List { - return emptyList() - TODO("Implement the mapper ") -// return response.assetInboxRequests?.mapNotNull { requestResponse -> -// AssetInboxRequest( -// address = requestResponse.address ?: return@mapNotNull null, -// requestCount = requestResponse.requestCount ?: 0 -// ) -// }.orEmpty() - } -} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/repository/AssetInboxRepositoryImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/repository/AssetInboxRepositoryImpl.kt deleted file mode 100644 index f1874fb45..000000000 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/repository/AssetInboxRepositoryImpl.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.wallet.asset.assetinbox.data.repository - -import com.algorand.wallet.asset.assetinbox.data.mapper.AssetInboxRequestMapper -import com.algorand.wallet.asset.assetinbox.data.service.AssetInboxApiService -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import com.algorand.wallet.asset.assetinbox.domain.repository.AssetInboxRepository -import com.algorand.wallet.foundation.PeraResult -import com.algorand.wallet.foundation.cache.InMemoryLocalCache -import com.algorand.wallet.foundation.network.exceptions.PeraRetrofitErrorHandler -import com.algorand.wallet.foundation.network.utils.requestWithPeraApiErrorHandler -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -internal class AssetInboxRepositoryImpl( - private val assetInboxApiService: AssetInboxApiService, - private val retrofitErrorHandler: PeraRetrofitErrorHandler, - private val assetInboxRequestMapper: AssetInboxRequestMapper, - private val inMemoryLocalCache: InMemoryLocalCache -) : AssetInboxRepository { - - override suspend fun getRequests(addresses: List): PeraResult> { - return requestWithPeraApiErrorHandler(retrofitErrorHandler) { - assetInboxApiService.getAssetInboxAllAccountsRequests(addresses.joinToString(",")) - }.map { response -> - assetInboxRequestMapper(response) - } - } - - override suspend fun cacheRequests(requests: List) { - val cacheData = requests.map { it.address to it } - inMemoryLocalCache.putAll(cacheData) - } - - override fun getRequestCountFlow(): Flow { - return inMemoryLocalCache.getCacheFlow().map { cacheMap -> - cacheMap.values.sumOf { request -> - request.requestCount - } - } - } - - override suspend fun clearCache() { - inMemoryLocalCache.clear() - } - - override suspend fun getRequest(address: String): AssetInboxRequest? = inMemoryLocalCache[address] -} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/di/AssetInboxModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/di/AssetInboxModule.kt deleted file mode 100644 index 494fd0cd8..000000000 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/di/AssetInboxModule.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.wallet.asset.assetinbox.di - -import com.algorand.wallet.asset.assetinbox.data.mapper.AssetInboxRequestMapper -import com.algorand.wallet.asset.assetinbox.data.mapper.AssetInboxRequestMapperImpl -import com.algorand.wallet.asset.assetinbox.data.repository.AssetInboxRepositoryImpl -import com.algorand.wallet.asset.assetinbox.data.service.AssetInboxApiService -import com.algorand.wallet.asset.assetinbox.domain.AssetInboxCacheManager -import com.algorand.wallet.asset.assetinbox.domain.AssetInboxCacheManagerImpl -import com.algorand.wallet.asset.assetinbox.domain.repository.AssetInboxRepository -import com.algorand.wallet.asset.assetinbox.domain.usecase.CacheAssetInboxRequests -import com.algorand.wallet.asset.assetinbox.domain.usecase.ClearAssetInboxCache -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequest -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequestCountFlow -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequests -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxValidAddresses -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxValidAddressesUseCase -import com.algorand.wallet.foundation.cache.InMemoryLocalCache -import com.algorand.wallet.foundation.network.exceptions.PeraRetrofitErrorHandler -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import retrofit2.Retrofit -import javax.inject.Named -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object AssetInboxModule { - - @Provides - fun provideAssetInboxRequestMapper(impl: AssetInboxRequestMapperImpl): AssetInboxRequestMapper = impl - - @Provides - @Singleton - fun provideAssetInboxCacheManager(impl: AssetInboxCacheManagerImpl): AssetInboxCacheManager = impl - - @Provides - @Singleton - fun provideAssetInboxAllAccountsApiService( - @Named("mobileAlgorandRetrofitInterface") retrofit: Retrofit - ): AssetInboxApiService { - return retrofit.create(AssetInboxApiService::class.java) - } - - @Provides - @Singleton - fun provideAssetInboxRepository( - assetInboxApiService: AssetInboxApiService, - retrofitErrorHandler: PeraRetrofitErrorHandler, - assetInboxRequestMapper: AssetInboxRequestMapper, - ): AssetInboxRepository { - return AssetInboxRepositoryImpl( - assetInboxApiService, - retrofitErrorHandler, - assetInboxRequestMapper, - InMemoryLocalCache() - ) - } - - @Provides - fun provideGetAssetInboxRequests(repository: AssetInboxRepository): GetAssetInboxRequests { - return GetAssetInboxRequests(repository::getRequests) - } - - @Provides - fun provideCacheAssetInboxRequests(repository: AssetInboxRepository): CacheAssetInboxRequests { - return CacheAssetInboxRequests(repository::cacheRequests) - } - - @Provides - fun provideClearAssetInboxCache(repository: AssetInboxRepository): ClearAssetInboxCache { - return ClearAssetInboxCache(repository::clearCache) - } - - @Provides - fun provideGetAssetInboxRequestCountFlow(repository: AssetInboxRepository): GetAssetInboxRequestCountFlow { - return GetAssetInboxRequestCountFlow(repository::getRequestCountFlow) - } - - @Provides - fun provideGetAssetInboxRequest(repository: AssetInboxRepository): GetAssetInboxRequest { - return GetAssetInboxRequest(repository::getRequest) - } - - @Provides - fun provideGetAssetInboxValidAddresses( - useCase: GetAssetInboxValidAddressesUseCase - ): GetAssetInboxValidAddresses = useCase -} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt index fece49c8c..87ea0d543 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/ClearPreviousSessionCacheUseCase.kt @@ -13,8 +13,8 @@ package com.algorand.wallet.cache.domain.usecase import com.algorand.wallet.account.info.domain.usecase.ClearAccountInformationCache -import com.algorand.wallet.asset.assetinbox.domain.usecase.ClearAssetInboxCache import com.algorand.wallet.asset.domain.usecase.ClearAssetCache +import com.algorand.wallet.inbox.domain.usecase.ClearInboxCache import com.algorand.wallet.nameservice.domain.usecase.ClearNameServiceCache import javax.inject.Inject @@ -22,13 +22,13 @@ internal class ClearPreviousSessionCacheUseCase @Inject constructor( private val clearAccountInformationCache: ClearAccountInformationCache, private val clearAssetCache: ClearAssetCache, private val clearNameServiceCache: ClearNameServiceCache, - private val clearAssetInboxCache: ClearAssetInboxCache + private val clearInboxCache: ClearInboxCache ) : ClearPreviousSessionCache { override suspend fun invoke() { clearAccountInformationCache() clearAssetCache() clearNameServiceCache() - clearAssetInboxCache() + clearInboxCache() } } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/InitializeAppCacheImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/InitializeAppCacheImpl.kt index 8ba5c9845..014335ced 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/InitializeAppCacheImpl.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/cache/domain/usecase/InitializeAppCacheImpl.kt @@ -14,8 +14,8 @@ package com.algorand.wallet.cache.domain.usecase import androidx.lifecycle.Lifecycle import com.algorand.wallet.account.info.domain.manager.AccountCacheManager -import com.algorand.wallet.asset.assetinbox.domain.AssetInboxCacheManager import com.algorand.wallet.asset.manager.AlgoAssetDetailCacheManager +import com.algorand.wallet.inbox.domain.InboxCacheManager import com.algorand.wallet.nameservice.domain.manager.LocalAccountsNameServiceManager import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -24,7 +24,7 @@ import javax.inject.Inject internal class InitializeAppCacheImpl @Inject constructor( private val accountCacheManager: AccountCacheManager, private val localAccountsNameServiceManager: LocalAccountsNameServiceManager, - private val assetInboxCacheManager: AssetInboxCacheManager, + private val inboxCacheManager: InboxCacheManager, private val clearPreviousSessionCache: ClearPreviousSessionCache, private val algoAssetDetailCacheManager: AlgoAssetDetailCacheManager ) : InitializeAppCache { @@ -35,7 +35,7 @@ internal class InitializeAppCacheImpl @Inject constructor( accountCacheManager.initialize(lifecycle) algoAssetDetailCacheManager.initialize(lifecycle) localAccountsNameServiceManager.initialize(lifecycle) - assetInboxCacheManager.initialize(lifecycle) + inboxCacheManager.initialize(lifecycle) } } } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/cards/domain/usecase/CardUseCases.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/cards/domain/usecase/CardUseCases.kt index fd044e5b2..47ed10a46 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/cards/domain/usecase/CardUseCases.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/cards/domain/usecase/CardUseCases.kt @@ -18,7 +18,3 @@ import com.algorand.wallet.foundation.PeraResult fun interface GetCardFundAddresses { suspend operator fun invoke(): PeraResult> } - -fun interface IsCountryWaitlistedForCards { - suspend operator fun invoke(): PeraResult -} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImpl.kt new file mode 100644 index 000000000..3ee4ad277 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImpl.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.asset.data.repository + +import com.algorand.wallet.foundation.cache.InMemoryCachedObject +import com.algorand.wallet.inbox.asset.domain.model.AssetInboxRequest +import com.algorand.wallet.inbox.asset.domain.repository.AssetInboxRepository +import com.algorand.wallet.inbox.domain.model.InboxMessages +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +internal class AssetInboxRepositoryImpl( + private val inboxCache: InMemoryCachedObject, + private val inboxCacheFlow: Flow +) : AssetInboxRepository { + + override fun getRequestCountFlow(): Flow { + return inboxCacheFlow.map { inboxMessages -> + inboxMessages?.assetInboxes?.sumOf { it.requestCount } ?: 0 + } + } + + override suspend fun getRequest(address: String): AssetInboxRequest? { + val inboxMessages = inboxCache.get() + return inboxMessages?.assetInboxes?.find { it.address == address }?.let { + AssetInboxRequest(address = it.address, requestCount = it.requestCount) + } + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/di/AssetInboxModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/di/AssetInboxModule.kt new file mode 100644 index 000000000..dfd5c38d4 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/di/AssetInboxModule.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.asset.di + +import com.algorand.wallet.foundation.cache.InMemoryCachedObject +import com.algorand.wallet.inbox.asset.data.repository.AssetInboxRepositoryImpl +import com.algorand.wallet.inbox.asset.domain.repository.AssetInboxRepository +import com.algorand.wallet.inbox.asset.domain.usecase.GetAssetInboxRequest +import com.algorand.wallet.inbox.asset.domain.usecase.GetAssetInboxRequestCountFlow +import com.algorand.wallet.inbox.domain.model.InboxMessages +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Named + +@Module +@InstallIn(SingletonComponent::class) +internal object AssetInboxModule { + + @Provides + fun provideAssetInboxRepository( + @Named("inboxCache") inboxCache: InMemoryCachedObject, + @Named("inboxCacheFlow") inboxCacheFlow: MutableStateFlow + ): AssetInboxRepository { + return AssetInboxRepositoryImpl(inboxCache, inboxCacheFlow) + } + + @Provides + fun provideGetAssetInboxRequestCountFlow(repository: AssetInboxRepository): GetAssetInboxRequestCountFlow { + return GetAssetInboxRequestCountFlow(repository::getRequestCountFlow) + } + + @Provides + fun provideGetAssetInboxRequest(repository: AssetInboxRepository): GetAssetInboxRequest { + return GetAssetInboxRequest(repository::getRequest) + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/model/AssetInboxRequest.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/model/AssetInboxRequest.kt similarity index 92% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/model/AssetInboxRequest.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/model/AssetInboxRequest.kt index c134bfce8..b09b9807c 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/model/AssetInboxRequest.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/model/AssetInboxRequest.kt @@ -10,7 +10,7 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain.model +package com.algorand.wallet.inbox.asset.domain.model data class AssetInboxRequest( val address: String, diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/repository/AssetInboxRepository.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/repository/AssetInboxRepository.kt similarity index 67% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/repository/AssetInboxRepository.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/repository/AssetInboxRepository.kt index 3b2c8b99a..beb2da4fd 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/repository/AssetInboxRepository.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/repository/AssetInboxRepository.kt @@ -10,21 +10,14 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain.repository +package com.algorand.wallet.inbox.asset.domain.repository -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import com.algorand.wallet.foundation.PeraResult +import com.algorand.wallet.inbox.asset.domain.model.AssetInboxRequest import kotlinx.coroutines.flow.Flow internal interface AssetInboxRepository { - suspend fun getRequests(addresses: List): PeraResult> - - suspend fun cacheRequests(requests: List) - - suspend fun clearCache() + fun getRequestCountFlow(): Flow suspend fun getRequest(address: String): AssetInboxRequest? - - fun getRequestCountFlow(): Flow } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapper.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/usecase/AssetInboxUseCases.kt similarity index 62% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapper.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/usecase/AssetInboxUseCases.kt index 0a2e85c90..55af64072 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/mapper/AssetInboxRequestMapper.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/asset/domain/usecase/AssetInboxUseCases.kt @@ -10,11 +10,15 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.data.mapper +package com.algorand.wallet.inbox.asset.domain.usecase -import com.algorand.wallet.asset.assetinbox.data.model.AssetInboxRequestsResponse -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest +import com.algorand.wallet.inbox.asset.domain.model.AssetInboxRequest +import kotlinx.coroutines.flow.Flow -internal interface AssetInboxRequestMapper { - operator fun invoke(response: AssetInboxRequestsResponse): List +fun interface GetAssetInboxRequestCountFlow { + operator fun invoke(): Flow +} + +fun interface GetAssetInboxRequest { + suspend operator fun invoke(address: String): AssetInboxRequest? } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/data/repository/InboxApiRepositoryImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/data/repository/InboxApiRepositoryImpl.kt new file mode 100644 index 000000000..d89a54ab7 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/data/repository/InboxApiRepositoryImpl.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.data.repository + +import com.algorand.wallet.foundation.PeraResult +import com.algorand.wallet.foundation.network.exceptions.PeraRetrofitErrorHandler +import com.algorand.wallet.foundation.network.utils.requestWithPeraApiErrorHandler +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.inbox.domain.model.InboxSearchInput +import com.algorand.wallet.inbox.domain.repository.InboxApiRepository +import com.algorand.wallet.inbox.jointaccount.data.mapper.InboxSearchMapper +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchResponse +import com.algorand.wallet.inbox.jointaccount.data.service.InboxApiService +import javax.inject.Inject + +internal class InboxApiRepositoryImpl @Inject constructor( + private val inboxApiService: InboxApiService, + private val inboxSearchMapper: InboxSearchMapper, + private val peraApiErrorHandler: PeraRetrofitErrorHandler +) : InboxApiRepository { + + override suspend fun getInboxMessages( + deviceId: Long, + inboxSearchInput: InboxSearchInput + ): PeraResult { + val request = inboxSearchMapper.mapToInboxSearchRequest(inboxSearchInput) + return requestWithPeraApiErrorHandler(peraApiErrorHandler) { + inboxApiService.getInboxMessages(deviceId, request) + }.mapToInboxMessages() + } + + private fun PeraResult.mapToInboxMessages(): PeraResult { + return when (this) { + is PeraResult.Success -> { + val inboxMessages = inboxSearchMapper.mapToInboxMessages(data) + if (inboxMessages != null) { + PeraResult.Success(inboxMessages) + } else { + PeraResult.Error(Exception("Failed to map inbox messages")) + } + } + is PeraResult.Error -> this + } + } + + override suspend fun deleteJointInvitationNotification( + deviceId: Long, + jointAddress: String + ): PeraResult { + return requestWithPeraApiErrorHandler(peraApiErrorHandler) { + inboxApiService.deleteInboxJointInvitationNotification(deviceId, jointAddress) + } + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/di/InboxModule.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/di/InboxModule.kt new file mode 100644 index 000000000..f8a20f7a3 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/di/InboxModule.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.di + +import com.algorand.wallet.foundation.cache.InMemoryCacheProvider +import com.algorand.wallet.foundation.cache.InMemoryCachedObject +import com.algorand.wallet.inbox.data.repository.InboxApiRepositoryImpl +import com.algorand.wallet.inbox.domain.InboxCacheManager +import com.algorand.wallet.inbox.domain.InboxCacheManagerImpl +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.inbox.domain.repository.InboxApiRepository +import com.algorand.wallet.inbox.domain.usecase.CacheInboxMessages +import com.algorand.wallet.inbox.domain.usecase.ClearInboxCache +import com.algorand.wallet.inbox.domain.usecase.GetInboxMessages +import com.algorand.wallet.inbox.domain.usecase.GetInboxMessagesFlow +import com.algorand.wallet.inbox.domain.usecase.GetInboxValidAddresses +import com.algorand.wallet.inbox.domain.usecase.GetInboxValidAddressesUseCase +import com.algorand.wallet.inbox.domain.usecase.HasInboxItemsForAddress +import com.algorand.wallet.inbox.domain.usecase.HasInboxItemsForAddressUseCase +import com.algorand.wallet.inbox.domain.usecase.RefreshInboxCache +import com.algorand.wallet.inbox.jointaccount.data.mapper.InboxSearchMapper +import com.algorand.wallet.inbox.jointaccount.data.mapper.InboxSearchMapperImpl +import com.algorand.wallet.inbox.jointaccount.data.service.InboxApiService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.flow.MutableStateFlow +import retrofit2.Retrofit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object InboxModule { + + private const val INBOX_CACHE_NAME = "inboxCache" + private const val INBOX_CACHE_FLOW_NAME = "inboxCacheFlow" + + @Provides + @Singleton + fun provideInboxApiService( + @Named("mobileAlgorandRetrofitInterface") retrofit: Retrofit + ): InboxApiService { + return retrofit.create(InboxApiService::class.java) + } + + @Provides + fun provideInboxApiRepository( + repository: InboxApiRepositoryImpl + ): InboxApiRepository = repository + + @Provides + @Singleton + fun provideInboxCacheManager(impl: InboxCacheManagerImpl): InboxCacheManager = impl + + @Provides + @Singleton + @Named(INBOX_CACHE_NAME) + fun provideInboxCache( + inMemoryCacheProvider: InMemoryCacheProvider + ): InMemoryCachedObject = inMemoryCacheProvider.getInMemoryCache() + + @Provides + @Singleton + @Named(INBOX_CACHE_FLOW_NAME) + fun provideInboxCacheFlow(): MutableStateFlow = MutableStateFlow(null) + + @Provides + fun provideInboxSearchMapper(impl: InboxSearchMapperImpl): InboxSearchMapper = impl + + @Provides + fun provideCacheInboxMessages( + @Named(INBOX_CACHE_NAME) cache: InMemoryCachedObject, + @Named(INBOX_CACHE_FLOW_NAME) cacheFlow: MutableStateFlow + ): CacheInboxMessages { + return CacheInboxMessages { inboxMessages -> + cache.put(inboxMessages) + cacheFlow.value = inboxMessages + } + } + + @Provides + fun provideClearInboxCache( + @Named(INBOX_CACHE_NAME) cache: InMemoryCachedObject, + @Named(INBOX_CACHE_FLOW_NAME) cacheFlow: MutableStateFlow + ): ClearInboxCache { + return ClearInboxCache { + cache.clear() + cacheFlow.value = null + } + } + + @Provides + fun provideGetInboxMessagesFlow( + @Named(INBOX_CACHE_FLOW_NAME) cacheFlow: MutableStateFlow + ): GetInboxMessagesFlow { + return GetInboxMessagesFlow { cacheFlow } + } + + @Provides + fun provideGetInboxMessages( + @Named(INBOX_CACHE_NAME) cache: InMemoryCachedObject + ): GetInboxMessages { + return GetInboxMessages { cache.get() } + } + + @Provides + fun provideGetInboxValidAddresses( + useCase: GetInboxValidAddressesUseCase + ): GetInboxValidAddresses = useCase + + @Provides + fun provideHasInboxItemsForAddress( + useCase: HasInboxItemsForAddressUseCase + ): HasInboxItemsForAddress = useCase + + @Provides + fun provideRefreshInboxCache( + inboxCacheManager: InboxCacheManager + ): RefreshInboxCache = RefreshInboxCache(inboxCacheManager::refreshCache) +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManager.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManager.kt similarity index 86% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManager.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManager.kt index 395fc7959..a5ee6b4d3 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManager.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManager.kt @@ -10,10 +10,11 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain +package com.algorand.wallet.inbox.domain import androidx.lifecycle.Lifecycle -internal interface AssetInboxCacheManager { +interface InboxCacheManager { fun initialize(lifecycle: Lifecycle) + suspend fun refreshCache() } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManagerImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManagerImpl.kt similarity index 57% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManagerImpl.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManagerImpl.kt index cac90c368..d92cbd9dc 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/AssetInboxCacheManagerImpl.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/InboxCacheManagerImpl.kt @@ -10,30 +10,33 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain +package com.algorand.wallet.inbox.domain import androidx.lifecycle.Lifecycle import com.algorand.wallet.account.info.domain.model.AccountCacheStatus.INITIALIZED import com.algorand.wallet.account.info.domain.usecase.GetAccountDetailCacheStatusFlow import com.algorand.wallet.account.info.domain.usecase.GetAllAccountInformationFlow -import com.algorand.wallet.asset.assetinbox.domain.usecase.CacheAssetInboxRequests -import com.algorand.wallet.asset.assetinbox.domain.usecase.ClearAssetInboxCache -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxRequests -import com.algorand.wallet.asset.assetinbox.domain.usecase.GetAssetInboxValidAddresses import com.algorand.wallet.cache.LifecycleAwareCacheManager +import com.algorand.wallet.deviceregistration.domain.usecase.GetSelectedNodeDeviceId +import com.algorand.wallet.inbox.domain.model.InboxSearchInput +import com.algorand.wallet.inbox.domain.repository.InboxApiRepository +import com.algorand.wallet.inbox.domain.usecase.CacheInboxMessages +import com.algorand.wallet.inbox.domain.usecase.ClearInboxCache +import com.algorand.wallet.inbox.domain.usecase.GetInboxValidAddresses import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest import javax.inject.Inject -internal class AssetInboxCacheManagerImpl @Inject constructor( +internal class InboxCacheManagerImpl @Inject constructor( private val cacheManager: LifecycleAwareCacheManager, private val getAccountDetailCacheStatusFlow: GetAccountDetailCacheStatusFlow, - private val getAssetInboxRequests: GetAssetInboxRequests, - private val cacheAssetInboxRequests: CacheAssetInboxRequests, - private val clearAssetInboxCache: ClearAssetInboxCache, - private val getAssetInboxValidAddresses: GetAssetInboxValidAddresses, - private val getAllAccountInformationFlow: GetAllAccountInformationFlow -) : AssetInboxCacheManager, LifecycleAwareCacheManager.CacheManagerListener { + private val cacheInboxMessages: CacheInboxMessages, + private val clearInboxCache: ClearInboxCache, + private val getInboxValidAddresses: GetInboxValidAddresses, + private val getAllAccountInformationFlow: GetAllAccountInformationFlow, + private val getSelectedNodeDeviceId: GetSelectedNodeDeviceId, + private val inboxApiRepository: InboxApiRepository +) : InboxCacheManager, LifecycleAwareCacheManager.CacheManagerListener { override suspend fun onInitializeManager(coroutineScope: CoroutineScope) { initialize() @@ -59,20 +62,32 @@ internal class AssetInboxCacheManagerImpl @Inject constructor( private suspend fun runManagerJob() { getAllAccountInformationFlow().collectLatest { - clearAssetInboxCache() - updateAssetInboxCache() + updateInboxCache() } } - private suspend fun updateAssetInboxCache() { - val validAddresses = getAssetInboxValidAddresses() - getAssetInboxRequests(validAddresses).use( - onSuccess = { requests -> - cacheAssetInboxRequests(requests) + private suspend fun updateInboxCache() { + val validAddresses = getInboxValidAddresses() + if (validAddresses.isEmpty()) { + clearInboxCache() + return + } + + val deviceId = getSelectedNodeDeviceId()?.toLongOrNull() ?: run { + return + } + + val inboxSearchInput = InboxSearchInput(addresses = validAddresses) + inboxApiRepository.getInboxMessages(deviceId, inboxSearchInput).use( + onSuccess = { inboxMessages -> + cacheInboxMessages(inboxMessages) }, onFailed = { _, _ -> - clearAssetInboxCache() } ) } + + override suspend fun refreshCache() { + updateInboxCache() + } } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/model/AssetInboxRequestsResponse.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/AssetInbox.kt similarity index 70% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/model/AssetInboxRequestsResponse.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/AssetInbox.kt index 418f3ba61..293ead603 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/model/AssetInboxRequestsResponse.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/AssetInbox.kt @@ -10,11 +10,10 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.data.model +package com.algorand.wallet.inbox.domain.model -import com.google.gson.annotations.SerializedName - -internal data class AssetInboxRequestsResponse( - @SerializedName("results") - val assetInboxRequests: List? +data class AssetInbox( + val address: String, + val inboxAddress: String?, + val requestCount: Int ) diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxMessages.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxMessages.kt new file mode 100644 index 000000000..b71a7e437 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxMessages.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.domain.model + +import com.algorand.wallet.jointaccount.creation.domain.model.JointAccount +import com.algorand.wallet.jointaccount.transaction.domain.model.JointSignRequest + +data class InboxMessages( + val jointAccountImportRequests: List?, + val jointAccountSignRequests: List?, + val assetInboxes: List? +) diff --git a/app/src/main/kotlin/com/algorand/android/ui/webview/bridge/model/PeraWebInterfaceEventResponse.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxSearchInput.kt similarity index 69% rename from app/src/main/kotlin/com/algorand/android/ui/webview/bridge/model/PeraWebInterfaceEventResponse.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxSearchInput.kt index bd105ecb9..714aa1e4c 100644 --- a/app/src/main/kotlin/com/algorand/android/ui/webview/bridge/model/PeraWebInterfaceEventResponse.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/model/InboxSearchInput.kt @@ -10,13 +10,8 @@ * limitations under the License */ -package com.algorand.android.ui.webview.bridge.model +package com.algorand.wallet.inbox.domain.model -import com.google.gson.annotations.SerializedName - -data class PeraWebInterfaceEventResponse( - @SerializedName("action") - val action: String, - @SerializedName("payload") - val payload: String +data class InboxSearchInput( + val addresses: List ) diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/service/AssetInboxApiService.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/repository/InboxApiRepository.kt similarity index 53% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/service/AssetInboxApiService.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/repository/InboxApiRepository.kt index 54227ed71..30f251f21 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/data/service/AssetInboxApiService.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/repository/InboxApiRepository.kt @@ -10,17 +10,21 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.data.service +package com.algorand.wallet.inbox.domain.repository -import com.algorand.wallet.asset.assetinbox.data.model.AssetInboxRequestsResponse -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Query +import com.algorand.wallet.foundation.PeraResult +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.inbox.domain.model.InboxSearchInput -internal interface AssetInboxApiService { +interface InboxApiRepository { - @GET("v1/asa-inboxes/requests/") - suspend fun getAssetInboxAllAccountsRequests( - @Query("addresses") addresses: String - ): Response + suspend fun getInboxMessages( + deviceId: Long, + inboxSearchInput: InboxSearchInput + ): PeraResult + + suspend fun deleteJointInvitationNotification( + deviceId: Long, + jointAddress: String + ): PeraResult } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCase.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/GetInboxValidAddressesUseCase.kt similarity index 86% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCase.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/GetInboxValidAddressesUseCase.kt index e2644cdef..44b9e8804 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCase.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/GetInboxValidAddressesUseCase.kt @@ -10,15 +10,15 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain.usecase +package com.algorand.wallet.inbox.domain.usecase import com.algorand.wallet.account.detail.domain.model.AccountType import com.algorand.wallet.account.detail.domain.usecase.GetAccountsDetails import javax.inject.Inject -internal class GetAssetInboxValidAddressesUseCase @Inject constructor( +internal class GetInboxValidAddressesUseCase @Inject constructor( private val getAccountsDetails: GetAccountsDetails -) : GetAssetInboxValidAddresses { +) : GetInboxValidAddresses { override suspend fun invoke(): List { return getAccountsDetails().mapNotNull { diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCase.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCase.kt new file mode 100644 index 000000000..00813f10b --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCase.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.domain.usecase + +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.remoteconfig.domain.model.FeatureToggle +import com.algorand.wallet.remoteconfig.domain.usecase.IsFeatureToggleEnabled +import javax.inject.Inject + +internal class HasInboxItemsForAddressUseCase @Inject constructor( + private val getInboxMessages: GetInboxMessages, + private val isFeatureToggleEnabled: IsFeatureToggleEnabled +) : HasInboxItemsForAddress { + + override suspend fun invoke(address: String): Boolean { + val inboxMessages = getInboxMessages() ?: return false + val isJointAccountEnabled = isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) + + return if (isJointAccountEnabled) { + hasAssetInboxRequests(inboxMessages, address) || + hasJointAccountInvitations(inboxMessages, address) || + hasSignRequests(inboxMessages, address) + } else { + hasAssetInboxRequests(inboxMessages, address) + } + } + + private fun hasAssetInboxRequests(messages: InboxMessages, address: String): Boolean { + return messages.assetInboxes.orEmpty().any { + it.address == address && it.requestCount > 0 + } + } + + private fun hasJointAccountInvitations(messages: InboxMessages, address: String): Boolean { + return messages.jointAccountImportRequests.orEmpty().any { + it.participantAddresses.orEmpty().contains(address) + } + } + + private fun hasSignRequests(messages: InboxMessages, address: String): Boolean { + return messages.jointAccountSignRequests.orEmpty().any { signRequest -> + signRequest.jointAccount?.let { jointAccount -> + jointAccount.address == address || + jointAccount.participantAddresses.orEmpty().contains(address) + } ?: false + } + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/AssetInboxUseCases.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/InboxUseCases.kt similarity index 50% rename from common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/AssetInboxUseCases.kt rename to common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/InboxUseCases.kt index 31f9d0715..81096bc13 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/AssetInboxUseCases.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/domain/usecase/InboxUseCases.kt @@ -10,32 +10,35 @@ * limitations under the License */ -package com.algorand.wallet.asset.assetinbox.domain.usecase +package com.algorand.wallet.inbox.domain.usecase -import com.algorand.wallet.asset.assetinbox.domain.model.AssetInboxRequest -import com.algorand.wallet.foundation.PeraResult +import com.algorand.wallet.inbox.domain.model.InboxMessages import kotlinx.coroutines.flow.Flow -fun interface GetAssetInboxRequests { - suspend operator fun invoke(addresses: List): PeraResult> +fun interface CacheInboxMessages { + suspend operator fun invoke(inboxMessages: InboxMessages) } -fun interface CacheAssetInboxRequests { - suspend operator fun invoke(requests: List) +fun interface ClearInboxCache { + suspend operator fun invoke() } -fun interface ClearAssetInboxCache { - suspend operator fun invoke() +fun interface GetInboxMessagesFlow { + operator fun invoke(): Flow } -fun interface GetAssetInboxValidAddresses { +fun interface GetInboxMessages { + suspend operator fun invoke(): InboxMessages? +} + +fun interface GetInboxValidAddresses { suspend operator fun invoke(): List } -fun interface GetAssetInboxRequestCountFlow { - suspend operator fun invoke(): Flow +fun interface HasInboxItemsForAddress { + suspend operator fun invoke(address: String): Boolean } -fun interface GetAssetInboxRequest { - suspend operator fun invoke(address: String): AssetInboxRequest? +fun interface RefreshInboxCache { + suspend operator fun invoke() } diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapper.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapper.kt new file mode 100644 index 000000000..daf3fd0b5 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapper.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.jointaccount.data.mapper + +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.inbox.domain.model.InboxSearchInput +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchRequest +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchResponse + +internal interface InboxSearchMapper { + fun mapToInboxSearchRequest(input: InboxSearchInput): InboxSearchRequest + fun mapToInboxMessages(response: InboxSearchResponse?): InboxMessages? +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapperImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapperImpl.kt new file mode 100644 index 000000000..5a94f2e6a --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/mapper/InboxSearchMapperImpl.kt @@ -0,0 +1,60 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.jointaccount.data.mapper + +import com.algorand.wallet.inbox.domain.model.AssetInbox +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.inbox.domain.model.InboxSearchInput +import com.algorand.wallet.inbox.jointaccount.data.model.AssetInboxResponse +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchRequest +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchResponse +import com.algorand.wallet.jointaccount.creation.data.mapper.JointAccountDTOMapper +import com.algorand.wallet.jointaccount.transaction.data.mapper.JointSignRequestMapper +import javax.inject.Inject + +internal class InboxSearchMapperImpl @Inject constructor( + private val jointAccountDTOMapper: JointAccountDTOMapper, + private val jointSignRequestMapper: JointSignRequestMapper +) : InboxSearchMapper { + + override fun mapToInboxSearchRequest(input: InboxSearchInput): InboxSearchRequest { + return InboxSearchRequest( + addresses = input.addresses + ) + } + + override fun mapToInboxMessages(response: InboxSearchResponse?): InboxMessages? { + return response?.let { + InboxMessages( + jointAccountImportRequests = it.jointAccountImportRequests?.mapNotNull { account -> + jointAccountDTOMapper.mapToJointAccountDTO(account) + }, + jointAccountSignRequests = it.jointAccountSignRequests?.mapNotNull { signRequest -> + jointSignRequestMapper.mapToJointSignRequest(signRequest) + }, + assetInboxes = it.asaInboxes?.mapNotNull { assetInbox -> + mapToAssetInbox(assetInbox) + } + ) + } + } + + private fun mapToAssetInbox(response: AssetInboxResponse): AssetInbox? { + val address = response.address ?: return null + return AssetInbox( + address = address, + inboxAddress = response.inboxAddress, + requestCount = response.requestCount ?: 0 + ) + } +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/model/InboxSearchRequest.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/model/InboxSearchRequest.kt new file mode 100644 index 000000000..d231c16ec --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/model/InboxSearchRequest.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.jointaccount.data.model + +import com.algorand.wallet.jointaccount.creation.data.model.JointAccountResponse +import com.algorand.wallet.jointaccount.transaction.data.model.JointSignRequestResponse +import com.google.gson.annotations.SerializedName + +internal data class InboxSearchRequest( + @SerializedName("addresses") + val addresses: List +) + +internal data class InboxSearchResponse( + @SerializedName("joint_account_import_requests") + val jointAccountImportRequests: List?, + @SerializedName("joint_account_sign_requests") + val jointAccountSignRequests: List?, + @SerializedName("asa_inboxes") + val asaInboxes: List? +) + +internal data class AssetInboxResponse( + @SerializedName("address") + val address: String?, + @SerializedName("inbox_address") + val inboxAddress: String?, + @SerializedName("request_count") + val requestCount: Int? +) diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/service/InboxApiService.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/service/InboxApiService.kt new file mode 100644 index 000000000..9463749a0 --- /dev/null +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/inbox/jointaccount/data/service/InboxApiService.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.jointaccount.data.service + +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchRequest +import com.algorand.wallet.inbox.jointaccount.data.model.InboxSearchResponse +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.POST +import retrofit2.http.Path + +internal interface InboxApiService { + + @POST("v1/inbox/{device_id}/") + suspend fun getInboxMessages( + @Path("device_id") deviceId: Long, + @Body inboxSearchRequest: InboxSearchRequest + ): Response + + @DELETE("v1/joint-accounts/inbox/device-import/{device_id}/{multisig_address}/") + suspend fun deleteInboxJointInvitationNotification( + @Path("device_id") deviceId: Long, + @Path("multisig_address") jointAddress: String + ): Response +} diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/jointaccount/data/repository/JointAccountRepositoryImpl.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/jointaccount/data/repository/JointAccountRepositoryImpl.kt index 15931befc..c0dcb95ad 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/jointaccount/data/repository/JointAccountRepositoryImpl.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/jointaccount/data/repository/JointAccountRepositoryImpl.kt @@ -22,10 +22,10 @@ import com.algorand.wallet.jointaccount.creation.domain.model.CreateJointAccount import com.algorand.wallet.jointaccount.creation.domain.model.JointAccount import com.algorand.wallet.jointaccount.data.service.JointAccountApiService import com.algorand.wallet.jointaccount.domain.repository.JointAccountRepository -import com.algorand.wallet.jointaccount.transaction.data.mapper.JointSignRequestMapper +import com.algorand.wallet.jointaccount.transaction.data.mapper.AddSignatureInputMapper import com.algorand.wallet.jointaccount.transaction.data.mapper.CreateSignRequestInputMapper +import com.algorand.wallet.jointaccount.transaction.data.mapper.JointSignRequestMapper import com.algorand.wallet.jointaccount.transaction.data.mapper.SearchSignRequestsInputMapper -import com.algorand.wallet.jointaccount.transaction.data.mapper.AddSignatureInputMapper import com.algorand.wallet.jointaccount.transaction.data.model.JointSignRequestResponse import com.algorand.wallet.jointaccount.transaction.domain.model.AddSignatureInput import com.algorand.wallet.jointaccount.transaction.domain.model.CreateSignRequestInput diff --git a/common-sdk/src/main/kotlin/com/algorand/wallet/spotbanner/domain/usecase/GetSpotBannersFlowUseCase.kt b/common-sdk/src/main/kotlin/com/algorand/wallet/spotbanner/domain/usecase/GetSpotBannersFlowUseCase.kt index 69ab7dd19..0ea2116e7 100644 --- a/common-sdk/src/main/kotlin/com/algorand/wallet/spotbanner/domain/usecase/GetSpotBannersFlowUseCase.kt +++ b/common-sdk/src/main/kotlin/com/algorand/wallet/spotbanner/domain/usecase/GetSpotBannersFlowUseCase.kt @@ -36,9 +36,9 @@ internal class GetSpotBannersFlowUseCase @Inject constructor( private fun isThereAnyNotBackedUpAuthAddressWithBalance(data: List): Boolean { return data.any { - !it.isBackedUp && - it.type?.canSignTransaction() == true && - (it.primaryBalance ?: BigDecimal.ZERO).compareTo(BigDecimal.ZERO) == 1 + it.type?.canSignTransaction() == true && + !it.isBackedUp && + (it.primaryBalance ?: BigDecimal.ZERO).compareTo(BigDecimal.ZERO) == 1 } } } diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCaseTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCaseTest.kt deleted file mode 100644 index 8449a91d3..000000000 --- a/common-sdk/src/test/kotlin/com/algorand/wallet/asset/assetinbox/domain/usecase/GetAssetInboxValidAddressesUseCaseTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2022-2025 Pera Wallet, LDA - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.algorand.wallet.asset.assetinbox.domain.usecase - -import com.algorand.test.peraFixture -import com.algorand.wallet.account.detail.domain.model.AccountDetail -import com.algorand.wallet.account.detail.domain.model.AccountType -import com.algorand.wallet.account.detail.domain.usecase.GetAccountsDetails -import io.mockk.coEvery -import io.mockk.mockk -import kotlinx.coroutines.test.TestResult -import kotlinx.coroutines.test.runTest -import org.junit.Assert.assertEquals -import org.junit.Test - -class GetAssetInboxValidAddressesUseCaseTest { - - private val getAccountsDetails: GetAccountsDetails = mockk() - - private val sut = GetAssetInboxValidAddressesUseCase(getAccountsDetails) - - @Test - fun `EXPECT valid asset inbox addresses`(): TestResult = runTest { - coEvery { getAccountsDetails() } returns ACCOUNT_DETAILS - - val result = sut() - - val expected = listOf("address1", "address2") - assertEquals(expected, result) - } - - private companion object { - val NO_AUTH_ACCOUNT = peraFixture().copy( - accountType = AccountType.NoAuth - ) - val NULL_TYPE_ACCOUNT = peraFixture().copy( - accountType = null - ) - val VALID_ACCOUNT_1 = peraFixture().copy( - address = "address1", - accountType = AccountType.Algo25 - ) - val VALID_ACCOUNT_2 = peraFixture().copy( - address = "address2", - accountType = AccountType.Algo25 - ) - - val ACCOUNT_DETAILS = listOf( - NO_AUTH_ACCOUNT, - NULL_TYPE_ACCOUNT, - VALID_ACCOUNT_1, - VALID_ACCOUNT_2 - ) - } -} diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImplTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImplTest.kt new file mode 100644 index 000000000..fd0d7f30b --- /dev/null +++ b/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/asset/data/repository/AssetInboxRepositoryImplTest.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.asset.data.repository + +import com.algorand.test.test +import com.algorand.wallet.foundation.cache.InMemoryCachedObject +import com.algorand.wallet.inbox.domain.model.AssetInbox +import com.algorand.wallet.inbox.domain.model.InboxMessages +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestResult +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +internal class AssetInboxRepositoryImplTest { + + private val inboxCache: InMemoryCachedObject = mockk(relaxed = true) + private val inboxCacheFlow = MutableStateFlow(null) + + private val assetInboxRepositoryImpl = AssetInboxRepositoryImpl(inboxCache, inboxCacheFlow) + + @Test + fun `EXPECT request count flow to return sum of request counts`(): TestResult = runTest { + inboxCacheFlow.value = InboxMessages( + jointAccountImportRequests = null, + jointAccountSignRequests = null, + assetInboxes = listOf( + AssetInbox(ADDRESS_1, null, 1), + AssetInbox(ADDRESS_2, null, 4) + ) + ) + + val result = assetInboxRepositoryImpl.getRequestCountFlow().test() + + result.assertValue(5) + } + + @Test + fun `EXPECT zero WHEN asset inboxes is null`(): TestResult = runTest { + inboxCacheFlow.value = InboxMessages( + jointAccountImportRequests = null, + jointAccountSignRequests = null, + assetInboxes = null + ) + + val result = assetInboxRepositoryImpl.getRequestCountFlow().test() + + result.assertValue(0) + } + + @Test + fun `EXPECT zero WHEN inbox messages is null`(): TestResult = runTest { + inboxCacheFlow.value = null + + val result = assetInboxRepositoryImpl.getRequestCountFlow().test() + + result.assertValue(0) + } + + @Test + fun `EXPECT null WHEN getRequest is invoked but requested address is not in inbox`(): TestResult = runTest { + every { inboxCache.get() } returns InboxMessages( + jointAccountImportRequests = null, + jointAccountSignRequests = null, + assetInboxes = listOf(AssetInbox(ADDRESS_2, null, 4)) + ) + + val result = assetInboxRepositoryImpl.getRequest(ADDRESS_1) + + assertNull(result) + } + + @Test + fun `EXPECT null WHEN getRequest is invoked but asset inboxes is null`(): TestResult = runTest { + every { inboxCache.get() } returns InboxMessages( + jointAccountImportRequests = null, + jointAccountSignRequests = null, + assetInboxes = null + ) + + val result = assetInboxRepositoryImpl.getRequest(ADDRESS_1) + + assertNull(result) + } + + @Test + fun `EXPECT null WHEN getRequest is invoked but inbox messages is null`(): TestResult = runTest { + every { inboxCache.get() } returns null + + val result = assetInboxRepositoryImpl.getRequest(ADDRESS_1) + + assertNull(result) + } + + @Test + fun `EXPECT request detail WHEN getRequest is invoked and requested address is in inbox`(): TestResult = runTest { + every { inboxCache.get() } returns InboxMessages( + jointAccountImportRequests = null, + jointAccountSignRequests = null, + assetInboxes = listOf( + AssetInbox(ADDRESS_1, null, 1), + AssetInbox(ADDRESS_2, null, 4) + ) + ) + + val result = assetInboxRepositoryImpl.getRequest(ADDRESS_1) + + assertEquals(ADDRESS_1, result?.address) + assertEquals(1, result?.requestCount) + } + + private companion object { + const val ADDRESS_1 = "address1" + const val ADDRESS_2 = "address2" + } +} diff --git a/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCaseTest.kt b/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCaseTest.kt new file mode 100644 index 000000000..bda1ad674 --- /dev/null +++ b/common-sdk/src/test/kotlin/com/algorand/wallet/inbox/domain/usecase/HasInboxItemsForAddressUseCaseTest.kt @@ -0,0 +1,224 @@ +/* + * Copyright 2022-2025 Pera Wallet, LDA + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.algorand.wallet.inbox.domain.usecase + +import com.algorand.wallet.inbox.domain.model.AssetInbox +import com.algorand.wallet.inbox.domain.model.InboxMessages +import com.algorand.wallet.jointaccount.creation.domain.model.JointAccount +import com.algorand.wallet.jointaccount.transaction.domain.model.JointSignRequest +import com.algorand.wallet.remoteconfig.domain.model.FeatureToggle +import com.algorand.wallet.remoteconfig.domain.usecase.IsFeatureToggleEnabled +import io.mockk.coEvery +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class HasInboxItemsForAddressUseCaseTest { + + private val getInboxMessages: GetInboxMessages = mockk() + private val isFeatureToggleEnabled: IsFeatureToggleEnabled = mockk { + every { this@mockk(any()) } returns false + } + + @Test + fun `EXPECT false WHEN inbox messages is null`() = runTest { + coEvery { getInboxMessages() } returns null + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT true WHEN has asset inbox with matching address and count greater than 0`() = runTest { + coEvery { getInboxMessages() } returns createInboxMessages( + assetInboxes = listOf(AssetInbox(address = TEST_ADDRESS, inboxAddress = null, requestCount = 5)) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertTrue(result) + } + + @Test + fun `EXPECT false WHEN has asset inbox with matching address but count is 0`() = runTest { + coEvery { getInboxMessages() } returns createInboxMessages( + assetInboxes = listOf(AssetInbox(address = TEST_ADDRESS, inboxAddress = null, requestCount = 0)) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT false WHEN has asset inbox with different address`() = runTest { + coEvery { getInboxMessages() } returns createInboxMessages( + assetInboxes = listOf(AssetInbox(address = "OTHER", inboxAddress = null, requestCount = 5)) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT false WHEN joint account disabled and has invitation`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns false + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountImportRequests = listOf(createJointAccount(participantAddresses = listOf(TEST_ADDRESS))) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT true WHEN joint account enabled and has invitation with address as participant`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountImportRequests = listOf(createJointAccount(participantAddresses = listOf(TEST_ADDRESS, "OTHER"))) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertTrue(result) + } + + @Test + fun `EXPECT false WHEN joint account enabled and has invitation but address is not participant`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountImportRequests = listOf(createJointAccount(participantAddresses = listOf("OTHER1", "OTHER2"))) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT true WHEN joint account enabled and has sign request where address is joint account`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountSignRequests = listOf( + createSignRequest(jointAccountAddress = TEST_ADDRESS, participantAddresses = listOf("P1", "P2")) + ) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertTrue(result) + } + + @Test + fun `EXPECT true WHEN joint account enabled and has sign request where address is participant`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountSignRequests = listOf( + createSignRequest(jointAccountAddress = "JOINT", participantAddresses = listOf(TEST_ADDRESS, "OTHER")) + ) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertTrue(result) + } + + @Test + fun `EXPECT false WHEN joint account enabled and has sign request but address not related`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + jointAccountSignRequests = listOf( + createSignRequest(jointAccountAddress = "OTHER_JOINT", participantAddresses = listOf("P1", "P2")) + ) + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + @Test + fun `EXPECT false WHEN all lists are empty`() = runTest { + every { isFeatureToggleEnabled(FeatureToggle.JOINT_ACCOUNT.key) } returns true + coEvery { getInboxMessages() } returns createInboxMessages( + assetInboxes = emptyList(), + jointAccountImportRequests = emptyList(), + jointAccountSignRequests = emptyList() + ) + val sut = createUseCase() + + val result = sut(TEST_ADDRESS) + + assertFalse(result) + } + + private fun createUseCase() = HasInboxItemsForAddressUseCase(getInboxMessages, isFeatureToggleEnabled) + + private fun createInboxMessages( + assetInboxes: List? = null, + jointAccountImportRequests: List? = null, + jointAccountSignRequests: List? = null + ) = InboxMessages( + assetInboxes = assetInboxes, + jointAccountImportRequests = jointAccountImportRequests, + jointAccountSignRequests = jointAccountSignRequests + ) + + private fun createJointAccount(participantAddresses: List) = JointAccount( + creationDatetime = null, + address = "JOINT_ADDRESS", + version = 1, + threshold = 2, + participantAddresses = participantAddresses + ) + + private fun createSignRequest( + jointAccountAddress: String, + participantAddresses: List + ) = JointSignRequest( + id = "1", + jointAccount = JointAccount( + creationDatetime = null, + address = jointAccountAddress, + version = 1, + threshold = 2, + participantAddresses = participantAddresses + ), + proposerAddress = "PROPOSER", + type = "transfer", + rawTransactionLists = null, + transactionLists = null, + expectedExpireDatetime = null, + status = null + ) + + private companion object { + const val TEST_ADDRESS = "TEST_ADDRESS" + } +}