From b27da25f5aadd4a7ec7bf6087e141c13e29439ad Mon Sep 17 00:00:00 2001 From: Constantine Kevin <78351447+conxtantyn@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:14:21 +0100 Subject: [PATCH 1/2] [86c6krz1a] (#176) --- README.md | 2 +- .../ads/ui/checkout/CheckoutError.kt | 1 - .../ads/ui/overview/OverviewError.kt | 32 +- app/build.gradle.kts | 8 +- .../app/interactor/NavigationInteractor.kt | 27 +- .../eu/peernetwork/app/ui/content/Content.kt | 4 +- .../app/ui/content/ContentModule.kt | 6 +- .../kotlin/eu/peernetwork/app/ui/feed/Feed.kt | 4 +- .../eu/peernetwork/app/ui/feed/FeedModule.kt | 6 +- .../app/ui/onboarding/OnboardingScreen.kt | 2 +- .../eu/peernetwork/app/ui/profile/Profile.kt | 4 +- .../app/ui/profile/ProfileModule.kt | 8 +- .../app/ui/renderer/BalanceRenderer.kt | 4 +- .../eu/peernetwork/app/ui/search/Search.kt | 4 +- .../peernetwork/app/ui/search/SearchModule.kt | 6 +- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 16 +- .../peernetwork/app/ui/wallet/WalletModule.kt | 32 +- .../app/ui/wallet/WalletNavigation.kt | 29 +- .../peernetwork/app/ui/wallet/WalletPage.kt | 63 ++ .../peernetwork/app/ui/wallet/WalletScreen.kt | 139 ++-- ...eractorUsecase.kt => EngagementUsecase.kt} | 4 +- .../blog/ui/comment/CommentViewModel.kt | 11 +- .../blog/ui/usecase/InteractionUsecase.kt | 6 +- .../ui/design/compose/DesignErrorLabel.kt | 96 --- .../ui/design/compose/DesignPagingScaffold.kt | 87 --- .../compose/DesignRefreshableScaffold.kt | 125 ---- .../core/ui/design/luna/DesignAvatar.kt | 3 +- .../{compose => material}/DesignError.kt | 3 +- .../DesignStatefulScaffold.kt | 2 +- .../media/ui/editor/video/VideoScreen.kt | 4 +- .../media/ui/selector/audio/AudioScreen.kt | 7 +- .../ui/selector/directory/DirectoryScreen.kt | 4 +- .../media/ui/selector/photo/PhotoPage.kt | 4 +- .../media/ui/selector/photo/PhotoScreen.kt | 4 +- .../media/ui/selector/video/VideoScreen.kt | 4 +- .../AuthenticationInteractorDelegate.kt | 3 +- .../wallet/data/api/TransferApi.kt | 7 + .../TransactionRepositoryDelegate.kt | 9 + .../peernetwork/wallet/domain/model/Amount.kt | 8 + .../wallet/domain/model/Category.kt | 13 + .../peernetwork/wallet/domain/model/Fees.kt | 10 + .../peernetwork/wallet/domain/model/Filter.kt | 9 + .../peernetwork/wallet/domain/model/Sort.kt | 6 + .../peernetwork/wallet/domain/model/Status.kt | 7 + .../wallet/domain/model/Transaction.kt | 20 + .../peernetwork/wallet/domain/model/User.kt | 10 + .../repository/TransactionRepository.kt | 11 + .../domain/usecase/TransactionsUsecase.kt | 24 + .../src/main/graphql/wallet.schema.graphqls | 631 ++++++++++++------ .../wallet/remote/transactions.graphql | 60 ++ .../wallet/remote/api/TransferApiDelegate.kt | 38 +- .../wallet/remote/mapper/Amount.kt | 5 + .../wallet/remote/mapper/Category.kt | 18 + .../peernetwork/wallet/remote/mapper/Fee.kt | 14 + .../wallet/remote/mapper/Filter.kt | 17 + .../peernetwork/wallet/remote/mapper/Sort.kt | 11 + .../wallet/remote/mapper/Status.kt | 13 + .../wallet/remote/mapper/Transaction.kt | 31 + .../peernetwork/wallet/remote/mapper/User.kt | 26 + .../wallet/remote/mapper/Wallet.kt | 11 +- .../remote/api/TransferApiDelegateTest.kt | 2 +- .../wallet/ui/balance/BalanceError.kt | 108 +++ .../wallet/ui/balance/BalanceOverview.kt | 64 ++ .../wallet/ui/balance/BalancePreview.kt | 85 +++ .../wallet/ui/balance/BalanceScreen.kt | 67 +- .../wallet/ui/balance/BalanceSkeleton.kt | 76 +++ .../wallet/ui/balance/BalanceViewModel.kt | 17 + .../Service.kt => dashboard/Dashboard.kt} | 14 +- .../DashboardError.kt} | 8 +- .../DashboardModule.kt} | 18 +- .../DashboardPage.kt} | 57 +- .../DashboardScreen.kt} | 60 +- .../DashboardTransfer.kt} | 18 +- .../DashboardViewModel.kt} | 4 +- .../ui/extension/TransactionsNavigator.kt | 12 + .../eu/peernetwork/wallet/ui/mapper/Amount.kt | 12 + .../peernetwork/wallet/ui/mapper/Category.kt | 48 ++ .../eu/peernetwork/wallet/ui/mapper/Fees.kt | 14 + .../eu/peernetwork/wallet/ui/mapper/Status.kt | 12 + .../eu/peernetwork/wallet/ui/mapper/Tax.kt | 8 +- .../wallet/ui/mapper/Transaction.kt | 30 + .../eu/peernetwork/wallet/ui/mapper/User.kt | 15 + .../peernetwork/wallet/ui/model/UiAmount.kt | 8 + .../peernetwork/wallet/ui/model/UiCategory.kt | 13 + .../eu/peernetwork/wallet/ui/model/UiFees.kt | 10 + .../peernetwork/wallet/ui/model/UiStatus.kt | 7 + .../eu/peernetwork/wallet/ui/model/UiTax.kt | 11 +- .../wallet/ui/model/UiTransaction.kt | 17 + .../eu/peernetwork/wallet/ui/model/UiUser.kt | 10 + .../wallet/ui/overview/OverviewError.kt | 69 -- .../wallet/ui/overview/OverviewModule.kt | 38 -- .../wallet/ui/overview/OverviewScaffold.kt | 131 ---- .../wallet/ui/overview/OverviewScreen.kt | 118 ---- .../wallet/ui/overview/OverviewViewModel.kt | 49 -- .../Transactions.kt} | 15 +- .../ui/transactions/TransactionsAvatar.kt | 173 +++++ .../ui/transactions/TransactionsError.kt | 88 +++ .../ui/transactions/TransactionsItem.kt | 246 +++++++ .../ui/transactions/TransactionsList.kt | 132 ++++ .../ui/transactions/TransactionsModule.kt | 27 + .../ui/transactions/TransactionsNavigator.kt | 22 + .../ui/transactions/TransactionsScreen.kt | 73 ++ .../ui/transactions/TransactionsSkeleton.kt | 60 ++ .../ui/transactions/TransactionsSummery.kt | 79 +++ .../ui/transactions/TransactionsUsername.kt | 84 +++ .../ui/transactions/TransactionsViewModel.kt | 73 ++ .../wallet/ui/transfer/TransferSheet.kt | 2 +- .../ui/usecase/TransactionListingUsecase.kt | 81 +++ .../src/main/res/drawable/ic_caret_right.xml | 9 + wallet/ui/src/main/res/drawable/ic_hidden.xml | 9 + wallet/ui/src/main/res/drawable/ic_mint.xml | 9 + wallet/ui/src/main/res/drawable/ic_pin.xml | 9 + wallet/ui/src/main/res/drawable/ic_report.xml | 16 + .../main/res/drawable/ic_transaction_post.xml | 9 + .../res/drawable/ic_transfer_direction.xml | 9 + wallet/ui/src/main/res/values/strings.xml | 29 + .../ui/overview/OverviewViewModelTest.kt | 75 --- .../wallet/ui/service/ServiceViewModelTest.kt | 11 +- 118 files changed, 3042 insertions(+), 1301 deletions(-) create mode 100644 app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt rename blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/{ContentInteractorUsecase.kt => EngagementUsecase.kt} (85%) delete mode 100644 core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignErrorLabel.kt delete mode 100644 core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignPagingScaffold.kt delete mode 100644 core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignRefreshableScaffold.kt rename core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/{compose => material}/DesignError.kt (97%) rename core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/{compose => material}/DesignStatefulScaffold.kt (99%) create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Amount.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Category.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Filter.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Sort.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Status.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Transaction.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/User.kt create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransactionsUsecase.kt create mode 100644 wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Amount.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Category.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Fee.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Filter.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Sort.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Status.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Transaction.kt create mode 100644 wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/User.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceError.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceSkeleton.kt rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/Service.kt => dashboard/Dashboard.kt} (58%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceError.kt => dashboard/DashboardError.kt} (95%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceModule.kt => dashboard/DashboardModule.kt} (77%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServicePage.kt => dashboard/DashboardPage.kt} (83%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceScreen.kt => dashboard/DashboardScreen.kt} (60%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceTransfer.kt => dashboard/DashboardTransfer.kt} (84%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceViewModel.kt => dashboard/DashboardViewModel.kt} (92%) create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/TransactionsNavigator.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Status.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/User.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiAmount.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiCategory.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiFees.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiStatus.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiUser.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewError.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewModule.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScaffold.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScreen.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModel.kt rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{overview/Overview.kt => transactions/Transactions.kt} (50%) create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsError.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsNavigator.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSkeleton.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt create mode 100644 wallet/ui/src/main/res/drawable/ic_caret_right.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_hidden.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_mint.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_pin.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_report.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_transaction_post.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_transfer_direction.xml delete mode 100644 wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModelTest.kt diff --git a/README.md b/README.md index c6966eadc..44b6fdada 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ peer_android_frontend/ The UI acts as a composition of three distinct layers: 1. **Stateful Components (Screens)**: Composition roots (e.g., `LoginScreen`) responsible for Dependency Injection, ViewModel creation, and state collection. 2. **Stateless Components**: Complex UI compositions (e.g., `LoginPage`) that define the layout and behavioral contracts. They are pure functions receiving state and event callbacks. -3. **View Components**: Reusable, atomic building blocks (e.g., `ConnectionButton`, `DesignOutlinedButton`) shared across the application. +3. **View Components**: Reusable, atomic building blocks (e.g., `DesignTextField`, `DesignButton`) shared across the application. ## 🚀 Build & Setup diff --git a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutError.kt b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutError.kt index deb624db6..2837063e1 100644 --- a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutError.kt +++ b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutError.kt @@ -1,6 +1,5 @@ package eu.peernetwork.ads.ui.checkout -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues diff --git a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/overview/OverviewError.kt b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/overview/OverviewError.kt index 7080e31b3..30c8a1cdb 100644 --- a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/overview/OverviewError.kt +++ b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/overview/OverviewError.kt @@ -13,10 +13,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview @@ -31,11 +33,21 @@ fun OverviewError( Column(modifier = Modifier.fillMaxSize() .padding(horizontal = 16.dp) .padding(vertical = 12.dp)) { - Box(modifier = Modifier.padding(bottom = 10.dp) - .fillMaxWidth() - .height(48.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainerLowest)) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.padding(bottom = 10.dp) + .fillMaxWidth() + .height(48.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .padding(horizontal = 20.dp) + ) { + CompositionLocalProvider( + LocalTextStyle provides MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.error + ) + ) { updatedContent() } + } Box(modifier = Modifier.fillMaxWidth() .height(48.dp) .clip(CircleShape) @@ -52,13 +64,7 @@ fun OverviewError( .clip(RoundedCornerShape(24.dp)) .background(MaterialTheme.colorScheme.surfaceContainerLowest) .padding(16.dp) - ) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.error - ) - ) { updatedContent() } - } + ) } } @@ -66,6 +72,6 @@ fun OverviewError( @Composable fun PreviewOverviewError() { DesignTheme(isDarkMode = true) { - OverviewError {} + OverviewError { Text("Error occurred!...") } } } diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7803a29fc..216adaa13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,8 +6,8 @@ plugins { android { ndkVersion = "26.1.10909125" defaultConfig { - versionCode = 38 - versionName = "1.11.0" + versionCode = 39 + versionName = "1.11.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -25,8 +25,8 @@ android { isMinifyEnabled = false applicationIdSuffix = ".debug" versionNameSuffix = "-DEBUG" - buildConfigField("String", "BASE_URL", "\"https://backend.peerapp.eu\"") - buildConfigField("String", "MEDIA_URL", "\"https://media.peerapp.eu\"") + buildConfigField("String", "BASE_URL", "\"https://getpeer.eu\"") + buildConfigField("String", "MEDIA_URL", "\"https://media.getpeer.eu\"") buildConfigField("boolean", "USE_SYSTEM_THEME", "false") buildConfigField("String","PRIVACY_POLICY_URL","\"https://www.freeprivacypolicy.com/live/02865c3a-79db-4baf-9ca1-7d91e2cf1724\"") proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") diff --git a/app/src/main/kotlin/eu/peernetwork/app/interactor/NavigationInteractor.kt b/app/src/main/kotlin/eu/peernetwork/app/interactor/NavigationInteractor.kt index 97fa8c588..2e2ec3c67 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/interactor/NavigationInteractor.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/interactor/NavigationInteractor.kt @@ -7,12 +7,13 @@ import androidx.navigation.NavHostController import eu.peernetwork.ads.ui.article.ArticleNavigator import eu.peernetwork.blog.ui.post.PostNavigator import eu.peernetwork.user.ui.user.UserNavigator +import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator import java.net.URLEncoder class NavigationInteractor( private val context: Context, private val controller: NavHostController -) : PostNavigator, ArticleNavigator, UserNavigator { +) : PostNavigator, ArticleNavigator, UserNavigator, TransactionsNavigator { override fun navigate(route: PostNavigator.Route) { when(route) { is PostNavigator.Route.Profile -> { @@ -84,4 +85,28 @@ class NavigationInteractor( } } } + + override fun navigate(route: TransactionsNavigator.Route) { + when(route) { + is TransactionsNavigator.Route.Profile -> { + controller.navigate("profile/${route.id}") { + launchSingleTop = true + } + } + is TransactionsNavigator.Route.Search -> { + val encoded = URLEncoder.encode(route.query, "UTF-8") + controller.navigate("search/${route.type}/$encoded") { + launchSingleTop = true + } + } + is TransactionsNavigator.Route.Link -> { + val url = if (!route.url.startsWith("http")) { + "https://${route.url}" + } else { + route.url + } + context.startActivity(Intent(Intent.ACTION_VIEW, url.toUri())) + } + } + } } diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt index ab14f497d..759b680e4 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt @@ -14,7 +14,7 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard interface Content : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -36,7 +36,7 @@ interface Content : ApplicationProvider { Confirmation, Screen, Detail, - Service, + Dashboard, Balance, Checkout, Boost diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt index 6f0e0b920..5ef1a521d 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt @@ -19,7 +19,7 @@ import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard @Module object ContentModule { @@ -100,9 +100,9 @@ object ContentModule { @Content.Scope @Provides @IntoMap - @UiBuilder(Service.Builder::class) + @UiBuilder(Dashboard.Builder::class) fun provideServiceBuilder(component: Content.Component): UiComponent.Builder { - return Service.Builder(component) + return Dashboard.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt index fbe7ce436..590945726 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt @@ -15,7 +15,7 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard interface Feed : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -37,7 +37,7 @@ interface Feed : ApplicationProvider { Timeline, Connection, Confirmation, - Service, + Dashboard, Balance, Checkout, Screen { diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt index ed1fa70cf..ff43bb917 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt @@ -24,7 +24,7 @@ import eu.peernetwork.core.ui.factory.UiViewModelFactory import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard import javax.inject.Provider @Module @@ -129,9 +129,9 @@ object FeedModule { @Feed.Scope @Provides @IntoMap - @UiBuilder(Service.Builder::class) + @UiBuilder(Dashboard.Builder::class) fun provideServiceBuilder(component: Feed.Component): UiComponent.Builder { - return Service.Builder(component) + return Dashboard.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/onboarding/OnboardingScreen.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/onboarding/OnboardingScreen.kt index 3a81a0772..8f2f5454d 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/onboarding/OnboardingScreen.kt @@ -19,7 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.app.model.Properties import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignError +import eu.peernetwork.core.ui.design.material.DesignError import eu.peernetwork.core.ui.design.luna.DesignStream import eu.peernetwork.core.ui.design.luna.DesignStreamState import eu.peernetwork.core.ui.extension.builder diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt index f3e15deed..72ae1620a 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt @@ -21,7 +21,7 @@ import eu.peernetwork.social.ui.report.Report import eu.peernetwork.user.ui.user.User import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard as WalletDashboard interface Profile : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -49,7 +49,7 @@ interface Profile : ApplicationProvider { Dashboard, Boost, Balance, - Service, + WalletDashboard, Checkout, Feed, Report, diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt index 3f873683e..3ce43a508 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt @@ -26,7 +26,7 @@ import eu.peernetwork.social.ui.report.Report import eu.peernetwork.user.ui.user.User import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard as WalletDashboard @Module object ProfileModule { @@ -179,9 +179,9 @@ object ProfileModule { @Profile.Scope @Provides @IntoMap - @UiBuilder(Service.Builder::class) - fun provideServiceBuilder(component: Profile.Component): UiComponent.Builder { - return Service.Builder(component) + @UiBuilder(WalletDashboard.Builder::class) + fun provideWalletDashboardBuilder(component: Profile.Component): UiComponent.Builder { + return WalletDashboard.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt index 2b373a222..ba652aa15 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt @@ -37,7 +37,7 @@ import eu.peernetwork.core.ui.design.luna.DesignStreamState import eu.peernetwork.core.ui.theme.PeerAppRed import eu.peernetwork.wallet.ui.R import eu.peernetwork.wallet.ui.balance.BalanceScreen -import eu.peernetwork.wallet.ui.service.ServiceScreen +import eu.peernetwork.wallet.ui.dashboard.DashboardScreen import javax.inject.Inject class BalanceRenderer @Inject constructor( @@ -80,7 +80,7 @@ class BalanceRenderer @Inject constructor( content: @Composable ((State>, () -> Unit) -> Unit) ) { val updatedContent by rememberUpdatedState(content) - ServiceScreen( + DashboardScreen( provider = provider, viewModelStoreOwner = viewModelStoreOwner ) { service, action -> diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt index 1e1ac192d..c4dbcca2f 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt @@ -17,7 +17,7 @@ import eu.peernetwork.social.ui.search.tag.Tag import eu.peernetwork.social.ui.search.title.Title import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard interface Search : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -42,7 +42,7 @@ interface Search : ApplicationProvider { Connection, Confirmation, Boost, - Service, + Dashboard, Balance, Checkout, Screen diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt index 4ef597325..05d15c6e7 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt @@ -23,7 +23,7 @@ import eu.peernetwork.social.ui.search.tag.Tag import eu.peernetwork.social.ui.search.title.Title import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard @Module object SearchModule { @@ -136,9 +136,9 @@ object SearchModule { @Search.Scope @Provides @IntoMap - @UiBuilder(Service.Builder::class) + @UiBuilder(Dashboard.Builder::class) fun provideServiceBuilder(component: Search.Component): UiComponent.Builder { - return Service.Builder(component) + return Dashboard.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt index 24dc93982..3baffb9e3 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt @@ -3,12 +3,14 @@ package eu.peernetwork.app.ui.wallet import android.content.Context import eu.peernetwork.app.provider.ApplicationProvider import eu.peernetwork.app.ui.profile.Profile +import eu.peernetwork.app.ui.search.Search import eu.peernetwork.app.ui.settings.SettingsEvent import eu.peernetwork.core.ui.component.UiComponent import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.search.member.Member -import eu.peernetwork.wallet.ui.overview.Overview -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.balance.Balance +import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer interface Wallet : ApplicationProvider { @@ -23,7 +25,15 @@ interface Wallet : ApplicationProvider { dependencies = [ Wallet::class ], modules = [ WalletModule::class ] ) - interface Component : Wallet, Overview, Transfer, Member, Profile, Service, UiComponentProvider + interface Component : Wallet, + Transfer, + Member, + Profile, + Balance, + Dashboard, + Transactions, + Search, + UiComponentProvider class Builder(private val dependency: Wallet): UiComponent.DefaultBuilder() { override fun build(context: Context): Component { diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt index e640168e2..743bef16b 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt @@ -4,13 +4,15 @@ import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap import eu.peernetwork.app.ui.profile.Profile +import eu.peernetwork.app.ui.search.Search import eu.peernetwork.core.ui.annotation.UiBuilder import eu.peernetwork.core.ui.component.UiComponent import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.social.ui.search.member.Member -import eu.peernetwork.wallet.ui.overview.Overview -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.balance.Balance +import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer import javax.inject.Provider @@ -26,9 +28,9 @@ object WalletModule { @Provides @Wallet.Scope @IntoMap - @UiBuilder(Overview.Builder::class) - fun provideOverviewBuilder(component: Wallet.Component): UiComponent.Builder { - return Overview.Builder(component) + @UiBuilder(Balance.Builder::class) + fun provideBalanceBuilder(component: Wallet.Component): UiComponent.Builder { + return Balance.Builder(component) } @Provides @@ -42,9 +44,9 @@ object WalletModule { @Provides @Wallet.Scope @IntoMap - @UiBuilder(Service.Builder::class) + @UiBuilder(Dashboard.Builder::class) fun provideServiceBuilder(component: Wallet.Component): UiComponent.Builder { - return Service.Builder(component) + return Dashboard.Builder(component) } @Wallet.Scope @@ -62,4 +64,20 @@ object WalletModule { fun provideProfileBuilder(component: Wallet.Component): UiComponent.Builder { return Profile.Builder(component) } + + @Wallet.Scope + @Provides + @IntoMap + @UiBuilder(Transactions.Builder::class) + fun provideTransactionsBuilder(component: Wallet.Component): UiComponent.Builder { + return Transactions.Builder(component) + } + + @Wallet.Scope + @Provides + @IntoMap + @UiBuilder(Search.Builder::class) + fun provideSearchBuilder(component: Wallet.Component): UiComponent.Builder { + return Search.Builder(component) + } } diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt index db9ba0acc..4e0f5b2d6 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt @@ -6,9 +6,11 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument +import eu.peernetwork.app.BuildConfig import eu.peernetwork.app.ui.profile.ProfileScreen +import eu.peernetwork.app.ui.search.SearchMode +import eu.peernetwork.app.ui.search.SearchScreen import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.design.material.DesignRouter import eu.peernetwork.user.domain.model.Account @@ -16,10 +18,10 @@ import eu.peernetwork.user.domain.model.Account @Composable fun WalletNavigation( account: Account, + controller: NavHostController, provider: UiComponentProvider, content: @Composable (NavHostController) -> Unit, ) { - val controller = rememberNavController() val updatedContent by rememberUpdatedState(content) DesignRouter( navController = controller, @@ -39,5 +41,28 @@ fun WalletNavigation( viewModelStoreOwner = backStackEntry, ) } + composable( + "search/{type}/{query}", + arguments = listOf( + navArgument("type") { type = NavType.StringType }, + navArgument("query") { type = NavType.StringType } + ) + ) { backStackEntry -> + val type = backStackEntry.arguments?.getString("type") ?: "" + val query = backStackEntry.arguments?.getString("query") ?: "" + val mode = when (type) { + "username" -> SearchMode.Username + "tag" -> SearchMode.Tag + else -> SearchMode.Default + } + SearchScreen( + account = account, + limit = BuildConfig.PAGING_LIMIT, + provider = provider, + query = query, + viewModelStoreOwner = backStackEntry, + mode = mode, + ) + } } } diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt new file mode 100644 index 000000000..1f2867c39 --- /dev/null +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt @@ -0,0 +1,63 @@ +package eu.peernetwork.app.ui.wallet + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignRefreshScaffold +import eu.peernetwork.core.ui.design.material.DesignScaffold +import eu.peernetwork.wallet.ui.R + +@Composable +fun WalletPage( + onRefresh: () -> Unit, + header: @Composable () -> Unit, + transactions: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + val updatedHeader by rememberUpdatedState(header) + val updatedTransactions by rememberUpdatedState(transactions) + val updatedContent by rememberUpdatedState(content) + val isRefreshing = remember { mutableStateOf(false) } + DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { + DesignScaffold( + alwaysReturn = true, + modifier = Modifier.fillMaxSize() + .padding(horizontal = 18.dp), + header = { + Box( + modifier = Modifier.padding(top = 8.dp) + .padding(bottom = 10.dp) + ) { updatedHeader() } + } + ) { + DesignScaffold( + alwaysReturn = true, + modifier = Modifier.fillMaxSize(), + header = { updatedContent() } + ) { + Column { + Text( + text = stringResource(R.string.transactions_label), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(top = 12.dp) + .padding(horizontal = 8.dp) + .padding(bottom = 10.dp) + ) + updatedTransactions() + } + } + } + } +} diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt index db31685a0..24552ed07 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt @@ -1,41 +1,35 @@ package eu.peernetwork.app.ui.wallet -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner +import androidx.navigation.compose.rememberNavController +import eu.peernetwork.app.interactor.NavigationInteractor import eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignRefreshableScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.material.DesignTitle import eu.peernetwork.core.ui.design.material.DesignTitleBarHost import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.core.ui.extension.navigateIfNecessary import eu.peernetwork.social.ui.search.member.MemberModal import eu.peernetwork.user.domain.model.Account +import eu.peernetwork.wallet.ui.balance.BalanceOverview import eu.peernetwork.wallet.ui.model.UiRecipient -import eu.peernetwork.wallet.ui.overview.OverviewScreen -import eu.peernetwork.wallet.ui.service.ServiceScreen -import eu.peernetwork.wallet.ui.service.ServiceState +import eu.peernetwork.wallet.ui.dashboard.DashboardScreen +import eu.peernetwork.wallet.ui.dashboard.DashboardState import eu.peernetwork.wallet.ui.saveable.UiRecipientSaver +import eu.peernetwork.wallet.ui.transactions.TransactionsList +import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator +import kotlinx.coroutines.launch @Composable fun WalletScreen( @@ -45,6 +39,7 @@ fun WalletScreen( viewModelStoreOwner: ViewModelStoreOwner ) { val context = LocalContext.current + val scope = rememberCoroutineScope() val component = remember { provider.builder(Wallet.Builder::class.java).build(context) } @@ -53,65 +48,65 @@ fun WalletScreen( } val service = remember(recipient.value) { mutableStateOf(recipient.value?.let { - ServiceState.Transfer(it) - } ?: ServiceState.Default) + DashboardState.Transfer(it) + } ?: DashboardState.Default) } + val listState = rememberLazyListState() + val controller = rememberNavController() val showSheet = rememberSaveable { mutableStateOf(false) } - val lastUpdated = remember { mutableLongStateOf(System.currentTimeMillis()) } - WalletNavigation( - account = account, - provider = component - ) { controller -> - WalletScreen( - onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, - header = { OverviewScreen(lastUpdated, component, viewModelStoreOwner) } - ) { - ServiceScreen( - serviceState = service, - provider = component, - viewModelStoreOwner = viewModelStoreOwner, - onAccountClicked = { - controller.navigateIfNecessary("profile/${it}") - }, - onClear = { recipient.value = null } - ) { showSheet.value = true } - } - DesignTitleBarHost("WalletScreen") { - titleBar { - DesignTitle { - Text(stringResource(R.string.wallet_label)) + val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val navigator = remember { NavigationInteractor(context, controller) } + CompositionLocalProvider(TransactionsNavigator.LocalTransactionsNavigator provides navigator) { + WalletNavigation( + account = account, + controller = controller, + provider = component + ) { controller -> + WalletPage( + onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, + header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) }, + transactions = { + TransactionsList( + uuid = account.id, + limit = postLimit, + lastUpdated = lastUpdated, + listState = listState, + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { controller.navigateIfNecessary("profile/${it}") } } + ) { + DashboardScreen( + dashboardState = service, + provider = component, + viewModelStoreOwner = viewModelStoreOwner, + onAccountClicked = { controller.navigateIfNecessary("profile/${it}") }, + onClear = { recipient.value = null } + ) { showSheet.value = true } + } + DesignTitleBarHost( + tag = "WalletScreen", + listener = { + scope.launch { + listState.animateScrollToItem(0) + } + } + ) { + titleBar { + DesignTitle { + Text(stringResource(R.string.wallet_label)) + } + } + } + MemberModal(postLimit, showSheet, component, viewModelStoreOwner) { + recipient.value = UiRecipient( + id = it.id, + username = it.username, + slug = it.slug, + imageUrl = it.imageUrl + ) + true } - } - MemberModal(postLimit, showSheet, component, viewModelStoreOwner) { - recipient.value = UiRecipient( - id = it.id, - username = it.username, - slug = it.slug, - imageUrl = it.imageUrl - ) - true - } - } -} - -@Composable -fun WalletScreen( - onRefresh: () -> Unit, - header: @Composable () -> Unit, - content: @Composable () -> Unit, -) { - val updatedHeader by rememberUpdatedState(header) - val updatedContent by rememberUpdatedState(content) - val state = remember { derivedStateOf { DesignStatefulScaffoldState.Success(Unit) } } - DesignRefreshableScaffold(state, onRefresh = onRefresh) { - Column(modifier = Modifier.padding(horizontal = 18.dp, vertical = 8.dp) - .fillMaxSize() - .verticalScroll(rememberScrollState()) - ) { - updatedHeader() - Spacer(modifier = Modifier.height(16.dp)) - updatedContent() } } } diff --git a/blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/ContentInteractorUsecase.kt b/blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/EngagementUsecase.kt similarity index 85% rename from blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/ContentInteractorUsecase.kt rename to blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/EngagementUsecase.kt index bd367bc6f..271eba5d9 100644 --- a/blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/ContentInteractorUsecase.kt +++ b/blog/domain/src/main/kotlin/eu/peernetwork/blog/domain/usecase/EngagementUsecase.kt @@ -8,9 +8,9 @@ import eu.peernetwork.core.common.paging.Pageable import eu.peernetwork.core.common.usecase.ParameterizedSuspendableUseCase import javax.inject.Inject -class ContentInteractorUsecase @Inject constructor( +class EngagementUsecase @Inject constructor( private val repository: EngagementRepository -) : ParameterizedSuspendableUseCase> { +) : ParameterizedSuspendableUseCase> { override suspend fun invoke(param: Parameter): Page { return repository.reactors( id = param.id, diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/comment/CommentViewModel.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/comment/CommentViewModel.kt index 297e41637..630589e76 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/comment/CommentViewModel.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/comment/CommentViewModel.kt @@ -118,17 +118,16 @@ class CommentViewModel @Inject constructor( } sealed interface Intent { - data object Idle: Intent data object Like: Intent data object Report: Intent data object Comment: Intent } - sealed class Status(val intent: Intent) { - data object Empty: Status(Intent.Idle) - data class Loading(val action: Intent): Status(action) - data class Success(val action: Intent, val content: T): Status(action) - data class Error(val action: Intent, val error: Throwable): Status(action) + sealed interface Status { + data object Empty: Status + data class Loading(val action: Intent): Status + data class Success(val action: Intent, val content: T): Status + data class Error(val action: Intent, val error: Throwable): Status } sealed interface State { diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/usecase/InteractionUsecase.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/usecase/InteractionUsecase.kt index 740e29fb7..bdd6a2472 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/usecase/InteractionUsecase.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/usecase/InteractionUsecase.kt @@ -6,7 +6,7 @@ import androidx.paging.PagingData import androidx.paging.PagingSource.LoadParams import androidx.paging.PagingSource.LoadResult import eu.peernetwork.blog.domain.model.Engagement -import eu.peernetwork.blog.domain.usecase.ContentInteractorUsecase +import eu.peernetwork.blog.domain.usecase.EngagementUsecase import eu.peernetwork.blog.ui.mapper.mapFromDomain import eu.peernetwork.blog.ui.model.UiAuthor import eu.peernetwork.core.common.paging.Pageable @@ -19,7 +19,7 @@ import javax.inject.Inject class InteractionUsecase @Inject constructor( private val dispatcher: Dispatcher, - private val usecase: ContentInteractorUsecase + private val usecase: EngagementUsecase ) : PagingUsecase() { private lateinit var param: Parameter @@ -41,7 +41,7 @@ class InteractionUsecase @Inject constructor( limit = param.page.limit ) val response = usecase( - ContentInteractorUsecase.Parameter( + EngagementUsecase.Parameter( id = param.id, engagement = param.engagement, page = currentPage diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignErrorLabel.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignErrorLabel.kt deleted file mode 100644 index 26d92faf2..000000000 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignErrorLabel.kt +++ /dev/null @@ -1,96 +0,0 @@ -package eu.peernetwork.core.ui.design.compose - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.common.interactor.ResourceInteractor -import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.exception.NoContentException -import eu.peernetwork.core.ui.theme.PeerTheme - -@Composable -fun DesignErrorLabel( - onRefresh: () -> Unit = {}, - error: Throwable, - resource: ResourceInteractor, - contentPadding: PaddingValues = PaddingValues(vertical = 8.dp), -) { - val errorMessage = stringResource(R.string.unknown_error_message) - val noContentMessage = stringResource(R.string.empty_message) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(contentPadding) - ) { - Box(modifier = Modifier.clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceVariant) - ) { - Icon( - painter = painterResource(R.drawable.ic_warning), - contentDescription = stringResource(R.string.error_label), - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.padding(14.dp) - .size(16.dp) - ) - } - Spacer(modifier = Modifier.width(12.dp)) - DesignErrorText( - if (error is NoContentException) { - Throwable(noContentMessage, error) - } else { Throwable(resource.string(error.message ?: errorMessage), error) }, - textAlign = TextAlign.Start, - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.tertiary - ), - modifier = Modifier.weight(1f) - ) - Spacer(modifier = Modifier.width(12.dp)) - IconButton(onRefresh) { - Icon( - painterResource(R.drawable.ic_refresh), - contentDescription = stringResource(R.string.retry_label), - tint = MaterialTheme.colorScheme.surfaceDim, - modifier = Modifier.padding(12.dp) - .size(24.dp) - ) - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewDesignErrorLabel() { - PeerTheme { - val resource = remember { object : ResourceInteractor { - override fun getBaseUrl(): String = "" - - override fun string(key: String): String = key - } } - DesignErrorLabel( - error = RuntimeException(), - resource = resource - ) - } -} diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignPagingScaffold.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignPagingScaffold.kt deleted file mode 100644 index fd2ff87a0..000000000 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignPagingScaffold.kt +++ /dev/null @@ -1,87 +0,0 @@ -package eu.peernetwork.core.ui.design.compose - -import androidx.compose.animation.core.Easing -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.paging.LoadState -import androidx.paging.PagingData -import androidx.paging.compose.LazyPagingItems -import androidx.paging.compose.collectAsLazyPagingItems -import kotlinx.coroutines.flow.Flow - -@Composable -fun DesignPagingScaffold( - state: State, - modifier: Modifier = Modifier, - onRefresh: () -> Unit = {}, - durationMillis: Int = 1000, - easing: Easing = FastOutSlowInEasing, - contentAlignment: Alignment = Alignment.Center, - placeholder: (@Composable () -> Unit)? = null, - errorContent: (@Composable (Throwable, () -> Unit) -> Unit)? = null, - content: @Composable (State, LazyPagingItems) -> Unit, -) { - val updatedContent by rememberUpdatedState(content) - val updatedErrorContent by rememberUpdatedState(errorContent) - DesignStatefulScaffold>>( - state = state, - modifier = modifier, - onRefresh = onRefresh, - durationMillis = durationMillis, - easing = easing, - contentAlignment = contentAlignment, - placeholder = placeholder, - errorContent = { - updatedErrorContent?.invoke(it, onRefresh) - ?: DesignError( - it, onRetry = onRefresh, - modifier = Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - ) }, - ) { flow -> - val lazyPagingItems = flow.collectAsLazyPagingItems() - val contentState = remember(lazyPagingItems.loadState.refresh) { derivedStateOf { - if (lazyPagingItems.loadState.refresh is LoadState.Error) { - DesignStatefulScaffoldState.Error((lazyPagingItems.loadState.refresh as LoadState.Error).error) - } else if (lazyPagingItems.itemCount > 0) { - DesignStatefulScaffoldState.Success(lazyPagingItems) - } else if (lazyPagingItems.loadState.refresh is LoadState.Loading) { - DesignStatefulScaffoldState.Loading - } else if (state.value is DesignStatefulScaffoldState.Success<*>) { - DesignStatefulScaffoldState.Success(lazyPagingItems) - } else { - state.value - } - } } - DesignStatefulScaffold>( - state = contentState, - onRefresh = onRefresh, - autoRefresh = false, - durationMillis = durationMillis, - easing = easing, - contentAlignment = contentAlignment, - placeholder = placeholder, - errorContent = { - updatedErrorContent?.invoke(it, onRefresh) - ?: DesignError( - it, onRetry = onRefresh, - modifier = Modifier.fillMaxSize() - .padding(bottom = 64.dp) - .verticalScroll(rememberScrollState()) - ) }, - content = { updatedContent(contentState, it) } - ) - } -} diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignRefreshableScaffold.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignRefreshableScaffold.kt deleted file mode 100644 index 5b1bbedd7..000000000 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignRefreshableScaffold.kt +++ /dev/null @@ -1,125 +0,0 @@ -package eu.peernetwork.core.ui.design.compose - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import dev.materii.pullrefresh.DragRefreshLayout -import dev.materii.pullrefresh.rememberPullRefreshState -import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.theme.PeerTheme - -@Composable -@Suppress("UNCHECKED_CAST") -fun DesignRefreshableScaffold( - state: State, - modifier: Modifier = Modifier, - onRefresh: () -> Unit = {}, - placeholder: (@Composable () -> Unit)? = null, - errorContent: (@Composable (Throwable) -> Unit)? = null, - content: @Composable (T) -> Unit, -) { - val contentState = remember { - derivedStateOf { - (state.value as? DesignStatefulScaffoldState.Success<*>?)?.result as T? - } - } - val errorState = remember { derivedStateOf { - (state.value as? DesignStatefulScaffoldState.Error?)?.error - } } - val updatedContent by rememberUpdatedState(content) - val updatedPlaceholder by rememberUpdatedState(placeholder) - val updatedErrorContent by rememberUpdatedState(errorContent) - val refreshState = rememberPullRefreshState( - refreshing = state.value is DesignStatefulScaffoldState.Loading, - onRefresh = onRefresh - ) - DragRefreshLayout(state = refreshState, modifier = modifier) { - if (errorState.value != null) { - updatedErrorContent?.invoke(errorState.value!!) - ?: DesignRefreshErrorContent( - errorState.value!!, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - ) - } else if (contentState.value != null) { - updatedContent(contentState.value!!) - } else if (state.value is DesignStatefulScaffoldState.Empty) { - updatedPlaceholder?.invoke() ?: DesignRefreshErrorContent( - RuntimeException(stringResource(R.string.empty_message)), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - ) - } - } -} - -@Composable -fun DesignRefreshErrorContent( - error: Throwable, - modifier: Modifier = Modifier, - textAlign: TextAlign? = null, - verticalArrangement: Arrangement.Vertical = Arrangement.Center, - horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, - style: TextStyle = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.tertiary - ) -) { - Column( - modifier = modifier, - verticalArrangement = verticalArrangement, - horizontalAlignment = horizontalAlignment - ) { - Text( - if (!error.message.isNullOrEmpty()) { - error.message!! - } else { - stringResource(R.string.unknown_error_message) - }, - textAlign = textAlign, - style = style - ) - Text( - stringResource(R.string.refresh_message), - textAlign = textAlign, - modifier = Modifier.padding(top = 4.dp), - style = style.copy( - fontSize = MaterialTheme.typography.bodySmall.fontSize - ) - ) - Spacer(modifier = Modifier.height(64.dp)) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewDesignRefreshableContent() { - PeerTheme { - DesignRefreshErrorContent( - RuntimeException("Hello, world!"), - Modifier.fillMaxSize() - ) - } -} diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignAvatar.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignAvatar.kt index 3c09a9366..d521ee83d 100644 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignAvatar.kt +++ b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignAvatar.kt @@ -1,6 +1,5 @@ package eu.peernetwork.core.ui.design.luna -import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -53,7 +52,7 @@ fun DesignAvatar( } @Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Preview fun PreviewDesignAvatar() { DesignTheme(isDarkMode = true) { DesignAvatar( diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignError.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignError.kt similarity index 97% rename from core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignError.kt rename to core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignError.kt index 18476be45..061833280 100644 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignError.kt +++ b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignError.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.core.ui.design.compose +package eu.peernetwork.core.ui.design.material import android.content.res.Configuration import androidx.compose.foundation.Image @@ -30,7 +30,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.peernetwork.core.common.interactor.ResourceInteractor import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.design.material.DesignOutlinedButton import eu.peernetwork.core.ui.exception.NoContentException import eu.peernetwork.core.ui.theme.PeerTheme diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignStatefulScaffold.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignStatefulScaffold.kt similarity index 99% rename from core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignStatefulScaffold.kt rename to core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignStatefulScaffold.kt index 2d3244810..0afff7c9e 100644 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/compose/DesignStatefulScaffold.kt +++ b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/material/DesignStatefulScaffold.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.core.ui.design.compose +package eu.peernetwork.core.ui.design.material import android.content.res.Configuration import androidx.compose.animation.core.Easing diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/editor/video/VideoScreen.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/editor/video/VideoScreen.kt index 909b4903b..e520c635a 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/editor/video/VideoScreen.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/editor/video/VideoScreen.kt @@ -36,8 +36,8 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.material.DesignThumbnail import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.core.ui.theme.PeerTheme diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/audio/AudioScreen.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/audio/AudioScreen.kt index 7dea19a1a..4dc95c52d 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/audio/AudioScreen.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/audio/AudioScreen.kt @@ -38,9 +38,10 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.media.core.model.UiAttachment import eu.peernetwork.media.core.model.UiFile @@ -142,7 +143,7 @@ fun AudioScreen( verticalArrangement = Arrangement.Center ) { Image( - painter = painterResource(id = eu.peernetwork.core.ui.R.drawable.ic_music), + painter = painterResource(id = R.drawable.ic_music), contentDescription = "Audio Icon", modifier = Modifier .weight(1f) diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/directory/DirectoryScreen.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/directory/DirectoryScreen.kt index 673f671eb..4d7055f95 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/directory/DirectoryScreen.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/directory/DirectoryScreen.kt @@ -37,8 +37,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.material.DesignThumbnail import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.media.core.model.UiMimeType diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoPage.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoPage.kt index 35492a4e6..95b02dd1f 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoPage.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoPage.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.material.DesignCollapsibleBottomSheet import eu.peernetwork.core.ui.design.material.DesignOutlinedButton import eu.peernetwork.core.ui.design.material.DesignTitle diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoScreen.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoScreen.kt index 85c97520d..93f86a39d 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoScreen.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/photo/PhotoScreen.kt @@ -24,8 +24,8 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.media.core.model.UiAttachment import eu.peernetwork.media.core.model.UiFile diff --git a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/video/VideoScreen.kt b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/video/VideoScreen.kt index a7edf9e40..ec1226589 100644 --- a/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/video/VideoScreen.kt +++ b/media/ui/src/main/kotlin/eu/peernetwork/media/ui/selector/video/VideoScreen.kt @@ -34,8 +34,8 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.media.core.model.UiAttachment import eu.peernetwork.media.core.model.UiFile diff --git a/user/data/src/main/kotlin/eu/peernetwork/user/data/interactor/AuthenticationInteractorDelegate.kt b/user/data/src/main/kotlin/eu/peernetwork/user/data/interactor/AuthenticationInteractorDelegate.kt index edce8d95e..b98bdde82 100644 --- a/user/data/src/main/kotlin/eu/peernetwork/user/data/interactor/AuthenticationInteractorDelegate.kt +++ b/user/data/src/main/kotlin/eu/peernetwork/user/data/interactor/AuthenticationInteractorDelegate.kt @@ -7,6 +7,7 @@ import eu.peernetwork.persistence.domain.retrievable.RetrievableString import eu.peernetwork.user.domain.exception.AccountNotFoundException import eu.peernetwork.user.domain.interactor.AuthenticationInteractor import eu.peernetwork.user.domain.model.Account +import eu.peernetwork.user.domain.model.Status import eu.peernetwork.user.domain.repository.AccountRepository import eu.peernetwork.user.domain.repository.AuthenticationRepository import kotlinx.coroutines.flow.Flow @@ -47,7 +48,7 @@ class AuthenticationInteractorDelegate @Inject constructor( logout() } throw error - } + }.let { it.copy(status = it.status ?: Status.VISIBLE) } } override fun observeAccount(): Flow = observable(ACCOUNT_KEY).map { diff --git a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt index 4ac7766ca..fae0236e2 100644 --- a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt +++ b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt @@ -1,11 +1,18 @@ package eu.peernetwork.wallet.data.api +import eu.peernetwork.core.common.paging.Page +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.wallet.domain.model.Filter import eu.peernetwork.wallet.domain.model.Token import eu.peernetwork.wallet.domain.model.Quote import eu.peernetwork.wallet.domain.model.Receipt +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Transaction import java.math.BigDecimal interface TransferApi { + suspend fun getAll(filter: Filter, sort: Sort, page: Pageable): Page + suspend fun getQuote(token: Token): Quote suspend fun send(recipient: String, tokens: BigDecimal): Receipt diff --git a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt index c205c78f9..ebff3a699 100644 --- a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt +++ b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt @@ -1,9 +1,14 @@ package eu.peernetwork.wallet.data.repository +import eu.peernetwork.core.common.paging.Page +import eu.peernetwork.core.common.paging.Pageable import eu.peernetwork.wallet.data.api.TransferApi +import eu.peernetwork.wallet.domain.model.Filter import eu.peernetwork.wallet.domain.model.Token import eu.peernetwork.wallet.domain.model.Quote import eu.peernetwork.wallet.domain.model.Receipt +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Transaction import eu.peernetwork.wallet.domain.repository.TransactionRepository import java.math.BigDecimal import javax.inject.Inject @@ -11,6 +16,10 @@ import javax.inject.Inject class TransactionRepositoryDelegate @Inject constructor( private val api: TransferApi ) : TransactionRepository { + override suspend fun getAll(filter: Filter, sort: Sort, page: Pageable): Page { + return api.getAll(filter, sort, page) + } + override suspend fun getQuote(token: Token): Quote { return api.getQuote(token) } diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Amount.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Amount.kt new file mode 100644 index 000000000..ed56c2535 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Amount.kt @@ -0,0 +1,8 @@ +package eu.peernetwork.wallet.domain.model + +import java.math.BigDecimal + +data class Amount( + val net: BigDecimal, + val gross: BigDecimal, +) diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Category.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Category.kt new file mode 100644 index 000000000..571320d4a --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Category.kt @@ -0,0 +1,13 @@ +package eu.peernetwork.wallet.domain.model + +enum class Category { + AD, + MINT, + POST, + PURCHASE, + TRANSFER, + COMMENT, + LIKE, + DISLIKE, + DEFAULT, +} diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt new file mode 100644 index 000000000..d8a652f5b --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.domain.model + +import java.math.BigDecimal + +data class Fees( + val total: BigDecimal, + val burn: BigDecimal, + val peer: BigDecimal, + val commission: BigDecimal, +) diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Filter.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Filter.kt new file mode 100644 index 000000000..db2efa889 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Filter.kt @@ -0,0 +1,9 @@ +package eu.peernetwork.wallet.domain.model + +sealed class Filter( + val type: Transaction.Type?, + val start: String?, + val end: String?, +) { + data object None : Filter(null, null, null) +} diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Sort.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Sort.kt new file mode 100644 index 000000000..dc887294c --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Sort.kt @@ -0,0 +1,6 @@ +package eu.peernetwork.wallet.domain.model + +enum class Sort { + NEWEST, + OLDEST, +} diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Status.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Status.kt new file mode 100644 index 000000000..fbe4fa41b --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Status.kt @@ -0,0 +1,7 @@ +package eu.peernetwork.wallet.domain.model + +enum class Status { + VISIBLE, + HIDDEN, + ILLEGAL, +} diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Transaction.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Transaction.kt new file mode 100644 index 000000000..2248d3632 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Transaction.kt @@ -0,0 +1,20 @@ +package eu.peernetwork.wallet.domain.model + +data class Transaction( + val id: String, + val sender: User, + val recipient: User, + val message: String?, + val category: Category, + val amount: Amount, + val fees: Fees?, + val createdAt: Long +) { + enum class Type { + TRANSACTION, + AIRDROP, + PAYMENT, + BURN, + MINT + } +} diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/User.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/User.kt new file mode 100644 index 000000000..316a1ee9f --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/User.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.domain.model + +data class User( + val id: String, + val slug: Int, + val username: String, + val imageUrl: String, + val isAccessible: Boolean, + val status: Status +) diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt index 020b8434a..b032650fc 100644 --- a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt @@ -1,11 +1,22 @@ package eu.peernetwork.wallet.domain.repository +import eu.peernetwork.core.common.paging.Page +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.wallet.domain.model.Filter import eu.peernetwork.wallet.domain.model.Token import eu.peernetwork.wallet.domain.model.Quote import eu.peernetwork.wallet.domain.model.Receipt +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Transaction import java.math.BigDecimal interface TransactionRepository { + suspend fun getAll( + filter: Filter = Filter.None, + sort: Sort = Sort.NEWEST, + page: Pageable + ): Page + suspend fun getQuote(token: Token): Quote suspend fun send(recipient: String, price: BigDecimal): Receipt diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransactionsUsecase.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransactionsUsecase.kt new file mode 100644 index 000000000..1e91005ae --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransactionsUsecase.kt @@ -0,0 +1,24 @@ +package eu.peernetwork.wallet.domain.usecase + +import eu.peernetwork.core.common.paging.Page +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.core.common.usecase.ParameterizedSuspendableUseCase +import eu.peernetwork.wallet.domain.model.Filter +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Transaction +import eu.peernetwork.wallet.domain.repository.TransactionRepository +import javax.inject.Inject + +class TransactionsUsecase @Inject constructor( + private val repository: TransactionRepository +) : ParameterizedSuspendableUseCase>{ + override suspend fun invoke(param: Parameter): Page { + return repository.getAll(param.filter, param.sort, param.page) + } + + data class Parameter( + val filter: Filter = Filter.None, + val sort: Sort = Sort.NEWEST, + val page: Pageable + ) +} diff --git a/wallet/remote/src/main/graphql/wallet.schema.graphqls b/wallet/remote/src/main/graphql/wallet.schema.graphqls index 0cf1091c0..bd958e478 100644 --- a/wallet/remote/src/main/graphql/wallet.schema.graphqls +++ b/wallet/remote/src/main/graphql/wallet.schema.graphqls @@ -175,6 +175,10 @@ enum DayFilterType { D5 + D6 + + D7 + W0 M0 @@ -182,10 +186,58 @@ enum DayFilterType { Y0 } +enum TransactionCategory { + P2P_TRANSFER + + AD_PINNED + + POST_CREATE + + LIKE + + DISLIKE + + COMMENT + + TOKEN_MINT + + SHOP_PURCHASE +} + enum OnboardingType { INTROONBOARDING } +enum ModerationStatus { + waiting_for_review + + hidden + + restored + + illegal +} + +enum ModerationContentType { + post + + comment + + user +} + +enum ContentVisibilityStatus { + NORMAL + + HIDDEN + + ILLEGAL +} + +enum ShopSupportedDeliveryCountry { + GERMANY +} + input AdvertisementHistoryFilter { from: Date @@ -244,28 +296,6 @@ input PostInput { uploadedFiles: String } -input ChatInput { - name: String! - - image: String - - recipients: [String!]! -} - -input UpdateChatInput { - chatid: ID! - - name: String - - image: String -} - -input ChatParticipantsInput { - chatid: ID! - - recipients: [String!]! -} - input FeedInput { feedid: ID! @@ -284,10 +314,50 @@ input UserPreferencesInput { shownOnboardings: [OnboardingType!] } -type AdvertisementPost { - post: Post! +input ShopOrderDetailsInput { + name: String! - advertisement: AdvCreator! + email: String! + + addressline1: String! + + addressline2: String + + city: String! + + zipcode: String! + + country: ShopSupportedDeliveryCountry! + + shopItemSpecs: ShopItemSpecsInput +} + +input ShopItemSpecsInput { + size: String +} + +type TransactionHistoryItem { + operationid: String! + + transactionId: String! + + transactionCategory: TransactionCategory + + transactiontype: String! + + tokenamount: String! + + netTokenAmount: String! + + message: String + + createdat: String! + + sender: BasicUserInfo! + + recipient: BasicUserInfo! + + fees: TransactionFeeSummary } """ @@ -301,6 +371,22 @@ The `Boolean` scalar type represents `true` or `false`. """ scalar Boolean +type TransactionFeeSummary { + total: Decimal + + burn: Decimal + + peer: Decimal + + inviter: Decimal +} + +type AdvertisementPost { + post: Post! + + advertisement: AdvCreator! +} + type AdvCreator { advertisementid: ID! @@ -385,7 +471,9 @@ type AdvertisementHistoryResult { type ListedAdvertisementData { status: String! - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + meta: DefaultResponse! affectedRows: AdvertisementHistoryResult } @@ -423,7 +511,9 @@ type ContactusResponsePayload { type ListAdvertisementData { status: String! - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + meta: DefaultResponse! affectedRows: [AdvertisementRow] } @@ -443,6 +533,14 @@ type Post { createdat: Date! + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + + amountreports: Int! + amountlikes: Int! amountviews: Int! @@ -485,10 +583,20 @@ type Comment { createdat: Date! + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + amountlikes: Int! amountreplies: Int! + amountreports: Int! + + isreported: Boolean! + isliked: Boolean! user: ProfileUser! @@ -503,9 +611,21 @@ type ProfileUser { img: String - isfollowed: Boolean + visibilityStatus: ContentVisibilityStatus! - isfollowing: Boolean + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + + isfollowed: Boolean @deprecated(reason: "Use iFollowThisUser \/ thisUserFollowsMe") + + isfollowing: Boolean @deprecated(reason: "Use iFollowThisUser \/ thisUserFollowsMe") + + iFollowThisUser: Boolean! + + thisUserFollowsMe: Boolean! + + isreported: Boolean isfriend: Boolean } @@ -544,12 +664,20 @@ type Transaction { message: String! createdat: String! + + sender: BasicUserInfo! + + recipient: BasicUserInfo! } type TransferToken { - tokenSend: Decimal + tokenSend: Decimal @deprecated(reason: "Use tokenSendFormatted") - tokensSubstractedFromWallet: Decimal + tokensSubstractedFromWallet: Decimal @deprecated(reason: "Use tokensSubstractedFromWalletFormatted") + + tokenSendFormatted: String! + + tokensSubstractedFromWalletFormatted: String! createdat: String } @@ -597,15 +725,15 @@ type LogWins { } type UserLogWins { - status: String + status: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") counter: Int! - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") affectedRows: [LogWins] - meta: DefaultResponse + meta: DefaultResponse! } type UserInfo { @@ -615,6 +743,8 @@ type UserInfo { amountposts: Int! + amountreports: Int! + amountblocked: Int! amountfollower: Int! @@ -638,46 +768,6 @@ type CommentInfo { comments: Int! } -type Chat { - id: ID! - - image: String! - - name: String! - - createdat: Date - - updatedat: Date - - chatmessages: [ChatMessage!]! - - chatparticipants: [ChatParticipant!]! -} - -type ChatMessage { - id: ID! - - senderid: ID! - - chatid: ID! - - content: String! - - createdat: Date! -} - -type ChatParticipant { - userid: ID! - - img: String! - - username: String! - - slug: Int! - - hasaccess: Int! -} - type Profile { id: ID @@ -691,9 +781,21 @@ type Profile { biography: String - isfollowed: Boolean + visibilityStatus: ContentVisibilityStatus! - isfollowing: Boolean + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + + isfollowed: Boolean @deprecated(reason: "Use iFollowThisUser \/ thisUserFollowsMe") + + isfollowing: Boolean @deprecated(reason: "Use iFollowThisUser \/ thisUserFollowsMe") + + iFollowThisUser: Boolean! + + thisUserFollowsMe: Boolean! + + isreported: Boolean amountposts: Int @@ -707,19 +809,23 @@ type Profile { amountblocked: Int - imageposts: [ProfilePostMedia!] + amountreports: Int - textposts: [ProfilePostMedia!] + imageposts: [ProfilePostMedia!] @deprecated(reason: "use listPosts by userid") - videoposts: [ProfilePostMedia!] + textposts: [ProfilePostMedia!] @deprecated(reason: "use listPosts by userid") - audioposts: [ProfilePostMedia!] + videoposts: [ProfilePostMedia!] @deprecated(reason: "use listPosts by userid") + + audioposts: [ProfilePostMedia!] @deprecated(reason: "use listPosts by userid") } type ProfileInfo { - status: String + status: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - ResponseCode: String + meta: DefaultResponse! affectedRows: Profile } @@ -738,6 +844,12 @@ type BlockedUser { username: String slug: Int + + hasActiveReports: Boolean! + + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! } type BlockedUsers { @@ -780,12 +892,22 @@ type MintingData { tokensMintedYesterday: Float! } +type MintAccount { + accountid: ID! + + initialBalance: Decimal! + + currentBalance: Decimal! + + updatedat: String! +} + type CurrentLiquidity { - status: String + status: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - meta: DefaultResponse + meta: DefaultResponse! currentliquidity: Decimal } @@ -803,6 +925,10 @@ type DailyGemStatusData { d5: Decimal + d6: Decimal + + d7: Decimal + w0: Decimal m0: Decimal @@ -896,6 +1022,10 @@ type ProfilePostMedia { media: String! createdat: String + + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! } type PostCommentsData { @@ -918,6 +1048,10 @@ type PostCommentsData { user: BasicUserInfo subcomments: [PostSubCommentsData] + + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! } type PostSubCommentsData { @@ -940,6 +1074,10 @@ type PostSubCommentsData { isliked: Boolean user: BasicUserInfo + + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! } type BasicUserInfo { @@ -953,6 +1091,12 @@ type BasicUserInfo { biography: String + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + updatedat: Date! } @@ -969,6 +1113,12 @@ type User { biography: String + visibilityStatus: ContentVisibilityStatus! + + isHiddenForUsers: Boolean! + + hasActiveReports: Boolean! + createdat: Date updatedat: Date @@ -1010,22 +1160,6 @@ type PostInfo { comments: Int! } -type ChatMessageInfo { - messid: ID - - chatid: ID - - userid: ID - - content: String - - createdat: Date -} - -type ChatInfo { - chatid: ID -} - type AllUserInfo { followerid: ID @@ -1041,24 +1175,132 @@ type AllUserFriends { counter: Int! - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + meta: DefaultResponse! affectedRows: [AllUserInfo] } +type ModerationStatsResponse { + status: String! + + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + affectedRows: ModerationStats + + meta: DefaultResponse! +} + +type ModerationStats { + AmountAwaitingReview: Int! + + AmountHidden: Int! + + AmountRestored: Int! + + AmountIllegal: Int! +} + +type ModerationItemListResponse { + status: String! + + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + affectedRows: [ModerationItem!]! + + meta: DefaultResponse! +} + +type ModerationItem { + moderationTicketId: ID! + + targetContentId: ID! + + targettype: String! + + reportscount: Int! + + status: String! + + createdat: String! + + targetcontent: TargetContent! + + reporters: [BasicUserInfo!]! + + moderatedBy: BasicUserInfo +} + +type TargetContent { + post: Post + + comment: Comment + + user: BasicUserInfo +} + +type ShopItemSpecs { + size: String! +} + +type ShopOrderDetailsResponse { + meta: DefaultResponse! + + status: String! + + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") + + affectedRows: ShopOrderDetails! +} + +type ShopOrderDetails { + shopOrderId: String! + + shopItemId: String! + + shopItemSpecs: ShopItemSpecs + + deliveryDetails: ShopOrderDeliveryDetails! + + createdat: Date! +} + +type ShopOrderDeliveryDetails { + name: String! + + email: String! + + addressline1: String! + + addressline2: String + + city: String! + + zipcode: String! + + country: String! +} + type DefaultResponse { status: String! RequestId: String - ResponseCode: String + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") ResponseMessage: String } -type ListAdvertisementPostsResponse { +type TransactionHistoryResponse { meta: DefaultResponse + affectedRows: [TransactionHistoryItem!] +} + +type ListAdvertisementPostsResponse { + meta: DefaultResponse! + status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") @@ -1081,7 +1323,7 @@ type HelloResponse { } type ReferralResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1091,7 +1333,7 @@ type ReferralResponse { } type ContactusResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1101,9 +1343,9 @@ type ContactusResponse { } type RegisterResponse { - meta: DefaultResponse + meta: DefaultResponse! - status: String! + status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") @@ -1111,7 +1353,7 @@ type RegisterResponse { } type ResetPasswordRequestResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1121,7 +1363,7 @@ type ResetPasswordRequestResponse { } type AuthPayload { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1132,8 +1374,14 @@ type AuthPayload { refreshToken: String } +type MintAccountResponse { + meta: DefaultResponse! + + mintAccount: MintAccount! +} + type TransactionResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1143,7 +1391,7 @@ type TransactionResponse { } type TransferTokenResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1153,7 +1401,7 @@ type TransferTokenResponse { } type ListTodaysInteractionsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1163,7 +1411,7 @@ type ListTodaysInteractionsResponse { } type PostInteractionResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1173,7 +1421,7 @@ type PostInteractionResponse { } type GetDailyResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1191,7 +1439,7 @@ type DailyFreeResponse { } type PostEligibilityResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1201,7 +1449,7 @@ type PostEligibilityResponse { } type UserInfoResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1211,7 +1459,7 @@ type UserInfoResponse { } type PostInfoResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1221,7 +1469,7 @@ type PostInfoResponse { } type CommentInfoResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1230,20 +1478,8 @@ type CommentInfoResponse { affectedRows: CommentInfo } -type ChatResponse { - meta: DefaultResponse - - status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") - - counter: Int! - - ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - - affectedRows: [Chat!] -} - type FollowStatusResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1253,7 +1489,7 @@ type FollowStatusResponse { } type UserPreferencesResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1263,7 +1499,7 @@ type UserPreferencesResponse { } type AddPostResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1272,30 +1508,8 @@ type AddPostResponse { affectedRows: PostInfo } -type AddChatResponse { - meta: DefaultResponse - - status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") - - ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - - affectedRows: ChatInfo -} - -type AddChatmessageResponse { - meta: DefaultResponse - - status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") - - counter: Int! - - ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") - - affectedRows: [ChatMessageInfo] -} - type BasicUserInfoResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1305,7 +1519,7 @@ type BasicUserInfoResponse { } type UserFriendsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1317,7 +1531,7 @@ type UserFriendsResponse { } type FollowRelationsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1329,7 +1543,7 @@ type FollowRelationsResponse { } type BlockedUsersResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1341,7 +1555,7 @@ type BlockedUsersResponse { } type UserListResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1353,7 +1567,7 @@ type UserListResponse { } type TagSearchResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1365,7 +1579,7 @@ type TagSearchResponse { } type PostResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1375,7 +1589,7 @@ type PostResponse { } type PostListResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1387,7 +1601,7 @@ type PostListResponse { } type CommentResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1398,8 +1612,16 @@ type CommentResponse { affectedRows: [Comment] } +type CommentListResponse { + meta: DefaultResponse! + + counter: Int! + + affectedRows: [Comment!] +} + type ReferralInfoResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1411,7 +1633,7 @@ type ReferralInfoResponse { } type ReferralListResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1423,7 +1645,7 @@ type ReferralListResponse { } type GetActionPricesResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1433,11 +1655,11 @@ type GetActionPricesResponse { } type TokenomicsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") - ResponseCode: Int! + ResponseCode: String @deprecated(reason: "use meta.ResponseCode . this field will be removed after 15 October`.") actionTokenPrices: ActionPriceResult! @@ -1447,7 +1669,7 @@ type TokenomicsResponse { } type DailyGemStatusResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1457,7 +1679,7 @@ type DailyGemStatusResponse { } type DailyGemsResultsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1467,7 +1689,7 @@ type DailyGemsResultsResponse { } type GemstersResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1479,7 +1701,7 @@ type GemstersResponse { } type GemsterResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1489,7 +1711,7 @@ type GemsterResponse { } type LiquidityPoolResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1501,7 +1723,7 @@ type LiquidityPoolResponse { } type TestingPoolResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1513,7 +1735,7 @@ type TestingPoolResponse { } type PostCommentsResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1525,7 +1747,7 @@ type PostCommentsResponse { } type PercentBeforeTransactionResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1535,7 +1757,7 @@ type PercentBeforeTransactionResponse { } type RefreshMarketCapResponse { - meta: DefaultResponse + meta: DefaultResponse! status: String! @deprecated(reason: "use meta.status . this field will be removed after 15 October`.") @@ -1544,12 +1766,6 @@ type RefreshMarketCapResponse { affectedRows: RefreshMarketCapData } -type Subscription { - getChatMessages(chatid: ID!): AddChatmessageResponse! - - setChatMessages(chatid: ID!, content: String!): AddChatmessageResponse! -} - type Query { hello: HelloResponse! @@ -1559,23 +1775,21 @@ type Query { listUsersV2(contentFilterBy: ContentFilterType, userid: ID, username: String, offset: Int, limit: Int): UserListResponse! - getProfile(userid: ID, contentFilterBy: ContentFilterType, postLimit: Int): ProfileInfo! + getProfile(userid: ID, contentFilterBy: ContentFilterType, postLimit: Int @deprecated(reason: "to get posts, use listPosts with userid")): ProfileInfo! listFollowRelations(contentFilterBy: ContentFilterType, userid: ID, offset: Int, limit: Int): FollowRelationsResponse! - listFriends(contentFilterBy: ContentFilterType, offset: Int, limit: Int): UserFriendsResponse! + listFriends(contentFilterBy: ContentFilterType, userid: ID, offset: Int, limit: Int): UserFriendsResponse! getUserInfo: UserInfoResponse! - listBlockedUsers(offset: Int, limit: Int): BlockedUsersResponse! + listBlockedUsers(contentFilterBy: ContentFilterType, offset: Int, limit: Int): BlockedUsersResponse! listPosts(filterBy: [PostFilterType!], contentFilterBy: ContentFilterType, IgnorList: IgnoreOption, sortBy: PostSortType, userid: ID, postid: ID, title: String, tag: String, from: Date, to: Date, offset: Int, limit: Int, commentOffset: Int, commentLimit: Int): PostListResponse! - listAdvertisementPosts(filterBy: [ContentType!], userid: ID, postid: ID, tag: String, offset: Int, limit: Int, commentOffset: Int, commentLimit: Int): ListAdvertisementPostsResponse! - - getPostInfo(postid: ID!): PostInfoResponse! + listAdvertisementPosts(filterBy: [ContentType!], contentFilterBy: ContentFilterType, userid: ID, postid: ID, tag: String, offset: Int, limit: Int, commentOffset: Int, commentLimit: Int): ListAdvertisementPostsResponse! - getCommentInfo(commentId: ID!): CommentInfoResponse! + listComments(postid: ID!, contentFilterBy: ContentFilterType, commentOffset: Int, commentLimit: Int): CommentListResponse! listChildComments(parent: ID!, offset: Int, limit: Int): CommentResponse! @@ -1583,12 +1797,6 @@ type Query { searchTags(tagName: String!, offset: Int, limit: Int): TagSearchResponse! - getChat(chatid: ID!, messageOffset: Int, messageLimit: Int): ChatResponse! - - listChats(sortBy: String, from: Date, to: Date, offset: Int, limit: Int, messageOffset: Int, messageLimit: Int): ChatResponse! - - listChatMessages(chatid: ID!, offset: Int, limit: Int): AddChatmessageResponse! - getDailyFreeStatus: GetDailyResponse! balance: CurrentLiquidity! @@ -1609,11 +1817,15 @@ type Query { getTransactionHistory(type: TokenMovementFilterType, direction: DirectionFilterType, start_date: String, end_date: String, limit: Int, offset: Int, sort: SortFilterType): TransactionResponse! + transactionHistory(type: TokenMovementFilterType, start_date: String, end_date: String, limit: Int, offset: Int, sort: SortFilterType): TransactionHistoryResponse! + postInteractions(getOnly: GetOnly!, contentFilterBy: ContentFilterType, postOrCommentId: ID!, offset: Int, limit: Int): PostInteractionResponse getTokenomics: TokenomicsResponse! advertisementHistory(filter: AdvertisementHistoryFilter, sort: AdvertisementSort, offset: Int, limit: Int): ListedAdvertisementData! + + shopOrderDetails(transactionId: String!): ShopOrderDetailsResponse! } type Mutation { @@ -1635,30 +1847,12 @@ type Mutation { deleteAccount(password: String!): DefaultResponse! - createChat(input: ChatInput!): AddChatResponse! - - updateChatInformations(input: UpdateChatInput!): AddChatResponse! - - addChatParticipants(input: ChatParticipantsInput!): AddChatResponse! - - removeChatParticipants(input: ChatParticipantsInput!): AddChatResponse! - - sendChatMessage(chatid: ID!, content: String!): AddChatmessageResponse! - - deleteChatMessage(chatid: ID!, messid: Int!): DefaultResponse! - - deleteChat(id: ID!): DefaultResponse! - - createChatFeed(input: FeedInput!): AddPostResponse! - likeComment(commentid: ID!): DefaultResponse! reportComment(commentid: ID!): DefaultResponse! reportUser(userid: ID!): DefaultResponse! - deletePost(id: ID!): DefaultResponse! - createPost(action: PostType!, input: PostInput!): PostResponse! createComment(action: CommentType!, postid: ID!, parentid: ID, content: String!): CommentResponse! @@ -1671,7 +1865,9 @@ type Mutation { advertisePostBasic(postid: ID!, startday: Date!, durationInDays: AdDuration!, advertisePlan: AdvertisementBasicPlan!): ListAdvertisementData! - advertisePostPinned(postid: ID!, advertisePlan: AdvertisementPinnedPlan!, forceUpdate: Boolean): ListAdvertisementData! + advertisePostPinned(postid: ID!, advertisePlan: AdvertisementPinnedPlan!): ListAdvertisementData! + + performShopOrder(tokenAmount: Decimal!, shopItemId: String!, orderDetails: ShopOrderDetailsInput!): DefaultResponse! } """ @@ -1966,5 +2162,4 @@ directive @oneOf on INPUT_OBJECT schema { query: Query mutation: Mutation - subscription: Subscription } diff --git a/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql new file mode 100644 index 000000000..19141d48c --- /dev/null +++ b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql @@ -0,0 +1,60 @@ +query TransactionHistory( + $type: TokenMovementFilterType, + $start: String, + $end: String, + $limit: Int, + $offset: Int, + $sort: SortFilterType, +) { + transactionHistory( + type: $type, + start_date: $start, + end_date: $end, + limit: $limit, + offset: $offset, + sort: $sort + ) { + meta { + status + ResponseCode + } + affectedRows { + operationid + transactionId + transactionCategory + transactiontype + tokenamount + netTokenAmount + sender { + userid + img + username + slug + biography + visibilityStatus + isHiddenForUsers + hasActiveReports + updatedat + } + recipient { + userid + img + username + slug + biography + visibilityStatus + isHiddenForUsers + hasActiveReports + updatedat + } + fees { + total + burn + peer + inviter + } + message + createdat + } + } +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt index f7d064947..9e97d435b 100644 --- a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt @@ -1,20 +1,56 @@ package eu.peernetwork.wallet.remote.api +import com.apollographql.apollo3.api.Optional +import eu.peernetwork.core.common.paging.Page +import eu.peernetwork.core.common.paging.Pageable import eu.peernetwork.core.remote.api.RequestClient import eu.peernetwork.core.remote.extension.assertOrThrow +import eu.peernetwork.core.remote.extension.executeOrThrow import eu.peernetwork.core.remote.extension.getOrThrow import eu.peernetwork.wallet.data.api.TransferApi +import eu.peernetwork.wallet.domain.model.Filter import eu.peernetwork.wallet.domain.model.Token import eu.peernetwork.wallet.domain.model.Quote import eu.peernetwork.wallet.domain.model.Receipt +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Transaction +import eu.peernetwork.wallet.remote.mapper.mapFromDomain +import eu.peernetwork.wallet.remote.mapper.mapToDomain +import eu.peernetwork.wallet.remote.mapper.mapToType import wallet.wallet.eu.peernetwork.wallet.remote.GetActionPricesQuery import wallet.wallet.eu.peernetwork.wallet.remote.ResolveTransferMutation +import wallet.wallet.eu.peernetwork.wallet.remote.TransactionHistoryQuery import java.math.BigDecimal import javax.inject.Inject +import javax.inject.Named class TransferApiDelegate @Inject constructor( - private val client: RequestClient + private val client: RequestClient, + @Named("mediaUrl") private val url: String, ) : TransferApi { + override suspend fun getAll(filter: Filter, sort: Sort, page: Pageable): Page { + val query = TransactionHistoryQuery( + type = Optional.presentIfNotNull(filter.mapToType()), + limit = Optional.present(page.limit), + offset = Optional.present(page.offset), + sort = Optional.present(sort.mapFromDomain()) + ) + val response = client().query(query).executeOrThrow() + val data = response.getOrThrow().transactionHistory + response.assertOrThrow(data.meta?.status, data.meta?.ResponseCode) + val transactions = data.affectedRows?.map { transaction -> + transaction.mapToDomain().let { + it.copy(sender = it.sender.copy(imageUrl = "$url${it.sender.imageUrl}")) + .copy(recipient = it.recipient.copy(imageUrl = "$url${it.recipient.imageUrl}")) + } + } + return Page( + count = transactions?.size ?: 0, + offset = page.offset, + items = transactions ?: emptyList() + ) + } + override suspend fun getQuote(token: Token): Quote { val response = client().query(GetActionPricesQuery()).execute() val data = response.getOrThrow().getActionPrices diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Amount.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Amount.kt new file mode 100644 index 000000000..8da1787c5 --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Amount.kt @@ -0,0 +1,5 @@ +package eu.peernetwork.wallet.remote.mapper + +import java.math.BigDecimal + +fun String.mapToAmount(): BigDecimal = BigDecimal(this) diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Category.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Category.kt new file mode 100644 index 000000000..e096cd693 --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Category.kt @@ -0,0 +1,18 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Category +import wallet.type.TransactionCategory + +fun TransactionCategory.mapFromDomain(): Category { + return when (this) { + TransactionCategory.TOKEN_MINT -> Category.MINT + TransactionCategory.POST_CREATE -> Category.POST + TransactionCategory.SHOP_PURCHASE -> Category.PURCHASE + TransactionCategory.P2P_TRANSFER -> Category.TRANSFER + TransactionCategory.COMMENT -> Category.COMMENT + TransactionCategory.LIKE -> Category.LIKE + TransactionCategory.DISLIKE -> Category.DISLIKE + TransactionCategory.AD_PINNED -> Category.AD + TransactionCategory.UNKNOWN__ -> Category.DEFAULT + } +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Fee.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Fee.kt new file mode 100644 index 000000000..19885c12a --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Fee.kt @@ -0,0 +1,14 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Fees +import wallet.wallet.eu.peernetwork.wallet.remote.TransactionHistoryQuery +import java.math.BigDecimal + +fun TransactionHistoryQuery.Fees.mapFromDomain(): Fees { + return Fees( + total = total.toBigDecimalOrNull() ?: BigDecimal.ZERO, + burn = burn.toBigDecimalOrNull() ?: BigDecimal.ZERO, + peer = peer.toBigDecimalOrNull() ?: BigDecimal.ZERO, + commission = inviter?.toBigDecimalOrNull() ?: BigDecimal.ZERO, + ) +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Filter.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Filter.kt new file mode 100644 index 000000000..d18c3290a --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Filter.kt @@ -0,0 +1,17 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Filter +import eu.peernetwork.wallet.domain.model.Transaction +import wallet.type.TokenMovementFilterType + +fun Filter.mapToType(): TokenMovementFilterType? { + return type?.let { + when(it) { + Transaction.Type.TRANSACTION -> TokenMovementFilterType.TRANSACTION + Transaction.Type.AIRDROP -> TokenMovementFilterType.AIRDROP + Transaction.Type.PAYMENT -> TokenMovementFilterType.PAYMENT + Transaction.Type.BURN -> TokenMovementFilterType.BURN + Transaction.Type.MINT -> TokenMovementFilterType.MINT + } + } +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Sort.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Sort.kt new file mode 100644 index 000000000..1f194e45e --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Sort.kt @@ -0,0 +1,11 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Sort +import wallet.type.SortFilterType + +fun Sort.mapFromDomain(): SortFilterType? { + return when(this) { + Sort.NEWEST -> SortFilterType.NEWEST + Sort.OLDEST -> SortFilterType.OLDEST + } +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Status.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Status.kt new file mode 100644 index 000000000..87073ebea --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Status.kt @@ -0,0 +1,13 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Status +import wallet.type.ContentVisibilityStatus + +fun ContentVisibilityStatus.mapToDomain(): Status { + return when (this.rawValue) { + ContentVisibilityStatus.ILLEGAL.rawValue -> Status.ILLEGAL + ContentVisibilityStatus.HIDDEN.rawValue -> Status.HIDDEN + else -> Status.VISIBLE + } +} + diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Transaction.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Transaction.kt new file mode 100644 index 000000000..99e303e56 --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Transaction.kt @@ -0,0 +1,31 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.Amount +import eu.peernetwork.wallet.domain.model.Category +import eu.peernetwork.wallet.domain.model.Transaction +import wallet.wallet.eu.peernetwork.wallet.remote.TransactionHistoryQuery +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +fun TransactionHistoryQuery.AffectedRow.mapToDomain(): Transaction { + return Transaction( + id = transactionId, + sender = sender.mapToDomain(), + recipient = recipient.mapToDomain(), + message = message, + category = transactionCategory?.mapFromDomain() ?: Category.DEFAULT, + amount = Amount( + net = netTokenAmount.mapToAmount(), + gross = tokenamount.mapToAmount(), + ), + fees = fees?.mapFromDomain(), + createdAt = createdat.toTimestamp() + ) +} + +fun String.toTimestamp(pattern: String = "yyyy-MM-dd HH:mm:ss.SSSSSS"): Long { + return (SimpleDateFormat(pattern, Locale.getDefault()).parse(this)?.time ?: 0L).run { + this + TimeZone.getDefault().getOffset(this) + } +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/User.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/User.kt new file mode 100644 index 000000000..bdac5a7d8 --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/User.kt @@ -0,0 +1,26 @@ +package eu.peernetwork.wallet.remote.mapper + +import eu.peernetwork.wallet.domain.model.User +import wallet.wallet.eu.peernetwork.wallet.remote.TransactionHistoryQuery + +fun TransactionHistoryQuery.Sender.mapToDomain(): User { + return User( + id = userid ?: "", + slug = slug ?: 0, + username = username ?: "", + imageUrl = img ?: "", + status = visibilityStatus.mapToDomain(), + isAccessible = !isHiddenForUsers, + ) +} + +fun TransactionHistoryQuery.Recipient.mapToDomain(): User { + return User( + id = userid ?: "", + slug = slug ?: 0, + username = username ?: "", + imageUrl = img ?: "", + status = visibilityStatus.mapToDomain(), + isAccessible = !isHiddenForUsers, + ) +} diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Wallet.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Wallet.kt index d4712ea4b..52e719aad 100644 --- a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Wallet.kt +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Wallet.kt @@ -2,4 +2,13 @@ package eu.peernetwork.wallet.remote.mapper import java.math.BigDecimal -fun Any?.toBigDecimalOrNull(): BigDecimal? = BigDecimal(this.toString()) +fun Any?.toBigDecimalOrNull(): BigDecimal? { + if (this == null) return null + val value = this.toString().trim() + if (value.isEmpty() + || value.equals("null", ignoreCase = true) + || value.equals("nan", ignoreCase = true)) { + return null + } + return runCatching { BigDecimal(value) }.getOrNull() +} diff --git a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt index 97e6d6d61..88bac773a 100644 --- a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt +++ b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt @@ -27,7 +27,7 @@ internal class TransferApiDelegateTest { fun setup() { api = TransferApiDelegate(object : RequestClient { override fun invoke(): ApolloClient = client - }) + }, url = "http:localhost") } @Test diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceError.kt new file mode 100644 index 000000000..9b69935ca --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceError.kt @@ -0,0 +1,108 @@ +package eu.peernetwork.wallet.ui.balance + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.R +import eu.peernetwork.core.ui.theme.DesignTheme + +@Composable +fun BalanceError( + error: State, + component: Balance.Component, + onRefresh: () -> Unit +) { + error.value?.message?.let { + BalanceError( + error = component.resource().string(it), + onRefresh = onRefresh + ) + } +} + +@Composable +@SuppressLint("UnusedBoxWithConstraintsScope") +fun BalanceError( + error: String, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(24.dp), + onRefresh: () -> Unit +) { + val handleRefresh by rememberUpdatedState(onRefresh) + BoxWithConstraints { + val vertical = with(LocalDensity.current) { maxHeight.toPx() * .15f } + val horizontal = with(LocalDensity.current) { maxHeight.toPx() * .1f } + val verticalGradient = Brush.linearGradient( + start = Offset(0f, vertical), + colors = listOf( + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.primary.copy(alpha = .5f) + ), + end = Offset(horizontal, Float.POSITIVE_INFINITY) + ) + Row( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .background(brush = verticalGradient) + .padding(contentPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = error, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = onRefresh) { + Icon( + painter = painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.retry_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(start = 16.dp) + .size(36.dp) + .clickable { handleRefresh() }, + ) + } + } + } +} + +@Preview +@Composable +fun PreviewBalanceError() { + DesignTheme(isDarkMode = true) { + BalanceError( + error = "An unexpected error occurred while processing your payment. Please try again." + ) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt new file mode 100644 index 000000000..c3fa1108c --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt @@ -0,0 +1,64 @@ +package eu.peernetwork.wallet.ui.balance + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.design.luna.DesignStream +import eu.peernetwork.core.ui.design.luna.DesignStreamState +import java.math.RoundingMode + +@Composable +@Suppress("UNCHECKED_CAST") +fun BalanceOverview( + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, +) { + BalanceScreen( + provider = provider, + viewModelStoreOwner = viewModelStoreOwner, + ) { component, viewModel -> + val state by viewModel.state.collectAsStateWithLifecycle() + val status by viewModel.status.collectAsStateWithLifecycle() + val derivedState = remember { + derivedStateOf { + when (state) { + BalanceViewModel.State.Empty -> DesignStreamState.Default + BalanceViewModel.State.Loading -> DesignStreamState.Loading + is BalanceViewModel.State.Success -> { + DesignStreamState.Success( + (state as BalanceViewModel.State.Success).wallet + ) + } + is BalanceViewModel.State.Error -> DesignStreamState.Error( + (state as BalanceViewModel.State.Error).error + ) + } + } + } + DesignStream( + state = derivedState, + loading = { BalanceSkeleton() }, + error = { BalanceError(it, component) { viewModel() } } + ) { + val balance = remember { derivedStateOf { + it.value.balance.setScale(4, RoundingMode.HALF_UP) + } } + BalancePreview(balance.value.toString()) + } + LaunchedEffect(lastUpdated.value) { + (status as? BalanceViewModel.Status.Success?)?.let { + if (it.data != lastUpdated.value) { + viewModel() + } + } ?: viewModel() + viewModel.updatedAt(lastUpdated.value) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt new file mode 100644 index 000000000..f0843012e --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt @@ -0,0 +1,85 @@ +package eu.peernetwork.wallet.ui.balance + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R + +@Composable +@SuppressLint("UnusedBoxWithConstraintsScope") +fun BalancePreview( + balance: String, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(24.dp), +) { + BoxWithConstraints { + val vertical = with(LocalDensity.current) { maxHeight.toPx() * .15f } + val horizontal = with(LocalDensity.current) { maxHeight.toPx() * .1f } + val verticalGradient = Brush.linearGradient( + start = Offset(0f, vertical), + colors = listOf( + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.primary.copy(alpha = .5f) + ), + end = Offset(horizontal, Float.POSITIVE_INFINITY) + ) + Row( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .background(brush = verticalGradient) + .padding(contentPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = balance, + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onBackground, + textAlign = TextAlign.Center, + ) + Icon( + painter = painterResource(id = R.drawable.ic_icon), + contentDescription = stringResource(eu.peernetwork.core.ui.R.string.wallet_label), + modifier = Modifier.padding(start = 4.dp) + .size(42.dp), + tint = MaterialTheme.colorScheme.onBackground + ) + } + } +} + +@Preview +@Composable +fun PreviewServicePage() { + DesignTheme(isDarkMode = true) { + BalancePreview( + balance = "2 234" + ) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceScreen.kt index d5e8459a8..aa32c4cf7 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceScreen.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceScreen.kt @@ -19,12 +19,9 @@ import eu.peernetwork.wallet.ui.model.UiWallet @Composable fun BalanceScreen( - lastUpdated: State, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, - loading: @Composable () -> Unit = {}, - error: @Composable (error: State) -> Unit = {}, - content: @Composable (State) -> Unit + content: @Composable (Balance.Component, BalanceViewModel) -> Unit ) { val context = LocalContext.current val component = remember { @@ -35,32 +32,50 @@ fun BalanceScreen( viewModelStoreOwner = viewModelStoreOwner, factory = component.viewModelFactory() ) - val state by viewModel.state.collectAsStateWithLifecycle() - val derivedState = remember { - derivedStateOf { - when (state) { - BalanceViewModel.State.Empty -> DesignStreamState.Default - BalanceViewModel.State.Loading -> DesignStreamState.Loading - is BalanceViewModel.State.Success -> { - DesignStreamState.Success( - (state as BalanceViewModel.State.Success).wallet + val updatedContent by rememberUpdatedState(content) + updatedContent(component, viewModel) +} + +@Composable +fun BalanceScreen( + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + loading: @Composable () -> Unit = {}, + error: @Composable (error: State) -> Unit = {}, + content: @Composable (State) -> Unit +) { + BalanceScreen( + provider = provider, + viewModelStoreOwner = viewModelStoreOwner + ) { component, viewModel -> + val state by viewModel.state.collectAsStateWithLifecycle() + val derivedState = remember { + derivedStateOf { + when (state) { + BalanceViewModel.State.Empty -> DesignStreamState.Default + BalanceViewModel.State.Loading -> DesignStreamState.Loading + is BalanceViewModel.State.Success -> { + DesignStreamState.Success( + (state as BalanceViewModel.State.Success).wallet + ) + } + is BalanceViewModel.State.Error -> DesignStreamState.Error( + (state as BalanceViewModel.State.Error).error ) } - is BalanceViewModel.State.Error -> DesignStreamState.Error( - (state as BalanceViewModel.State.Error).error - ) } } - } - val updatedContent by rememberUpdatedState(content) - DesignStream( - state = derivedState, - loading = loading, - error = error - ) { updatedContent(it) } - LaunchedEffect(lastUpdated.value) { - if (state !is BalanceViewModel.State.Success) { - viewModel() + val updatedContent by rememberUpdatedState(content) + DesignStream( + state = derivedState, + loading = loading, + error = error + ) { updatedContent(it) } + LaunchedEffect(lastUpdated.value) { + if (state is BalanceViewModel.State.Empty) { + viewModel() + } } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceSkeleton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceSkeleton.kt new file mode 100644 index 000000000..2a213f9a7 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceSkeleton.kt @@ -0,0 +1,76 @@ +package eu.peernetwork.wallet.ui.balance + +import android.annotation.SuppressLint +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignSkeleton +import eu.peernetwork.core.ui.theme.DesignTheme + +@Composable +@SuppressLint("UnusedBoxWithConstraintsScope") +fun BalanceSkeleton( + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(28.dp), +) { + BoxWithConstraints { + val vertical = with(LocalDensity.current) { maxHeight.toPx() * .15f } + val horizontal = with(LocalDensity.current) { maxHeight.toPx() * .1f } + val verticalGradient = Brush.linearGradient( + start = Offset(0f, vertical), + colors = listOf( + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.surfaceDim, + MaterialTheme.colorScheme.primary.copy(alpha = .5f) + ), + end = Offset(horizontal, Float.POSITIVE_INFINITY) + ) + Row( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .background(brush = verticalGradient) + .padding(contentPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + DesignSkeleton( + modifier.fillMaxWidth(fraction = .4f) + .height(24.dp), + color = MaterialTheme.colorScheme.onBackground, + ) + DesignSkeleton( + modifier.padding(start = 16.dp) + .size(36.dp) + .height(36.dp), + color = MaterialTheme.colorScheme.onBackground, + ) + } + } +} + +@Preview +@Composable +fun PreviewBalanceSkeleton() { + DesignTheme(isDarkMode = true) { + BalanceSkeleton() + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceViewModel.kt index 5728a0fa3..c99996fe8 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceViewModel.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceViewModel.kt @@ -16,8 +16,12 @@ class BalanceViewModel @Inject constructor( ) : ViewModel() { private val _state = MutableStateFlow(State.Empty) + private val _status = MutableStateFlow(Status.Empty) + val state: StateFlow = _state.asStateFlow() + val status: StateFlow = _status.asStateFlow() + operator fun invoke() { viewModelScope.launch { _state.tryEmit(State.Loading) @@ -29,6 +33,19 @@ class BalanceViewModel @Inject constructor( } } + fun updatedAt(timestamp: Long) { + viewModelScope.launch { + _status.tryEmit(Status.Success(timestamp)) + } + } + + sealed interface Status { + data object Empty : Status + data object Loading : Status + data class Success(val data: T) : Status + data class Error(val error: Throwable) : Status + } + sealed interface State { data object Empty : State data object Loading : State diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/Service.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt similarity index 58% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/Service.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt index 1105c702a..d120cd7bb 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/Service.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import android.content.Context import androidx.lifecycle.ViewModelProvider @@ -8,23 +8,23 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.wallet.ui.provider.WalletProvider import eu.peernetwork.wallet.ui.transfer.Transfer -interface Service : WalletProvider, CoreProvider { +interface Dashboard : WalletProvider, CoreProvider { @javax.inject.Scope @Retention(AnnotationRetention.RUNTIME) annotation class Scope @Scope @dagger.Component( - dependencies = [ Service::class ], - modules = [ ServiceModule::class ] + dependencies = [ Dashboard::class ], + modules = [ DashboardModule::class ] ) - interface Component : Service, Transfer, UiComponentProvider { + interface Component : Dashboard, Transfer, UiComponentProvider { fun viewModelFactory(): ViewModelProvider.Factory } - class Builder(private val dependency: Service): UiComponent.DefaultBuilder() { + class Builder(private val dependency: Dashboard): UiComponent.DefaultBuilder() { override fun build(context: Context): Component { - return DaggerService_Component.builder().service(dependency).build() + return DaggerDashboard_Component.builder().dashboard(dependency).build() } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt similarity index 95% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceError.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt index 914c11451..1ee8ca600 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceError.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import android.content.res.Configuration import androidx.compose.foundation.background @@ -24,14 +24,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.peernetwork.core.common.interactor.ResourceInteractor import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.design.compose.DesignErrorText +import eu.peernetwork.core.ui.design.material.DesignErrorText import eu.peernetwork.core.ui.design.luna.DesignAvatar import eu.peernetwork.core.ui.design.material.DesignCard import eu.peernetwork.core.ui.design.material.DesignDetailLayout import eu.peernetwork.core.ui.theme.PeerTheme @Composable -fun ServiceError( +fun DashboardError( error: Throwable, resource: ResourceInteractor, onRetry: () -> Unit @@ -95,6 +95,6 @@ fun PreviewErrorLabel() { override fun getBaseUrl(): String = "" override fun string(key: String): String = key } } - ServiceError(RuntimeException(), resource) {} + DashboardError(RuntimeException(), resource) {} } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt similarity index 77% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceModule.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt index e65219772..109a4cc09 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceModule.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -15,16 +15,16 @@ import eu.peernetwork.wallet.ui.transfer.Transfer import javax.inject.Provider @Module -object ServiceModule { +object DashboardModule { @Provides - @Service.Scope + @Dashboard.Scope fun provideBuilderFactory(factory: Map, @JvmSuppressWildcards Provider>): UiComponentProvider.Factory { return UiBuilderFactory(factory) } @Provides - @Service.Scope + @Dashboard.Scope fun provideViewModelFactory( classToViewModel: @JvmSuppressWildcards Map, Provider> ): ViewModelProvider.Factory { @@ -33,15 +33,15 @@ object ServiceModule { @Provides @IntoMap - @Service.Scope - @UiViewModel(ServiceViewModel::class) - fun viewModel(viewModel: ServiceViewModel): ViewModel = viewModel + @Dashboard.Scope + @UiViewModel(DashboardViewModel::class) + fun viewModel(viewModel: DashboardViewModel): ViewModel = viewModel @Provides - @Service.Scope + @Dashboard.Scope @IntoMap @UiBuilder(Transfer.Builder::class) - fun provideTransferBuilder(component: Service.Component): UiComponent.Builder { + fun provideTransferBuilder(component: Dashboard.Component): UiComponent.Builder { return Transfer.Builder(component) } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServicePage.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt similarity index 83% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServicePage.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt index eb61551aa..2495308d2 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServicePage.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt @@ -1,6 +1,5 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard -import android.content.res.Configuration import androidx.compose.animation.Crossfade import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -31,16 +30,16 @@ import eu.peernetwork.wallet.ui.model.UiRecipient import java.util.UUID @Composable -fun ServicePage( +fun DashboardPage( name: String, - state: State, + state: State, onClick: () -> Unit, - content: @Composable (ServiceState.Transfer) -> Unit + content: @Composable (DashboardState.Transfer) -> Unit ) { val updateContent by rememberUpdatedState(content) Crossfade(state.value) { target -> when(target) { - ServiceState.Default -> { + DashboardState.Default -> { DesignOutlineButton( onClick = onClick, minHeight = 42.dp, @@ -53,29 +52,13 @@ fun ServicePage( } ) } - is ServiceState.Transfer -> { updateContent(target) } + is DashboardState.Transfer -> { updateContent(target) } } } } +@Preview @Composable -fun ServicePage() { - DesignCard( - color = MaterialTheme.colorScheme.surfaceDim, - contentPadding = PaddingValues(16.dp), - shape = RoundedCornerShape(24.dp) - ) { - DesignAvatar { - Box(modifier = Modifier - .size(36.dp) - .background(MaterialTheme.colorScheme.background), - ) - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) fun PreviewServicePage() { DesignTheme(isDarkMode = true) { Column { @@ -85,19 +68,35 @@ fun PreviewServicePage() { username = "johnDoe", imageUrl = "http://localhost" ) - ServicePage( + DashboardPage( "ServiceScreen", - remember { mutableStateOf(ServiceState.Transfer(recipient)) }, + remember { mutableStateOf(DashboardState.Transfer(recipient)) }, {} ) { - ServicePage() + DashboardPage() } Spacer(modifier = Modifier.height(16.dp)) - ServicePage( + DashboardPage( "ServiceScreen", - remember { mutableStateOf(ServiceState.Default) }, + remember { mutableStateOf(DashboardState.Default) }, {} ) {} } } } + +@Composable +fun DashboardPage() { + DesignCard( + color = MaterialTheme.colorScheme.surfaceDim, + contentPadding = PaddingValues(16.dp), + shape = RoundedCornerShape(24.dp) + ) { + DesignAvatar { + Box(modifier = Modifier + .size(36.dp) + .background(MaterialTheme.colorScheme.background), + ) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt similarity index 60% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceScreen.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt index 26e3c1120..5ffa3f4ec 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceScreen.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -13,44 +13,44 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.luna.DesignStreamState import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.wallet.ui.model.UiRecipient import eu.peernetwork.wallet.ui.model.UiTax -sealed interface ServiceState { - data object Default : ServiceState - data class Transfer(val recipient: UiRecipient) : ServiceState +sealed interface DashboardState { + data object Default : DashboardState + data class Transfer(val recipient: UiRecipient) : DashboardState } @Composable -fun ServiceScreen( +fun DashboardScreen( provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, content: @Composable (State>, () -> Unit) -> Unit ) { val context = LocalContext.current val component = remember { - provider.builder(Service.Builder::class.java).build(context) + provider.builder(Dashboard.Builder::class.java).build(context) } val viewModel = viewModel( - modelClass = ServiceViewModel::class.java, + modelClass = DashboardViewModel::class.java, viewModelStoreOwner = viewModelStoreOwner, factory = component.viewModelFactory() ) val state by viewModel.state.collectAsStateWithLifecycle() val derivedState = remember { derivedStateOf { when(state) { - ServiceViewModel.State.Empty -> DesignStreamState.Default - ServiceViewModel.State.Loading -> DesignStreamState.Loading - is ServiceViewModel.State.Success -> { - val data = (state as ServiceViewModel.State.Success) + DashboardViewModel.State.Empty -> DesignStreamState.Default + DashboardViewModel.State.Loading -> DesignStreamState.Loading + is DashboardViewModel.State.Success -> { + val data = (state as DashboardViewModel.State.Success) DesignStreamState.Success(data.tax) } - is ServiceViewModel.State.Error -> { - DesignStreamState.Error((state as ServiceViewModel.State.Error).error) + is DashboardViewModel.State.Error -> { + DesignStreamState.Error((state as DashboardViewModel.State.Error).error) } } } } @@ -58,15 +58,15 @@ fun ServiceScreen( val handleRefresh by rememberUpdatedState { viewModel.initialize() } updatedContent(derivedState, handleRefresh) LaunchedEffect(Unit) { - if (state is ServiceViewModel.State.Empty) { + if (state is DashboardViewModel.State.Empty) { viewModel.initialize() } } } @Composable -fun ServiceScreen( - serviceState: MutableState, +fun DashboardScreen( + dashboardState: MutableState, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, onAccountClicked: (String) -> Unit, @@ -75,36 +75,36 @@ fun ServiceScreen( ) { val context = LocalContext.current val component = remember { - provider.builder(Service.Builder::class.java).build(context) + provider.builder(Dashboard.Builder::class.java).build(context) } val viewModel = viewModel( - modelClass = ServiceViewModel::class.java, + modelClass = DashboardViewModel::class.java, viewModelStoreOwner = viewModelStoreOwner, factory = component.viewModelFactory() ) val state by viewModel.state.collectAsStateWithLifecycle() val derivedState = remember { derivedStateOf { when(state) { - ServiceViewModel.State.Empty -> DesignStatefulScaffoldState.Empty - ServiceViewModel.State.Loading -> DesignStatefulScaffoldState.Loading - is ServiceViewModel.State.Success -> { - val data = (state as ServiceViewModel.State.Success) + DashboardViewModel.State.Empty -> DesignStatefulScaffoldState.Empty + DashboardViewModel.State.Loading -> DesignStatefulScaffoldState.Loading + is DashboardViewModel.State.Success -> { + val data = (state as DashboardViewModel.State.Success) DesignStatefulScaffoldState.Success(data.tax) } - is ServiceViewModel.State.Error -> { - DesignStatefulScaffoldState.Error((state as ServiceViewModel.State.Error).error) + is DashboardViewModel.State.Error -> { + DesignStatefulScaffoldState.Error((state as DashboardViewModel.State.Error).error) } } } } DesignStatefulScaffold( derivedState, onRefresh = { viewModel.initialize() }, - placeholder = { ServicePage() }, - errorContent = { ServiceError(it, component.resource()) { viewModel.initialize() } } + placeholder = { DashboardPage() }, + errorContent = { DashboardError(it, component.resource()) { viewModel.initialize() } } ) { - ServiceTransfer( + DashboardTransfer( tax = ((it.burn + it.peer) * 100).toInt(), - serviceState, + dashboardState, component, viewModelStoreOwner, onAccountClicked, diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceTransfer.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt similarity index 84% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceTransfer.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt index 71c965d7b..1dc686c80 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceTransfer.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -16,10 +16,10 @@ import eu.peernetwork.wallet.ui.transfer.TransferScreen import eu.peernetwork.wallet.ui.transfer.TransferSheet @Composable -fun ServiceTransfer( +fun DashboardTransfer( tax: Int, - state: MutableState, - component: Service.Component, + state: MutableState, + component: Dashboard.Component, viewModelStoreOwner: ViewModelStoreOwner, onAccountClicked: (String) -> Unit, onClear: () -> Unit, @@ -34,13 +34,13 @@ fun ServiceTransfer( stringResource(R.string.transfer_label), showTransfer, { - if (lastState.value is ServiceState.Default && !it) { + if (lastState.value is DashboardState.Default && !it) { handleOnClear() } state.value = lastState.value } ) { - ServicePage( + DashboardPage( stringResource(R.string.recipient_selection_label), state, onClick, @@ -51,13 +51,13 @@ fun ServiceTransfer( provider = component, viewModelStoreOwner = viewModelStoreOwner, onClear = { - lastState.value = ServiceState.Default + lastState.value = DashboardState.Default showTransfer.value = false }, onTransfer = { transaction.value = it } ) { handleOnAccountClicked(it.id) } } } - (state.value as? ServiceState.Transfer?)?.let { + (state.value as? DashboardState.Transfer?)?.let { TransferSheet( transaction, component, @@ -65,7 +65,7 @@ fun ServiceTransfer( it.recipient, { transaction.value = null - lastState.value = ServiceState.Default + lastState.value = DashboardState.Default showTransfer.value = false } ) { handleOnAccountClicked(it.id) } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt similarity index 92% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModel.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt index 94f540d32..f3f424534 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModel.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject -class ServiceViewModel @Inject constructor( +class DashboardViewModel @Inject constructor( private val usecase: TaxUsecase ) : ViewModel() { private val mutableState = MutableStateFlow(State.Empty) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/TransactionsNavigator.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/TransactionsNavigator.kt new file mode 100644 index 000000000..c29bcca1a --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/TransactionsNavigator.kt @@ -0,0 +1,12 @@ +package eu.peernetwork.wallet.ui.extension + +import eu.peernetwork.core.ui.design.luna.DesignRichText +import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator + +fun DesignRichText.route(value: String): TransactionsNavigator.Route { + return when(this) { + is DesignRichText.Link -> TransactionsNavigator.Route.Link(value) + is DesignRichText.Tag -> TransactionsNavigator.Route.Search("tag", value) + is DesignRichText.Mention -> TransactionsNavigator.Route.Search("username", value) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt new file mode 100644 index 000000000..da3bbecf1 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt @@ -0,0 +1,12 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.wallet.domain.model.Amount +import eu.peernetwork.wallet.ui.model.UiAmount +import java.math.RoundingMode + +fun Amount.mapFromDomain(): UiAmount { + return UiAmount( + net = net.setScale(2, RoundingMode.HALF_UP), + gross = gross.setScale(2, RoundingMode.HALF_UP) + ) +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt new file mode 100644 index 000000000..42f1c4081 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt @@ -0,0 +1,48 @@ +package eu.peernetwork.wallet.ui.mapper + +import androidx.annotation.StringRes +import eu.peernetwork.wallet.domain.model.Category +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.model.UiCategory + +fun Category.mapFromDomain(): UiCategory { + return when (this) { + Category.AD -> UiCategory.AD + Category.MINT -> UiCategory.MINT + Category.POST -> UiCategory.POST + Category.PURCHASE -> UiCategory.PURCHASE + Category.TRANSFER -> UiCategory.TRANSFER + Category.COMMENT -> UiCategory.COMMENT + Category.LIKE -> UiCategory.LIKE + Category.DISLIKE -> UiCategory.DISLIKE + Category.DEFAULT -> UiCategory.DEFAULT + } +} + +@StringRes +fun Category.toResource(): Int? { + return when (this) { + Category.AD -> R.string.pinned_post_transaction_label + Category.MINT -> R.string.mint_transaction_label + Category.POST -> R.string.post_transaction_label + Category.PURCHASE -> R.string.purchase_transaction_label + Category.TRANSFER -> null + Category.COMMENT -> R.string.comment_transaction_label + Category.LIKE -> R.string.like_transaction_label + Category.DISLIKE -> R.string.dislike_transaction_label + Category.DEFAULT -> R.string.default_transaction_label + } +} + +@StringRes +fun Category.toIcon(): Int? { + return when (this) { + Category.AD -> R.drawable.ic_pin + Category.POST -> R.drawable.ic_transaction_post + Category.TRANSFER -> null + Category.COMMENT -> R.drawable.ic_comment + Category.LIKE -> R.drawable.ic_love + Category.DISLIKE -> R.drawable.ic_hate + else -> R.drawable.ic_mint + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt new file mode 100644 index 000000000..aec05d153 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt @@ -0,0 +1,14 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.wallet.domain.model.Fees +import eu.peernetwork.wallet.ui.model.UiFees +import java.math.RoundingMode + +fun Fees.mapFromDomain(): UiFees { + return UiFees( + total = total.setScale(2, RoundingMode.HALF_UP), + burn = burn.setScale(2, RoundingMode.HALF_UP), + peer = peer.setScale(2, RoundingMode.HALF_UP), + commission = commission.setScale(2, RoundingMode.HALF_UP) + ) +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Status.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Status.kt new file mode 100644 index 000000000..3670129e7 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Status.kt @@ -0,0 +1,12 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.wallet.domain.model.Status +import eu.peernetwork.wallet.ui.model.UiStatus + +fun Status.mapFromDomain(): UiStatus { + return when (this) { + Status.VISIBLE -> UiStatus.VISIBLE + Status.HIDDEN -> UiStatus.HIDDEN + Status.ILLEGAL -> UiStatus.ILLEGAL + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Tax.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Tax.kt index 23def2825..2d5bee519 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Tax.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Tax.kt @@ -5,9 +5,9 @@ import eu.peernetwork.wallet.ui.model.UiTax fun Tax.mapFromDomain(): UiTax { return UiTax( - burn = this.burn, - pool = this.pool, - peer = this.peer, - percentage = this.percentage + burn = burn, + pool = pool, + peer = peer, + percentage = percentage ) } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt new file mode 100644 index 000000000..de378ed35 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt @@ -0,0 +1,30 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.core.ui.mapper.annotate +import eu.peernetwork.wallet.domain.model.Transaction +import eu.peernetwork.wallet.ui.model.UiTax +import eu.peernetwork.wallet.ui.model.UiTransaction +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +fun Transaction.mapToTransaction(): UiTransaction { + return UiTransaction( + id = id, + res = category.toResource(), + icon = category.toIcon(), + sender = sender.mapFromDomain(), + recipient = recipient.mapFromDomain(), + message = message?.annotate(), + category = category.mapFromDomain(), + amount = amount.mapFromDomain(), + fees = fees?.mapFromDomain(), + tax = UiTax.Free, + createdAt = createdAt.toFormattedDate() + ) +} + +fun Long.toFormattedDate(): String { + val formatter = SimpleDateFormat("dd. MMM. yyyy, HH:mm:ss", Locale.getDefault()) + return formatter.format(Date(this)) +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/User.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/User.kt new file mode 100644 index 000000000..ee61346a1 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/User.kt @@ -0,0 +1,15 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.wallet.domain.model.User +import eu.peernetwork.wallet.ui.model.UiUser + +fun User.mapFromDomain(): UiUser { + return UiUser( + id = id, + slug = slug, + username = username, + imageUrl = imageUrl, + isAccessible = isAccessible, + status = status.mapFromDomain() + ) +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiAmount.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiAmount.kt new file mode 100644 index 000000000..dea469e55 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiAmount.kt @@ -0,0 +1,8 @@ +package eu.peernetwork.wallet.ui.model + +import java.math.BigDecimal + +data class UiAmount( + val net: BigDecimal, + val gross: BigDecimal, +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiCategory.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiCategory.kt new file mode 100644 index 000000000..6e62fd83b --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiCategory.kt @@ -0,0 +1,13 @@ +package eu.peernetwork.wallet.ui.model + +enum class UiCategory { + AD, + MINT, + POST, + PURCHASE, + TRANSFER, + COMMENT, + LIKE, + DISLIKE, + DEFAULT, +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiFees.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiFees.kt new file mode 100644 index 000000000..becd1cb43 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiFees.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.ui.model + +import java.math.BigDecimal + +data class UiFees( + val total: BigDecimal, + val burn: BigDecimal, + val peer: BigDecimal, + val commission: BigDecimal, +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiStatus.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiStatus.kt new file mode 100644 index 000000000..52f1cd663 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiStatus.kt @@ -0,0 +1,7 @@ +package eu.peernetwork.wallet.ui.model + +enum class UiStatus { + VISIBLE, + HIDDEN, + ILLEGAL, +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt index 4068df8e3..6a0ae63a1 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt @@ -5,4 +5,13 @@ data class UiTax( val pool: Double, val peer: Double, val percentage: Double -) +) { + companion object { + val Free = UiTax( + burn = 0.0, + pool = 0.0, + peer = 0.0, + percentage = 0.0 + ) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt new file mode 100644 index 000000000..1e4faaa15 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt @@ -0,0 +1,17 @@ +package eu.peernetwork.wallet.ui.model + +import androidx.compose.ui.text.AnnotatedString + +data class UiTransaction( + val id: String, + val res: Int?, + val icon: Int?, + val sender: UiUser, + val recipient: UiUser, + val category: UiCategory, + val amount: UiAmount, + val tax: UiTax, + val fees: UiFees?, + val message: AnnotatedString?, + val createdAt: String, +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiUser.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiUser.kt new file mode 100644 index 000000000..80cb5ed58 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiUser.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.ui.model + +data class UiUser( + val id: String, + val slug: Int, + val username: String, + val imageUrl: String, + val isAccessible: Boolean, + val status: UiStatus +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewError.kt deleted file mode 100644 index 2e6675728..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewError.kt +++ /dev/null @@ -1,69 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.common.interactor.ResourceInteractor -import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.design.compose.DesignErrorText -import eu.peernetwork.core.ui.exception.NoContentException -import eu.peernetwork.core.ui.theme.PeerTheme - -@Composable -fun OverviewError( - error: Throwable, - resource: ResourceInteractor, - modifier: Modifier = Modifier -) { - val errorMessage = stringResource(R.string.unknown_error_message) - val noContentMessage = stringResource(R.string.empty_message) - Box(modifier = modifier) { - OverviewScaffold( - rate = { - Box(modifier = Modifier.width(32.dp) - .height(12.dp) - .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(4.dp))) - }, - token = { - Box(modifier = Modifier.width(120.dp) - .height(16.dp) - .background(MaterialTheme.colorScheme.secondary, RoundedCornerShape(4.dp))) - } - ) { - DesignErrorText( - if (error is NoContentException) { - Throwable(noContentMessage, error) - } else { Throwable(resource.string(error.message ?: errorMessage), error) }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onBackground - ), - modifier = Modifier.padding(horizontal = 24.dp) - ) - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewOverviewError() { - PeerTheme { - val resource = remember { object : ResourceInteractor { - override fun getBaseUrl(): String = "" - override fun string(key: String): String = key - } } - OverviewError(RuntimeException(), resource) - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewModule.kt deleted file mode 100644 index a14eaa597..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoMap -import eu.peernetwork.core.ui.annotation.UiViewModel -import eu.peernetwork.core.ui.component.UiComponent -import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.factory.UiBuilderFactory -import eu.peernetwork.core.ui.factory.UiViewModelFactory -import javax.inject.Provider - -@Module -object OverviewModule { - @Provides - @Overview.Scope - fun provideBuilderFactory(factory: Map, - @JvmSuppressWildcards Provider>): UiComponentProvider.Factory { - return UiBuilderFactory(factory) - } - - @Provides - @Overview.Scope - fun provideViewModelFactory( - classToViewModel: - @JvmSuppressWildcards Map, Provider> - ): ViewModelProvider.Factory { - return UiViewModelFactory(classToViewModel) - } - - @Provides - @IntoMap - @Overview.Scope - @UiViewModel(OverviewViewModel::class) - fun viewModel(viewModel: OverviewViewModel): ViewModel = viewModel -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScaffold.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScaffold.kt deleted file mode 100644 index ff98cc462..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScaffold.kt +++ /dev/null @@ -1,131 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.wallet.ui.R - -@Composable -fun OverviewScaffold( - modifier: Modifier = Modifier, - rate: @Composable () -> Unit, - token: @Composable () -> Unit, - balance: @Composable () -> Unit, -) { - val updatedRate by rememberUpdatedState(rate) - val updatedToken by rememberUpdatedState(token) - val updatedBalance by rememberUpdatedState(balance) - val top = with(LocalDensity.current) { 280.dp.toPx() } - val verticalGradient = Brush.linearGradient( - start = Offset(top, 0f), - colors = listOf( - MaterialTheme.colorScheme.surfaceDim, - MaterialTheme.colorScheme.primary - ), - end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY) - ) - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceDim) - .background(brush = verticalGradient) - .padding(16.dp), - verticalArrangement = Arrangement.Top, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onBackground - ) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text(stringResource(R.string.wallet_title)) - Spacer(modifier = Modifier.weight(1f)) - updatedRate() - } - } - Spacer(modifier = Modifier.height(32.dp)) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth(), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_icon), - contentDescription = stringResource(eu.peernetwork.core.ui.R.string.wallet_label), - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onBackground - ) - updatedToken() - } - CompositionLocalProvider( - LocalTextStyle provides MaterialTheme.typography.bodySmall - ) { - updatedBalance() - } - Spacer(modifier = Modifier.height(36.dp)) - } -} - -@Composable -fun OverviewScaffold(modifier: Modifier = Modifier) { - Box(modifier = modifier) { - OverviewScaffold( - rate = { - Box(modifier = Modifier.width(32.dp) - .height(12.dp) - .background(MaterialTheme.colorScheme.onBackground, RoundedCornerShape(4.dp))) - }, - token = { - Box(modifier = Modifier.width(120.dp) - .height(16.dp) - .background(MaterialTheme.colorScheme.onBackground, RoundedCornerShape(4.dp))) - } - ) { - Box(modifier = Modifier.width(72.dp) - .height(12.dp) - .background(MaterialTheme.colorScheme.onBackground, RoundedCornerShape(4.dp))) - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewOverviewScaffold() { - DesignTheme(isDarkMode = true) { - OverviewScaffold() - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScreen.kt deleted file mode 100644 index 3cd3a5193..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewScreen.kt +++ /dev/null @@ -1,118 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import android.content.res.Configuration -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState -import eu.peernetwork.core.ui.extension.builder -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.core.ui.theme.PeerAppGreen -import eu.peernetwork.wallet.ui.model.UiWallet -import java.math.BigDecimal -import java.math.RoundingMode - -@Composable -fun OverviewScreen( - lastUpdated: State, - provider: UiComponentProvider, - viewModelStoreOwner: ViewModelStoreOwner, -) { - val context = LocalContext.current - val component = remember { - provider.builder(Overview.Builder::class.java).build(context) - } - val viewModel = viewModel( - modelClass = OverviewViewModel::class.java, - viewModelStoreOwner = viewModelStoreOwner, - factory = component.viewModelFactory() - ) - val state by viewModel.state.collectAsStateWithLifecycle() - val derivedState = remember { - derivedStateOf { - when (state) { - OverviewViewModel.State.Empty -> DesignStatefulScaffoldState.Empty - OverviewViewModel.State.Loading -> DesignStatefulScaffoldState.Loading - is OverviewViewModel.State.Success -> { - DesignStatefulScaffoldState.Success( - (state as OverviewViewModel.State.Success).wallet - ) - } - is OverviewViewModel.State.Error -> DesignStatefulScaffoldState.Error( - (state as OverviewViewModel.State.Error).error - ) - } - } - } - val updatedAt = remember { mutableLongStateOf(lastUpdated.value) } - DesignStatefulScaffold( - state = derivedState, - onRefresh = { viewModel.getBalance() }, - placeholder = { OverviewScaffold() }, - errorContent = { OverviewError(it, component.resource()) } - ) { OverviewScreen(it) } - LaunchedEffect(Unit) { viewModel.initialize() } - LaunchedEffect(lastUpdated.value) { - if (updatedAt.longValue != lastUpdated.value) { - viewModel.getBalance() - updatedAt.longValue = lastUpdated.value - } - } -} - -@Composable -fun OverviewScreen(wallet: UiWallet) { - OverviewScaffold( - rate = { - Text( - text = wallet.rate.toString(), - color = PeerAppGreen, - textAlign = TextAlign.Center, - ) - }, - token = { - Text( - text = wallet.balance.setScale(4, RoundingMode.HALF_UP).toString(), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center, - ) - } - ) { - Text( - text = "~ ${wallet.converted}${wallet.currency}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onBackground, - textAlign = TextAlign.Center - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewOverviewScreen() { - DesignTheme(isDarkMode = true) { - OverviewScreen( - UiWallet( - balance = BigDecimal(1000), - rate = 0.1f, - converted = BigDecimal(10), - currency = "$" - ) - ) - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModel.kt deleted file mode 100644 index c5183112a..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModel.kt +++ /dev/null @@ -1,49 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import eu.peernetwork.wallet.domain.usecase.ObservableOverviewUsecase -import eu.peernetwork.wallet.domain.usecase.OverviewUsecase -import eu.peernetwork.wallet.ui.mapper.mapFromDomain -import eu.peernetwork.wallet.ui.model.UiWallet -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import javax.inject.Inject - -class OverviewViewModel @Inject constructor( - private val usecase: OverviewUsecase, - private val observer: ObservableOverviewUsecase -) : ViewModel() { - private val mutableState = MutableStateFlow(State.Empty) - - val state: StateFlow = mutableState.asStateFlow() - - fun initialize() { - viewModelScope.launch { - observer().collectLatest { - mutableState.tryEmit(State.Success(it.mapFromDomain())) - } - } - } - - fun getBalance() { - viewModelScope.launch { - mutableState.tryEmit(State.Loading) - try { - mutableState.tryEmit(State.Success(usecase().mapFromDomain())) - } catch (error: Throwable) { - mutableState.tryEmit(State.Error(error)) - } - } - } - - sealed interface State { - data object Empty : State - data object Loading : State - data class Success(val wallet: UiWallet) : State - data class Error(val error: Throwable) : State - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/Overview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt similarity index 50% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/Overview.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt index 0a3724aa7..035a339f3 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/Overview.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt @@ -1,28 +1,29 @@ -package eu.peernetwork.wallet.ui.overview +package eu.peernetwork.wallet.ui.transactions import android.content.Context import androidx.lifecycle.ViewModelProvider import eu.peernetwork.core.common.provider.CoreProvider import eu.peernetwork.core.ui.component.UiComponent import eu.peernetwork.wallet.ui.provider.WalletProvider +import eu.peernetwork.wallet.ui.transfer.Transfer -interface Overview : WalletProvider, CoreProvider { +interface Transactions : WalletProvider, CoreProvider { @javax.inject.Scope @Retention(AnnotationRetention.RUNTIME) annotation class Scope @Scope @dagger.Component( - dependencies = [ Overview::class ], - modules = [ OverviewModule::class ] + dependencies = [ Transactions::class ], + modules = [ TransactionsModule::class ] ) - interface Component : Overview { + interface Component : Transactions, Transfer { fun viewModelFactory(): ViewModelProvider.Factory } - class Builder(private val dependency: Overview) : UiComponent.DefaultBuilder() { + class Builder(private val dependency: Transactions): UiComponent.DefaultBuilder() { override fun build(context: Context): Component { - return DaggerOverview_Component.builder().overview(dependency).build() + return DaggerTransactions_Component.builder().transactions(dependency).build() } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt new file mode 100644 index 000000000..41ed655e7 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt @@ -0,0 +1,173 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignAvatar +import eu.peernetwork.core.ui.design.luna.DesignImage +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppDarkRed +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.model.UiStatus +import eu.peernetwork.wallet.ui.model.UiUser +import kotlin.Boolean + +@Composable +fun TransactionsAvatar( + icon: Painter, + contentDescription: String? = null, + isRecipient: Boolean = false, + content: @Composable () -> Unit +) { + val updatedContent by rememberUpdatedState(content) + DesignAvatar( + icon = { + Icon( + painter = icon, + contentDescription = contentDescription, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(12.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + .padding(1.dp) + .graphicsLayer { + rotationZ = if (isRecipient) 180f else 0f + } + ) + }, + ) { updatedContent() } +} + +@Composable +fun TransactionsProfile( + user: UiUser, + isVisible: MutableState, + onClick: () -> Unit +) { + if (user.status == UiStatus.ILLEGAL) { + DesignAvatar { + Icon( + painter = painterResource(R.drawable.ic_report), + contentDescription = stringResource(R.string.illegal_content_description), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + .size(16.dp) + .clip(CircleShape) + ) + } + } else if (!user.isAccessible) { + if (isVisible.value) { + TransactionsProfile( + user = user, + onClick = onClick + ) + } else { + DesignAvatar { + Icon( + painter = painterResource(R.drawable.ic_hidden), + contentDescription = stringResource(R.string.hidden_content_description), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + .size(16.dp) + .clip(CircleShape) + .clickable { isVisible.value = true } + ) + } + } + } else { + TransactionsProfile( + user = user, + onClick = onClick + ) + } +} + +@Composable +fun TransactionsProfile( + user: UiUser, + onClick: () -> Unit +) { + DesignAvatar { + DesignImage( + label = user.username, + imageUrl = user.imageUrl, + size = 36.dp, + color = MaterialTheme.colorScheme.background, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onBackground + ), + modifier = Modifier.clickable( + role = Role.Button, + onClick = onClick + ) + ) + } +} + +@Composable +@Preview +fun PreviewTransactionsAvatar() { + DesignTheme(isDarkMode = true) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + val isVisible = remember { mutableStateOf(false) } + TransactionsAvatar( + icon = painterResource(R.drawable.ic_transfer_direction) + ) { + Icon( + painter = painterResource(R.drawable.ic_love), + contentDescription = null, + tint = PeerAppDarkRed, + modifier = Modifier.background(MaterialTheme.colorScheme.background) + .padding(8.dp) + ) + } + TransactionsProfile( + user = UiUser( + id = "123", + username = "test", + slug = 1234, + imageUrl = "http:localhost", + isAccessible = false, + status = UiStatus.ILLEGAL + ), + isVisible = isVisible, + ) {} + TransactionsProfile( + user = UiUser( + id = "123", + username = "test", + slug = 1234, + imageUrl = "http:localhost", + isAccessible = false, + status = UiStatus.VISIBLE + ), + isVisible = isVisible, + ) {} + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsError.kt new file mode 100644 index 000000000..a074a5df8 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsError.kt @@ -0,0 +1,88 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.R +import eu.peernetwork.core.ui.theme.DesignTheme + +@Composable +fun TransactionsError( + error: State, + component: Transactions.Component, + onRefresh: () -> Unit +) { + error.value?.message?.let { + TransactionsError( + error = component.resource().string(it), + onRefresh = onRefresh + ) + } +} + +@Composable +fun TransactionsError( + error: String, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(24.dp), + onRefresh: () -> Unit +) { + val handleRefresh by rememberUpdatedState(onRefresh) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(contentPadding) + ) { + Text( + text = error, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.weight(1f) + ) + IconButton(onClick = onRefresh) { + Icon( + painter = painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.retry_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(start = 16.dp) + .size(24.dp) + .clickable { handleRefresh() }, + ) + } + } +} + +@Composable +@Preview +fun PreviewTransactionsError() { + DesignTheme(isDarkMode = true) { + TransactionsError( + error = "An unexpected error occurred while processing your payment. Please try again.", + modifier = Modifier.padding(16.dp) + ) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt new file mode 100644 index 000000000..eb9170b27 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt @@ -0,0 +1,246 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignRichText +import eu.peernetwork.core.ui.extension.tap +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppDarkRed +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransactionsItem( + title: AnnotatedString, + description: AnnotatedString?, + createAt: String, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onProfileClicked: () -> Unit, + onMessageClicked: (DesignRichText, String) -> Unit, + leading: @Composable () -> Unit, + trailing: @Composable () -> Unit, + content: @Composable () -> Unit, +) { + val updatedLead by rememberUpdatedState(leading) + val updatedTrailing by rememberUpdatedState(trailing) + val updatedContent by rememberUpdatedState(content) + Column( + modifier = Modifier + .then(modifier) + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .clickable(onClick = onClick) + .padding(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + updatedLead() + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.tap(onProfileClicked) + ) + Text( + text = createAt, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline + ) + if (description != null + && description.isNotEmpty()) { + DesignRichText( + text = description, + maxLines = 1, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.SemiBold, + onClick = onMessageClicked + ) + } + } + updatedTrailing() + } + updatedContent() + } +} + +@Composable +fun TransactionsItem( + title: AnnotatedString, + description: AnnotatedString?, + price: String, + createAt: String, + expanded: MutableState, + onProfileClicked: () -> Unit, + onMessageClicked: (DesignRichText, String) -> Unit, + modifier: Modifier = Modifier, + leading: @Composable () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + val updatedContent by rememberUpdatedState(content) + TransactionsItem( + title = title, + description = description, + createAt = createAt, + onProfileClicked = onProfileClicked, + onMessageClicked = onMessageClicked, + modifier = modifier, + leading = leading, + trailing = { + TransactionsItemTrailing( + price = price, + isVisible = expanded + ) + }, + onClick = { expanded.value = !expanded.value } + ) { + AnimatedContent( + targetState = expanded.value, + transitionSpec = { + fadeIn(animationSpec = tween(300)) + + expandVertically( + animationSpec = tween(300), + expandFrom = Alignment.Top + ) togetherWith + fadeOut(animationSpec = tween(200)) + + shrinkVertically( + animationSpec = tween(200), + shrinkTowards = Alignment.Top + ) + }, + ) { visible -> + if (visible) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(top = 10.dp) + ) { + Box(modifier = Modifier.padding(bottom = 4.dp) + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colorScheme.surfaceContainerLow)) + updatedContent() + } + } + } + } +} + +@Composable +fun TransactionsItemTrailing( + price: String, + isVisible: State +) { + val rotation = animateFloatAsState( + targetValue = if (isVisible.value) -90f else 0f, + animationSpec = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ), + label = "TransactionsItemTrailingIndication" + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = price, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.SemiBold + ) + Icon( + painter = painterResource(R.drawable.ic_peer_token), + contentDescription = price, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(start = 4.dp) + .size(20.dp) + ) + Icon( + painter = painterResource(R.drawable.ic_caret_right), + contentDescription = null, + tint = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.size(16.dp) + .graphicsLayer { rotationZ = rotation.value } + ) + } +} + +@Composable +@Preview +fun PreviewTransactionsItem() { + DesignTheme(isDarkMode = true) { + val expanded = remember { mutableStateOf(false) } + TransactionsItem( + title = buildAnnotatedString { append("To @removed") }, + description = buildAnnotatedString { append("Hey! Thank you so much for all your help with the project presentation yesterday. I really appreciate how you stayed late to help me finalize the slides and practice the pitch. Your feedback was invaluable and I couldn't have done it without your support. The client loved it! Here's a little something to show my gratitude. Let's celebrate this weekend!") }, + price = "+534", + createAt = "10 Jun 2025, 04:20", + onProfileClicked = {}, + onMessageClicked = { _,_ -> }, + expanded = expanded, + leading = { + TransactionsAvatar( + icon = painterResource(R.drawable.ic_transfer_direction), + ) { + Icon( + painter = painterResource(R.drawable.ic_love), + contentDescription = null, + tint = PeerAppDarkRed, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(8.dp) + ) + } + }, + ) { + Box(modifier = Modifier + .fillMaxWidth() + .height(100.dp)) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt new file mode 100644 index 000000000..622f151f7 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt @@ -0,0 +1,132 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelStoreOwner +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.extension.route +import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator.Companion.LocalTransactionsNavigator + +@Composable +fun TransactionsList( + uuid: String, + limit: Int, + lastUpdated: State, + listState: LazyListState, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + onClick: (String) -> Unit +) { + val navigator = LocalTransactionsNavigator.current + val handleClick by rememberUpdatedState(onClick) + TransactionsScreen( + limit = limit, + lastUpdated = lastUpdated, + provider = provider, + viewModelStoreOwner = viewModelStoreOwner + ) { component, items -> + LazyColumn( + state = listState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 36.dp), + ) { + items( + count = items.itemCount, + key = { index -> items[index]?.id?.let { "$it;$index" } ?: index } + ) { index -> + items[index]?.let { transaction -> + val isVisible = rememberSaveable { mutableStateOf(false) } + val expanded = rememberSaveable { mutableStateOf(false) } + val isRecipient = transaction.recipient.id == uuid + val profile = if (isRecipient) { + transaction.sender + } else { + transaction.recipient + } + val title = transactionsUsername( + isVisible = isVisible, + isRecipient = isRecipient, + user = profile + ) + TransactionsItem( + title = transaction.res?.let { + buildAnnotatedString { append(stringResource(it)) } + } ?: title, + description = transaction.message, + createAt = transaction.createdAt, + price = "${transaction.amount.gross}", + expanded = expanded, + leading = { + TransactionsAvatar( + icon = painterResource(R.drawable.ic_transfer_direction), + contentDescription = transaction.res?.let { stringResource(it) } + ?: title.text, + isRecipient = isRecipient + ) { + if (transaction.icon != null && transaction.res != null) { + Icon( + painter = painterResource(transaction.icon), + contentDescription = stringResource(transaction.res), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(8.dp) + ) + } else { + TransactionsProfile( + user = profile, + isVisible = isVisible, + ) { handleClick(profile.id) } + } + } + }, + onProfileClicked = { + if (transaction.res == null) { + handleClick(profile.id) + } else { + expanded.value = !expanded.value + } + }, + onMessageClicked = { spec, value -> navigator.navigate(spec.route(value)) }, + modifier = Modifier.padding(bottom = 10.dp) + ) { + TransactionsSummeryItem( + title = stringResource(R.string.transaction_amount_label), + price = "${transaction.amount.net}", + ) + transaction.fees?.let { + val peer = (transaction.tax.peer * 100).toInt() + val burn = (transaction.tax.burn * 100).toInt() + TransactionsSummeryItem( + title = stringResource(R.string.platform_charge, "$peer"), + price = "${it.peer}", + ) + TransactionsSummeryItem( + title = stringResource(R.string.burn_charge, "$burn"), + price = "${it.burn}", + ) + } + } + } + } + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt new file mode 100644 index 000000000..0d4254061 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt @@ -0,0 +1,27 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import eu.peernetwork.core.ui.annotation.UiViewModel +import eu.peernetwork.core.ui.factory.UiViewModelFactory +import javax.inject.Provider + +@Module +object TransactionsModule { + @Provides + @Transactions.Scope + fun provideViewModelFactory( + classToViewModel: @JvmSuppressWildcards Map, Provider> + ): ViewModelProvider.Factory { + return UiViewModelFactory(classToViewModel) + } + + @Provides + @IntoMap + @Transactions.Scope + @UiViewModel(TransactionsViewModel::class) + fun viewModel(viewModel: TransactionsViewModel): ViewModel = viewModel +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsNavigator.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsNavigator.kt new file mode 100644 index 000000000..6cb9c5f4a --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsNavigator.kt @@ -0,0 +1,22 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.runtime.staticCompositionLocalOf + +interface TransactionsNavigator { + fun navigate(route: Route) + + sealed interface Route { + data class Search( + val type: String, + val query: String + ): Route + data class Link(val url: String): Route + data class Profile(val id: String): Route + } + + companion object Companion { + val LocalTransactionsNavigator = staticCompositionLocalOf { + error("${TransactionsNavigator::class.java.name} not provided") + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt new file mode 100644 index 000000000..858829fed --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt @@ -0,0 +1,73 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.paging.compose.LazyPagingItems +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.design.luna.DesignPagingStream +import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.core.ui.extension.builder +import eu.peernetwork.wallet.ui.model.UiTransaction + +@Composable +@Suppress("UNCHECKED_CAST") +fun TransactionsScreen( + limit: Int, + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + content: @Composable (Transactions.Component, LazyPagingItems) -> Unit +) { + val context = LocalContext.current + val component = remember { + provider.builder(Transactions.Builder::class.java).build(context) + } + val viewModel = viewModel( + modelClass = TransactionsViewModel::class.java, + viewModelStoreOwner = viewModelStoreOwner, + factory = component.viewModelFactory() + ) + val state by viewModel.state.collectAsStateWithLifecycle() + val status by viewModel.status.collectAsStateWithLifecycle() + val derivedState = remember { derivedStateOf { + when(state) { + TransactionsViewModel.State.Default -> DesignStreamState.Default + TransactionsViewModel.State.Loading -> DesignStreamState.Loading + is TransactionsViewModel.State.Success -> { + val data = (state as TransactionsViewModel.State.Success) + DesignStreamState.Success(data.content) + } + is TransactionsViewModel.State.Error -> { + DesignStreamState.Error((state as TransactionsViewModel.State.Error).error) + } + } + } } + val updatedContent by rememberUpdatedState(content) + DesignPagingStream( + state = derivedState, + loading = { TransactionsSkeleton(3) }, + error = { TransactionsError(it, component) { + viewModel(Pageable(0, limit)) + } } + ) { items -> + updatedContent(component, items) + } + LaunchedEffect(lastUpdated.value) { + (status as? TransactionsViewModel.Status.Success?)?.let { + if (it.data != lastUpdated.value) { + viewModel(Pageable(0, limit)) + } + } ?: viewModel(Pageable(0, limit)) + viewModel.updatedAt(lastUpdated.value) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSkeleton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSkeleton.kt new file mode 100644 index 000000000..05e50db27 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSkeleton.kt @@ -0,0 +1,60 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.theme.DesignTheme + +@Composable +fun TransactionsSkeleton(count: Int) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 36.dp), + ) { + items(count) { + TransactionsSkeleton( + modifier = Modifier.padding(bottom = 10.dp) + ) + } + } +} + +@Composable +fun TransactionsSkeleton(modifier: Modifier = Modifier) { + Row( + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + ) { + Box(modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + ) + } +} + +@Composable +@Preview +fun PreviewTransactionsSkeleton() { + DesignTheme(isDarkMode = true) { + TransactionsSkeleton(3) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt new file mode 100644 index 000000000..d76c63876 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt @@ -0,0 +1,79 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransactionsSummeryItem( + title: String, + price: String, + modifier: Modifier = Modifier, + priceColor: Color? = null, + color: Color = priceColor ?: MaterialTheme.colorScheme.outline, + style: TextStyle = MaterialTheme.typography.bodySmall +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + style = style, + modifier = Modifier.weight(1f), + color = color + ) + Text( + text = price, + style = style, + color = priceColor ?: MaterialTheme.colorScheme.onBackground, + fontWeight = priceColor?.let { FontWeight.Medium } ?: FontWeight.SemiBold + ) + Icon( + painter = painterResource(R.drawable.ic_peer_token), + contentDescription = stringResource(R.string.token_label), + tint = priceColor ?: MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(start = 2.dp) + .size(16.dp) + ) + } +} + +@Composable +@Preview +fun PreviewTransactionsSummery() { + DesignTheme(isDarkMode = true) { + Column( + modifier = Modifier.padding(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + TransactionsSummeryItem( + title = stringResource(R.string.transaction_amount_label), + price = "96", + ) + TransactionsSummeryItem( + title = stringResource(R.string.transaction_base_label), + style = MaterialTheme.typography.labelSmall, + price = "2", + priceColor = MaterialTheme.colorScheme.outline + ) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt new file mode 100644 index 000000000..7bb4f2460 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt @@ -0,0 +1,84 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.model.UiStatus +import eu.peernetwork.wallet.ui.model.UiUser + +@Composable +fun transactionsUsername( + user: UiUser, + isRecipient: Boolean, + isVisible: MutableState, +): AnnotatedString { + return buildAnnotatedString { + if (user.status == UiStatus.ILLEGAL) { + append(stringResource(R.string.illegal_username)) + } else if (!user.isAccessible) { + if (isVisible.value) { + Label(user, isRecipient) + } else { + append(stringResource(R.string.hidden_username)) + } + } else { + Label(user, isRecipient) + } + } +} + +@Composable +private fun AnnotatedString.Builder.Label( + user: UiUser, + isRecipient: Boolean +) { + append(if (isRecipient) { + stringResource(R.string.received_from, user.username) + } else { + stringResource(R.string.sent_to, user.username) + }) + append(" ") + withStyle(SpanStyle( + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.Medium, + fontSize = MaterialTheme.typography.labelMedium.fontSize + )) { + append("#${user.slug}") + } +} + +@Composable +@Preview +fun PreviewTransactionsUsername() { + DesignTheme(isDarkMode = true) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + val isVisible = remember { mutableStateOf(false) } + transactionsUsername( + isVisible = isVisible, + isRecipient = true, + user = UiUser( + id = "123", + username = "test", + slug = 1234, + imageUrl = "http:localhost", + isAccessible = false, + status = UiStatus.ILLEGAL + ), + ) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt new file mode 100644 index 000000000..495a6344c --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt @@ -0,0 +1,73 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.wallet.domain.model.Filter +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.ui.model.UiTransaction +import eu.peernetwork.wallet.ui.usecase.TransactionListingUsecase +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.launch +import javax.inject.Inject + +class TransactionsViewModel @Inject constructor( + private val usecase: TransactionListingUsecase +) : ViewModel() { + private val _state = MutableStateFlow(State.Default) + + private val _status = MutableStateFlow(Status.Empty) + + val state: StateFlow = _state.asStateFlow() + + val status: StateFlow = _status.asStateFlow() + + operator fun invoke( + page: Pageable, + filter: Filter = Filter.None, + sort: Sort = Sort.NEWEST, + ) { + viewModelScope.launch { + usecase( + param = TransactionListingUsecase.Parameter( + filter = filter, + sort = sort, + page = page + ) + ).catch { _state.tryEmit(State.Error(it)) } + .onStart { _state.tryEmit(State.Loading) } + .cachedIn(viewModelScope) + .apply { collectLatest { + _state.tryEmit(State.Success(this)) + } } + } + } + + fun updatedAt(timestamp: Long) { + viewModelScope.launch { + _status.tryEmit(Status.Success(timestamp)) + } + } + + sealed interface Status { + data object Empty : Status + data object Loading : Status + data class Success(val data: T) : Status + data class Error(val error: Throwable) : Status + } + + sealed interface State { + data object Default: State + data object Loading: State + data class Success(val content: Flow>): State + data class Error(val error: Throwable): State + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt index 342110c77..b0eac96cd 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.compose.DesignStatefulScaffoldState +import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState import eu.peernetwork.core.ui.design.material.DesignBottomSheetScaffold import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.core.ui.theme.DesignTheme diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt new file mode 100644 index 000000000..0b2977185 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt @@ -0,0 +1,81 @@ +package eu.peernetwork.wallet.ui.usecase + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource +import androidx.paging.PagingSource.LoadResult +import eu.peernetwork.core.common.paging.Pageable +import eu.peernetwork.core.ui.exception.NoContentException +import eu.peernetwork.core.ui.usecase.PagingUsecase +import eu.peernetwork.wallet.domain.model.Filter +import eu.peernetwork.wallet.domain.model.Sort +import eu.peernetwork.wallet.domain.model.Tax +import eu.peernetwork.wallet.domain.usecase.TaxUsecase +import eu.peernetwork.wallet.domain.usecase.TransactionsUsecase +import eu.peernetwork.wallet.ui.mapper.mapFromDomain +import eu.peernetwork.wallet.ui.mapper.mapToTransaction +import eu.peernetwork.wallet.ui.model.UiTransaction +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class TransactionListingUsecase @Inject constructor( + private val usecase: TransactionsUsecase, + private val taxUsecase: TaxUsecase, +) : PagingUsecase() { + private lateinit var param: Parameter + + private lateinit var tax: Tax + + override fun invoke(param: Parameter): Flow> { + this.param = param + return Pager( + config = PagingConfig( + pageSize = param.page.limit, + enablePlaceholders = false + ), + pagingSourceFactory = { source() } + ).flow + } + + override suspend fun getData( + params: PagingSource.LoadParams + ): LoadResult { + val currentOffset = params.key ?: param.page.offset + val currentPage = Pageable( + offset = currentOffset, + limit = param.page.limit + ) + val response = usecase( + param = TransactionsUsecase.Parameter( + filter = param.filter, + sort = param.sort, + page = currentPage + ) + ) + return if (response.items.isEmpty() && currentPage.offset == 0) { + LoadResult.Error(NoContentException()) + } else { + if (!this::tax.isInitialized) { + try { + tax = taxUsecase() + } catch (error: Throwable) { + return LoadResult.Error(error) + } + } + LoadResult.Page( + data = response.items.map { + it.mapToTransaction().copy(tax = tax.mapFromDomain()) + }, + prevKey = if (currentOffset <= 0) null else currentOffset - 1, + nextKey = if (response.items.isEmpty()) null else currentOffset + response.items.size + ) + } + } + + data class Parameter( + val filter: Filter, + val sort: Sort, + val page: Pageable + ) +} diff --git a/wallet/ui/src/main/res/drawable/ic_caret_right.xml b/wallet/ui/src/main/res/drawable/ic_caret_right.xml new file mode 100644 index 000000000..5141c4c0e --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_caret_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_hidden.xml b/wallet/ui/src/main/res/drawable/ic_hidden.xml new file mode 100644 index 000000000..cece5034e --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_hidden.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_mint.xml b/wallet/ui/src/main/res/drawable/ic_mint.xml new file mode 100644 index 000000000..8fd0ee999 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_mint.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_pin.xml b/wallet/ui/src/main/res/drawable/ic_pin.xml new file mode 100644 index 000000000..0a8643777 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_pin.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_report.xml b/wallet/ui/src/main/res/drawable/ic_report.xml new file mode 100644 index 000000000..7d4f947e5 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_report.xml @@ -0,0 +1,16 @@ + + + + diff --git a/wallet/ui/src/main/res/drawable/ic_transaction_post.xml b/wallet/ui/src/main/res/drawable/ic_transaction_post.xml new file mode 100644 index 000000000..ee92de9f9 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_transaction_post.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_transfer_direction.xml b/wallet/ui/src/main/res/drawable/ic_transfer_direction.xml new file mode 100644 index 000000000..33d3e78d1 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_transfer_direction.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index ae8a9f8cd..24ebc031c 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -2,13 +2,20 @@ Your account Your balance + Message: Transfer A fee of %d%% applies Amount Amount to send + Token + Debit + Credit Choose recipient Confirm transaction Transaction successful + Transaction amount + Base amount + Transactions peer tokens to %s Recipient address Transferred to @@ -35,4 +42,26 @@ You have %d Free %s Oops! an error occurred Retry + + Base amount + %s%% to Peer Bank (platform fee) + %s%% Burned (removed from supply) + + To @%s + From @%s + To %s + Daily Mint + Extra post + Purchase + Extra like + Dislike + Extra comment + Transaction + Pinned post promo + + Remove + + Hidden + This content may be sensitive or abusive. + This content was removed as illegal diff --git a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModelTest.kt b/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModelTest.kt deleted file mode 100644 index 2c30382c5..000000000 --- a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModelTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import app.cash.turbine.test -import eu.peernetwork.wallet.domain.model.Wallet -import eu.peernetwork.wallet.domain.usecase.ObservableOverviewUsecase -import eu.peernetwork.wallet.domain.usecase.OverviewUsecase -import eu.peernetwork.wallet.ui.mapper.mapFromDomain -import io.mockk.coEvery -import io.mockk.every -import io.mockk.mockk -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain -import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import java.math.BigDecimal -import kotlin.test.assertEquals - -@OptIn(ExperimentalCoroutinesApi::class) -internal class OverviewViewModelTest { - @get:Rule - val instantExecutorRule = InstantTaskExecutorRule() - - private val dispatcher = UnconfinedTestDispatcher() - - private val usecase = mockk() - - private val observableUsecase = mockk() - - private lateinit var viewModel: OverviewViewModel - - @Before - fun setup() { - Dispatchers.setMain(dispatcher) - viewModel = OverviewViewModel(usecase, observableUsecase) - } - - @After - fun tearDown() { - Dispatchers.resetMain() - } - - @Test - fun `test get overview success`() = runTest { - val balance = BigDecimal(100) - val mockData = mockk(relaxed = true) - every { mockData.balance } returns balance - coEvery { usecase() } coAnswers { - delay(100) - mockData - } - viewModel.getBalance() - viewModel.state.test { - assertEquals(OverviewViewModel.State.Loading, awaitItem()) - assertEquals(OverviewViewModel.State.Success(mockData.mapFromDomain()), awaitItem()) - } - } - - @Test - fun `test get overview error`() = runTest { - val error = RuntimeException() - coEvery { usecase() } throws error - viewModel.getBalance() - viewModel.state.test { - assertEquals(OverviewViewModel.State.Error(error), awaitItem()) - } - } -} diff --git a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt b/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt index cc1860a67..69e2e4d99 100644 --- a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt +++ b/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt @@ -4,6 +4,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import eu.peernetwork.wallet.domain.model.Tax import eu.peernetwork.wallet.domain.usecase.TaxUsecase +import eu.peernetwork.wallet.ui.dashboard.DashboardViewModel import eu.peernetwork.wallet.ui.mapper.mapFromDomain import io.mockk.coEvery import io.mockk.mockk @@ -29,12 +30,12 @@ internal class ServiceViewModelTest { private val usecase = mockk() - private lateinit var viewModel: ServiceViewModel + private lateinit var viewModel: DashboardViewModel @Before fun setup() { Dispatchers.setMain(dispatcher) - viewModel = ServiceViewModel(usecase) + viewModel = DashboardViewModel(usecase) } @After @@ -56,8 +57,8 @@ internal class ServiceViewModelTest { } viewModel.initialize() viewModel.state.test { - assertEquals(ServiceViewModel.State.Loading, awaitItem()) - assertEquals(ServiceViewModel.State.Success(tax.mapFromDomain()), awaitItem()) + assertEquals(DashboardViewModel.State.Loading, awaitItem()) + assertEquals(DashboardViewModel.State.Success(tax.mapFromDomain()), awaitItem()) } } @@ -67,7 +68,7 @@ internal class ServiceViewModelTest { coEvery { usecase() } throws exception viewModel.initialize() viewModel.state.test { - assertEquals(ServiceViewModel.State.Error(exception), awaitItem()) + assertEquals(DashboardViewModel.State.Error(exception), awaitItem()) } } } From 1f07b22801aa9943f06ad53351fc3c5638d7ff34 Mon Sep 17 00:00:00 2001 From: Constantine Kevin <78351447+conxtantyn@users.noreply.github.com> Date: Thu, 5 Feb 2026 14:48:55 +0100 Subject: [PATCH 2/2] [86c6kqcw7]: Add transaction message to token transfer (#177) --- README.md | 3 + .../ads/ui/checkout/CheckoutBalance.kt | 2 +- .../ads/ui/checkout/CheckoutScreen.kt | 2 +- .../ads/ui/checkout/CheckoutSummery.kt | 2 +- app/build.gradle.kts | 4 +- .../eu/peernetwork/app/ui/content/Content.kt | 4 +- .../app/ui/content/ContentModule.kt | 6 +- .../kotlin/eu/peernetwork/app/ui/feed/Feed.kt | 4 +- .../eu/peernetwork/app/ui/feed/FeedModule.kt | 6 +- .../eu/peernetwork/app/ui/profile/Profile.kt | 2 +- .../app/ui/profile/ProfileModule.kt | 2 +- .../app/ui/renderer/BalanceRenderer.kt | 6 +- .../eu/peernetwork/app/ui/search/Search.kt | 4 +- .../peernetwork/app/ui/search/SearchModule.kt | 6 +- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 3 +- .../peernetwork/app/ui/wallet/WalletModule.kt | 9 - .../app/ui/wallet/WalletNavigation.kt | 32 +++ .../peernetwork/app/ui/wallet/WalletPage.kt | 40 ++-- .../peernetwork/app/ui/wallet/WalletScreen.kt | 66 +++--- .../app/ui/wallet/WalletTransferButton.kt | 75 ++++++ app/src/main/res/drawable/ic_proceed.xml | 13 ++ app/src/main/res/drawable/ic_transaction.xml | 5 + app/src/main/res/raw/version.md | 56 ++--- app/src/main/res/values/strings.xml | 1 + .../eu/peernetwork/blog/ui/extension/Text.kt | 5 +- .../blog/ui/gallery/GalleryDetail.kt | 2 +- .../blog/ui/gallery/GalleryScreen.kt | 5 +- .../core/ui/design/luna/DesignText.kt | 29 ++- .../peernetwork/core/ui/theme/Typography.kt | 2 +- .../social/ui/search/member/MemberScreen.kt | 15 +- .../wallet/data/api/TransferApi.kt | 2 +- .../interactor/WalletInteractorDelegate.kt | 4 +- .../TransactionRepositoryDelegate.kt | 4 +- .../domain/interactor/WalletInteractor.kt | 2 +- .../repository/TransactionRepository.kt | 2 +- .../wallet/domain/usecase/TransferUsecase.kt | 5 +- .../wallet/remote/transfer.graphql | 4 +- .../wallet/remote/api/TransferApiDelegate.kt | 7 +- .../remote/api/TransferApiDelegateTest.kt | 4 +- .../wallet/remote/mock/TransferMock.kt | 4 +- .../wallet/ui/balance/BalanceOverview.kt | 33 ++- .../wallet/ui/balance/BalancePreview.kt | 10 +- .../wallet/ui/compose/ExpandableLabel.kt | 92 -------- .../wallet/ui/compose/ExpandableOption.kt | 116 ---------- .../wallet/ui/compose/TickButton.kt | 37 --- .../wallet/ui/compose/ToastLayout.kt | 87 ------- .../wallet/ui/dashboard/DashboardError.kt | 100 -------- .../wallet/ui/dashboard/DashboardModule.kt | 47 ---- .../wallet/ui/dashboard/DashboardPage.kt | 102 -------- .../wallet/ui/dashboard/DashboardScreen.kt | 116 ---------- .../wallet/ui/dashboard/DashboardTransfer.kt | 74 ------ .../peernetwork/wallet/ui/extension/String.kt | 5 + .../eu/peernetwork/wallet/ui/mapper/Amount.kt | 4 +- .../eu/peernetwork/wallet/ui/mapper/Fees.kt | 8 +- .../eu/peernetwork/wallet/ui/mapper/String.kt | 11 + .../wallet/ui/mapper/Transaction.kt | 2 - .../eu/peernetwork/wallet/ui/model/UiTax.kt | 11 +- .../wallet/ui/model/UiTransaction.kt | 1 - .../wallet/ui/model/UiTransferDetail.kt | 9 + .../{dashboard/Dashboard.kt => rate/Rate.kt} | 15 +- .../peernetwork/wallet/ui/rate/RateModule.kt | 27 +++ .../peernetwork/wallet/ui/rate/RateScreen.kt | 56 +++++ .../RateViewModel.kt} | 4 +- .../wallet/ui/transactions/Transactions.kt | 7 +- .../ui/transactions/TransactionsButton.kt | 75 ++++++ .../ui/transactions/TransactionsItem.kt | 15 +- .../ui/transactions/TransactionsList.kt | 37 ++- .../ui/transactions/TransactionsModule.kt | 20 ++ .../ui/transactions/TransactionsScreen.kt | 59 ++++- .../wallet/ui/transfer/Transfer.kt | 5 +- .../wallet/ui/transfer/TransferAmount.kt | 107 +++++++++ .../wallet/ui/transfer/TransferButton.kt | 71 ------ .../wallet/ui/transfer/TransferCheckout.kt | 186 +++++++++++++++ .../wallet/ui/transfer/TransferError.kt | 162 +++++++++++++ .../wallet/ui/transfer/TransferForm.kt | 95 -------- .../wallet/ui/transfer/TransferMessage.kt | 171 ++++++++++++++ .../wallet/ui/transfer/TransferModule.kt | 31 ++- .../wallet/ui/transfer/TransferPage.kt | 172 ++++++++++++++ .../wallet/ui/transfer/TransferPreview.kt | 135 +++++++++++ .../wallet/ui/transfer/TransferRecipient.kt | 210 +++++++++++------ .../wallet/ui/transfer/TransferScreen.kt | 181 ++++++--------- .../wallet/ui/transfer/TransferSheet.kt | 217 ------------------ .../ui/transfer/TransferSheetScaffold.kt | 136 ----------- .../wallet/ui/transfer/TransferSuccess.kt | 136 +++++++++++ .../wallet/ui/transfer/TransferSummary.kt | 145 ++++++++++++ .../wallet/ui/transfer/TransferViewModel.kt | 51 +++- .../ui/usecase/TransactionListingUsecase.kt | 17 +- wallet/ui/src/main/res/drawable/ic_edit.xml | 16 ++ .../ui/src/main/res/drawable/ic_message.xml | 19 ++ .../ui/src/main/res/drawable/ic_proceed.xml | 13 ++ .../main/res/drawable/ic_recipient_search.xml | 9 + .../src/main/res/drawable/ic_successful.xml | 13 ++ wallet/ui/src/main/res/values/strings.xml | 24 ++ .../RateViewModelTest.kt} | 15 +- 94 files changed, 2241 insertions(+), 1725 deletions(-) create mode 100644 app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletTransferButton.kt create mode 100644 app/src/main/res/drawable/ic_proceed.xml create mode 100644 app/src/main/res/drawable/ic_transaction.xml delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableLabel.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableOption.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/TickButton.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ToastLayout.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/String.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/String.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransferDetail.kt rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{dashboard/Dashboard.kt => rate/Rate.kt} (52%) create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateModule.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateScreen.kt rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{dashboard/DashboardViewModel.kt => rate/RateViewModel.kt} (92%) create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsButton.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferAmount.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferButton.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferCheckout.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferError.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferForm.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferMessage.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPage.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPreview.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheetScaffold.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSuccess.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSummary.kt create mode 100644 wallet/ui/src/main/res/drawable/ic_edit.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_message.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_proceed.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_recipient_search.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_successful.xml rename wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/{service/ServiceViewModelTest.kt => rate/RateViewModelTest.kt} (77%) diff --git a/README.md b/README.md index 44b6fdada..e8614c924 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,9 @@ The official Android client for Peer Network, built with modern Android development standards. +📱 **Get it on Google Play** +https://play.google.com/store/apps/details?id=eu.peernetwork.app + [![Contributing](https://img.shields.io/badge/Contributing-Guidelines-blue.svg)](https://github.com/peer-network/.github/blob/main/CONTRIBUTING.md) ## 🏗 Technology Stack diff --git a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutBalance.kt b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutBalance.kt index ca10d2c29..cbcb46f1e 100644 --- a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutBalance.kt +++ b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutBalance.kt @@ -9,7 +9,7 @@ import eu.peernetwork.core.ui.renderer.Renderer interface CheckoutBalance : Renderer.Stateless { @Composable - fun Charges( + fun Rate( viewModelStoreOwner: ViewModelStoreOwner, content: @Composable (State>, () -> Unit) -> Unit ) diff --git a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutScreen.kt b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutScreen.kt index ba5d97da8..04a6c59f5 100644 --- a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutScreen.kt +++ b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutScreen.kt @@ -54,7 +54,7 @@ fun CheckoutScreen( } } val handleFinish by rememberUpdatedState(onFinish) val handleProfile by rememberUpdatedState(onProfile) - component.checkoutBalance().Charges( + component.checkoutBalance().Rate( viewModelStoreOwner = viewModelStoreOwner ) { tax, refresh -> val price = remember { derivedStateOf { diff --git a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutSummery.kt b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutSummery.kt index be9363602..64cc7fe44 100644 --- a/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutSummery.kt +++ b/ads/ui/src/main/kotlin/eu/peernetwork/ads/ui/checkout/CheckoutSummery.kt @@ -63,7 +63,7 @@ fun CheckoutSummery( ) Row(verticalAlignment = Alignment.CenterVertically) { Text( - text = "200", + text = "${price.toInt()}", style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onBackground diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 216adaa13..ef986bdab 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,8 +6,8 @@ plugins { android { ndkVersion = "26.1.10909125" defaultConfig { - versionCode = 39 - versionName = "1.11.1" + versionCode = 40 + versionName = "1.12.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt index 759b680e4..89ed0abc1 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/content/Content.kt @@ -14,7 +14,7 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate interface Content : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -36,7 +36,7 @@ interface Content : ApplicationProvider { Confirmation, Screen, Detail, - Dashboard, + Rate, Balance, Checkout, Boost diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt index 5ef1a521d..72c13fd2f 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/content/ContentModule.kt @@ -19,7 +19,7 @@ import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate @Module object ContentModule { @@ -100,9 +100,9 @@ object ContentModule { @Content.Scope @Provides @IntoMap - @UiBuilder(Dashboard.Builder::class) + @UiBuilder(Rate.Builder::class) fun provideServiceBuilder(component: Content.Component): UiComponent.Builder { - return Dashboard.Builder(component) + return Rate.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt index 590945726..a066c35ac 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/Feed.kt @@ -15,7 +15,7 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate interface Feed : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -37,7 +37,7 @@ interface Feed : ApplicationProvider { Timeline, Connection, Confirmation, - Dashboard, + Rate, Balance, Checkout, Screen { diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt index ff43bb917..c3a862c88 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/feed/FeedModule.kt @@ -24,7 +24,7 @@ import eu.peernetwork.core.ui.factory.UiViewModelFactory import eu.peernetwork.social.ui.connection.Connection import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate import javax.inject.Provider @Module @@ -129,9 +129,9 @@ object FeedModule { @Feed.Scope @Provides @IntoMap - @UiBuilder(Dashboard.Builder::class) + @UiBuilder(Rate.Builder::class) fun provideServiceBuilder(component: Feed.Component): UiComponent.Builder { - return Dashboard.Builder(component) + return Rate.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt index 72ae1620a..7ebab4a31 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/Profile.kt @@ -21,7 +21,7 @@ import eu.peernetwork.social.ui.report.Report import eu.peernetwork.user.ui.user.User import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard as WalletDashboard +import eu.peernetwork.wallet.ui.rate.Rate as WalletDashboard interface Profile : ApplicationProvider { fun settingsEvent(): SettingsEvent diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt index 3ce43a508..d395c4f8a 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/profile/ProfileModule.kt @@ -26,7 +26,7 @@ import eu.peernetwork.social.ui.report.Report import eu.peernetwork.user.ui.user.User import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard as WalletDashboard +import eu.peernetwork.wallet.ui.rate.Rate as WalletDashboard @Module object ProfileModule { diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt index ba652aa15..c873f3817 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/renderer/BalanceRenderer.kt @@ -37,7 +37,7 @@ import eu.peernetwork.core.ui.design.luna.DesignStreamState import eu.peernetwork.core.ui.theme.PeerAppRed import eu.peernetwork.wallet.ui.R import eu.peernetwork.wallet.ui.balance.BalanceScreen -import eu.peernetwork.wallet.ui.dashboard.DashboardScreen +import eu.peernetwork.wallet.ui.rate.RateScreen import javax.inject.Inject class BalanceRenderer @Inject constructor( @@ -75,12 +75,12 @@ class BalanceRenderer @Inject constructor( } @Composable - override fun Charges( + override fun Rate( viewModelStoreOwner: ViewModelStoreOwner, content: @Composable ((State>, () -> Unit) -> Unit) ) { val updatedContent by rememberUpdatedState(content) - DashboardScreen( + RateScreen( provider = provider, viewModelStoreOwner = viewModelStoreOwner ) { service, action -> diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt index c4dbcca2f..31fd012bc 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/search/Search.kt @@ -17,7 +17,7 @@ import eu.peernetwork.social.ui.search.tag.Tag import eu.peernetwork.social.ui.search.title.Title import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate interface Search : ApplicationProvider { fun settingsEvent(): SettingsEvent @@ -42,7 +42,7 @@ interface Search : ApplicationProvider { Connection, Confirmation, Boost, - Dashboard, + Rate, Balance, Checkout, Screen diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt index 05d15c6e7..f1b7fbef7 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/search/SearchModule.kt @@ -23,7 +23,7 @@ import eu.peernetwork.social.ui.search.tag.Tag import eu.peernetwork.social.ui.search.title.Title import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.confirmation.Confirmation -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate @Module object SearchModule { @@ -136,9 +136,9 @@ object SearchModule { @Search.Scope @Provides @IntoMap - @UiBuilder(Dashboard.Builder::class) + @UiBuilder(Rate.Builder::class) fun provideServiceBuilder(component: Search.Component): UiComponent.Builder { - return Dashboard.Builder(component) + return Rate.Builder(component) } @Provides diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt index 3baffb9e3..96b0010df 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt @@ -9,7 +9,7 @@ import eu.peernetwork.core.ui.component.UiComponent import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.social.ui.search.member.Member import eu.peernetwork.wallet.ui.balance.Balance -import eu.peernetwork.wallet.ui.dashboard.Dashboard +import eu.peernetwork.wallet.ui.rate.Rate import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer @@ -30,7 +30,6 @@ interface Wallet : ApplicationProvider { Member, Profile, Balance, - Dashboard, Transactions, Search, UiComponentProvider diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt index 743bef16b..13f38b8c4 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletModule.kt @@ -11,7 +11,6 @@ import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.social.ui.search.member.Member import eu.peernetwork.wallet.ui.balance.Balance -import eu.peernetwork.wallet.ui.dashboard.Dashboard import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer import javax.inject.Provider @@ -41,14 +40,6 @@ object WalletModule { return Transfer.Builder(component) } - @Provides - @Wallet.Scope - @IntoMap - @UiBuilder(Dashboard.Builder::class) - fun provideServiceBuilder(component: Wallet.Component): UiComponent.Builder { - return Dashboard.Builder(component) - } - @Wallet.Scope @Provides @IntoMap diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt index 4e0f5b2d6..f359f04d4 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletNavigation.kt @@ -1,8 +1,11 @@ package eu.peernetwork.app.ui.wallet import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.focus.FocusRequester +import androidx.lifecycle.ViewModelStoreOwner import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.composable @@ -13,13 +16,23 @@ import eu.peernetwork.app.ui.search.SearchMode import eu.peernetwork.app.ui.search.SearchScreen import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.design.material.DesignRouter +import eu.peernetwork.core.ui.extension.navigateIfNecessary +import eu.peernetwork.core.ui.extension.route import eu.peernetwork.user.domain.model.Account +import eu.peernetwork.wallet.ui.model.UiRecipient +import eu.peernetwork.wallet.ui.transfer.TransferCheckout +import eu.peernetwork.wallet.ui.transfer.TransferScreen @Composable fun WalletNavigation( account: Account, + disable: MutableState, + recipient: MutableState, + focusRequester: FocusRequester, controller: NavHostController, provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + onSearch: () -> Unit, content: @Composable (NavHostController) -> Unit, ) { val updatedContent by rememberUpdatedState(content) @@ -28,6 +41,25 @@ fun WalletNavigation( startDestination = "wallet" ) { composable("wallet") { updatedContent(controller) } + composable("transfer") { + TransferScreen( + disable = disable, + recipient = recipient, + provider = provider, + focusRequester = focusRequester, + viewModelStoreOwner = viewModelStoreOwner, + onUserClicked = { controller.navigateIfNecessary("profile/${it}") }, + onClick = onSearch + ) { controller.navigateIfNecessary("checkout") } + } + composable("checkout") { + TransferCheckout( + provider = provider, + viewModelStoreOwner = viewModelStoreOwner, + onUserClicked = { controller.navigateIfNecessary("profile/${it}") }, + onBack = { controller.popBackStack() } + ) { controller.route("wallet") } + } composable( "profile/{id}", arguments = listOf(navArgument("id") { diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt index 1f2867c39..dc146a64a 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt @@ -1,32 +1,27 @@ package eu.peernetwork.app.ui.wallet import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.peernetwork.core.ui.design.luna.DesignRefreshScaffold import eu.peernetwork.core.ui.design.material.DesignScaffold -import eu.peernetwork.wallet.ui.R +import eu.peernetwork.core.ui.theme.DesignTheme @Composable fun WalletPage( onRefresh: () -> Unit, header: @Composable () -> Unit, - transactions: @Composable () -> Unit, content: @Composable () -> Unit, ) { val updatedHeader by rememberUpdatedState(header) - val updatedTransactions by rememberUpdatedState(transactions) val updatedContent by rememberUpdatedState(content) val isRefreshing = remember { mutableStateOf(false) } DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { @@ -40,24 +35,17 @@ fun WalletPage( .padding(bottom = 10.dp) ) { updatedHeader() } } - ) { - DesignScaffold( - alwaysReturn = true, - modifier = Modifier.fillMaxSize(), - header = { updatedContent() } - ) { - Column { - Text( - text = stringResource(R.string.transactions_label), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(top = 12.dp) - .padding(horizontal = 8.dp) - .padding(bottom = 10.dp) - ) - updatedTransactions() - } - } - } + ) { updatedContent() } + } +} + +@Preview +@Composable +fun PreviewWalletPage() { + DesignTheme(isDarkMode = true) { + WalletPage( + onRefresh = {}, + header = { }, + ) { } } } diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt index 24552ed07..a56bb4845 100644 --- a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.ViewModelStoreOwner @@ -24,8 +25,6 @@ import eu.peernetwork.social.ui.search.member.MemberModal import eu.peernetwork.user.domain.model.Account import eu.peernetwork.wallet.ui.balance.BalanceOverview import eu.peernetwork.wallet.ui.model.UiRecipient -import eu.peernetwork.wallet.ui.dashboard.DashboardScreen -import eu.peernetwork.wallet.ui.dashboard.DashboardState import eu.peernetwork.wallet.ui.saveable.UiRecipientSaver import eu.peernetwork.wallet.ui.transactions.TransactionsList import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator @@ -46,43 +45,39 @@ fun WalletScreen( val recipient = rememberSaveable(saver = UiRecipientSaver) { mutableStateOf(null) } - val service = remember(recipient.value) { - mutableStateOf(recipient.value?.let { - DashboardState.Transfer(it) - } ?: DashboardState.Default) - } val listState = rememberLazyListState() val controller = rememberNavController() + val focusRequester = remember { FocusRequester() } val showSheet = rememberSaveable { mutableStateOf(false) } val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } val navigator = remember { NavigationInteractor(context, controller) } CompositionLocalProvider(TransactionsNavigator.LocalTransactionsNavigator provides navigator) { WalletNavigation( account = account, + disable = showSheet, + recipient = recipient, + focusRequester = focusRequester, controller = controller, - provider = component + provider = component, + viewModelStoreOwner = viewModelStoreOwner, + onSearch = { showSheet.value = true } ) { controller -> WalletPage( onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) }, - transactions = { - TransactionsList( - uuid = account.id, - limit = postLimit, - lastUpdated = lastUpdated, - listState = listState, - provider = component, - viewModelStoreOwner = viewModelStoreOwner - ) { controller.navigateIfNecessary("profile/${it}") } - } ) { - DashboardScreen( - dashboardState = service, + TransactionsList( + uuid = account.id, + limit = postLimit, + lastUpdated = lastUpdated, + listState = listState, provider = component, - viewModelStoreOwner = viewModelStoreOwner, - onAccountClicked = { controller.navigateIfNecessary("profile/${it}") }, - onClear = { recipient.value = null } - ) { showSheet.value = true } + onTransaction = { + recipient.value = null + controller.navigateIfNecessary("transfer") + }, + viewModelStoreOwner = viewModelStoreOwner + ) { controller.navigateIfNecessary("profile/${it}") } } DesignTitleBarHost( tag = "WalletScreen", @@ -98,15 +93,20 @@ fun WalletScreen( } } } - MemberModal(postLimit, showSheet, component, viewModelStoreOwner) { - recipient.value = UiRecipient( - id = it.id, - username = it.username, - slug = it.slug, - imageUrl = it.imageUrl - ) - true - } + } + MemberModal( + postLimit = postLimit, + showSheet = showSheet, + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { + recipient.value = UiRecipient( + id = it.id, + username = it.username, + slug = it.slug, + imageUrl = it.imageUrl + ) + true } } } diff --git a/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletTransferButton.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletTransferButton.kt new file mode 100644 index 000000000..5a1212955 --- /dev/null +++ b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletTransferButton.kt @@ -0,0 +1,75 @@ +package eu.peernetwork.app.ui.wallet + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.app.R +import eu.peernetwork.core.ui.theme.DesignTheme + +@Composable +fun WalletTransferButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .clickable(onClick = onClick) + .padding(10.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_transaction), + contentDescription = stringResource(R.string.transfer_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + .padding(12.dp) + ) + Text( + text = stringResource(R.string.transfer_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 10.dp) + .weight(1f) + ) + Icon( + painter = painterResource(R.drawable.ic_proceed), + contentDescription = stringResource(R.string.transfer_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) + .clip(CircleShape) + .padding(6.dp) + ) + } +} + +@Preview +@Composable +fun PreviewWalletTransferButton() { + DesignTheme(isDarkMode = true) { + WalletTransferButton {} + } +} diff --git a/app/src/main/res/drawable/ic_proceed.xml b/app/src/main/res/drawable/ic_proceed.xml new file mode 100644 index 000000000..ba1996611 --- /dev/null +++ b/app/src/main/res/drawable/ic_proceed.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_transaction.xml b/app/src/main/res/drawable/ic_transaction.xml new file mode 100644 index 000000000..fcaf7bfb2 --- /dev/null +++ b/app/src/main/res/drawable/ic_transaction.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/raw/version.md b/app/src/main/res/raw/version.md index 2fee9bd2c..6382095bd 100644 --- a/app/src/main/res/raw/version.md +++ b/app/src/main/res/raw/version.md @@ -1,49 +1,37 @@ # 📝 Release Notes -### 🚀 Highlights -This release focuses on strengthening content moderation, reporting flows, and visibility controls for illegal and hidden content, ensuring better compliance, transparency, and safety across the platform. +## What’s New ---- - -### ✨ New Features & Updates - -- **Reported Content Moderation** - Introduced a unified moderation flow to handle reported posts, comments, and profiles more effectively. - -- **Report a Profile** - Users can now report profiles directly, enabling faster detection of abusive or illegal accounts. +### A Clear Transaction History -- **Report a Post** - Added the ability to report individual posts for policy violations or illegal content. +Your wallet now gives you a complete, transparent view of every transaction — making it easier to track how your Peer Tokens move. -- **Report a Comment** - Comments can now be reported independently, improving moderation granularity. +Each transaction includes: -- **Hidden Content Settings (Backend)** - Implemented backend settings to manage hidden content visibility and behavior across the system. +* **Total Peer Token amount** (including fees) +* **Fee breakdown** (Peer Bank, Burns, Inviter) +* **Timestamp** +* **Message** (if added) -- **Display an "Illegal" Post** - Posts marked as illegal can now be displayed with appropriate handling (e.g., warnings, restricted views). +You can expand any transaction to see full details, or collapse it to keep things clean. -- **Display an "Illegal" Profile** - Profiles flagged as illegal are now handled explicitly in the UI and backend logic. +This is the first iteration of **Transaction History**. Some transaction types (such as **Daily Mint**) are not included yet and will be added in a future update. -- **Display an "Illegal" Comment** - Added support for displaying comments marked as illegal with proper moderation states. +For now, most transactions shown are from the **last month**. Older transactions will be migrated and made visible in the next iteration. -- **Warnings for Promoting Reported Content** - Users are now warned when attempting to promote content that has been reported. - -- **Display Illegal/Hidden Content in Ad Stats** - Advertising statistics now include visibility into illegal or hidden content to ensure accurate reporting. +Everything you earn or spend is now easier to **track, understand, and trust**. --- -### 🧩 Technical Improvements -- Improved moderation workflows and state handling. -- Better alignment between moderation status and advertising analytics. +### Send Peer Tokens with a Personal Touch ---- +Transferring Peer Tokens just got more expressive. +You can now add an optional message when sending Peer Tokens to another user — whether it’s a thank-you, a shoutout, or just an emoji. + +What’s new in the transfer flow: -### 🧠 Known Issues -- None reported at this time. +* Add a note up to **500 characters** (letters, numbers, emojis & most symbols allowed) +* Clear **transaction summary** before sending +* Improved **error messages** that guide you to the next step +* Messages appear in **both users’ Transaction History** +* Safe, transparent, and now more personal diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7b606d07a..d0b11af57 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,6 +27,7 @@ Wallet Profile Trend + Transfer Version %s Skip diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/extension/Text.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/extension/Text.kt index 441be8b4f..46b11e515 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/extension/Text.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/extension/Text.kt @@ -2,7 +2,6 @@ package eu.peernetwork.blog.ui.extension import androidx.compose.ui.text.AnnotatedString -fun AnnotatedString.normalizeWhitespaces(): AnnotatedString { - val sanitizedText = this.text.replace(Regex("\\s{2,}"), " ").trim() - return AnnotatedString(sanitizedText, this.spanStyles, this.paragraphStyles) +fun AnnotatedString.normalizeWhitespaces(): String { + return replace(Regex("\\s{2,}"), " ").trim() } diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryDetail.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryDetail.kt index 00662b9c0..746fb462a 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryDetail.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryDetail.kt @@ -52,7 +52,7 @@ fun GalleryDetail( modifier = Modifier.padding(top = 4.dp), ) { DesignRichText( - text = description.normalizeWhitespaces(), + text = description, maxLines = 2, lineHeight = 18.sp, style = MaterialTheme.typography.labelLarge, diff --git a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryScreen.kt b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryScreen.kt index 469608249..3293badaf 100644 --- a/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryScreen.kt +++ b/blog/ui/src/main/kotlin/eu/peernetwork/blog/ui/gallery/GalleryScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.unit.dp import eu.peernetwork.blog.ui.engagement.EngagementInteractor.Companion.LocalEngagementInteractor import eu.peernetwork.blog.ui.engagement.EngagementReaction.Companion.LocalEngagementReaction import eu.peernetwork.blog.ui.engagement.EngagementReactionStream +import eu.peernetwork.blog.ui.extension.normalizeWhitespaces import eu.peernetwork.blog.ui.extension.route import eu.peernetwork.blog.ui.mapper.format import eu.peernetwork.blog.ui.model.UiPost @@ -26,6 +27,7 @@ import eu.peernetwork.blog.ui.model.UiPostType import eu.peernetwork.blog.ui.post.PostInteractor.Companion.LocalPostInteractor import eu.peernetwork.blog.ui.post.PostNavigator import eu.peernetwork.blog.ui.post.PostNavigator.Companion.LocalPostNavigator +import eu.peernetwork.core.ui.mapper.annotate @Composable fun GalleryScreen( @@ -51,7 +53,8 @@ fun GalleryScreen( GalleryScaffold( type = post.type, title = post.title, - description = post.description, + description = post.description.normalizeWhitespaces() + .annotate(), time = context.format(post.time), asset = post.asset, isVisible = isVisible, diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignText.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignText.kt index 7d3b764fe..d3cd211fa 100644 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignText.kt +++ b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/design/luna/DesignText.kt @@ -115,19 +115,27 @@ fun DesignText( style = style ) LaunchedEffect(expanded) { - if (expanded) { + val isMax = maxLines == Int.MAX_VALUE + if (expanded || isMax) { adjustedText = buildAnnotatedString { append(text) - pushStringAnnotation(tag = expandEllipsis, annotation = expandEllipsis) - withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { - append(collapseEllipsis) + if (!isMax) { + pushStringAnnotation(tag = expandEllipsis, annotation = expandEllipsis) + withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) { + append(collapseEllipsis) + } + pop() } - pop() } } else if (hasOverflow) { hasOverflow = false layoutResult?.let { result -> - val lastVisibleCharIndex = result.getLineEnd(maxLines - 1, visibleEnd = true) + try { + result.getLineEnd(maxLines - 1, visibleEnd = true) + } catch (_: Throwable) { + null + } + }?.let { lastVisibleCharIndex -> val cutoffIndex = (lastVisibleCharIndex - expandEllipsis.length).coerceAtLeast(0) adjustedText = buildAnnotatedString { append(text.subSequence(0, cutoffIndex)) @@ -141,9 +149,14 @@ fun DesignText( } } LaunchedEffect(hasOverflow, text, expandEllipsis) { - if (hasOverflow) { + if (hasOverflow && maxLines != Int.MAX_VALUE) { layoutResult?.let { result -> - val lastVisibleCharIndex = result.getLineEnd(maxLines - 1, visibleEnd = true) + try { + result.getLineEnd(maxLines - 1, visibleEnd = true) + } catch (_: Throwable) { + null + } + }?.let { lastVisibleCharIndex -> val cutoffIndex = (lastVisibleCharIndex - expandEllipsis.length).coerceAtLeast(0) adjustedText = buildAnnotatedString { append(text.subSequence(0, cutoffIndex)) diff --git a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/theme/Typography.kt b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/theme/Typography.kt index 55ac8226a..414513f16 100644 --- a/core/ui/src/main/kotlin/eu/peernetwork/core/ui/theme/Typography.kt +++ b/core/ui/src/main/kotlin/eu/peernetwork/core/ui/theme/Typography.kt @@ -64,7 +64,7 @@ val Typography = Typography( fontFamily = Poppins, fontWeight = FontWeight.Normal, fontSize = 13.sp, - lineHeight = 16.sp + lineHeight = 18.sp ), labelMedium = TextStyle( fontFamily = Poppins, diff --git a/social/ui/src/main/kotlin/eu/peernetwork/social/ui/search/member/MemberScreen.kt b/social/ui/src/main/kotlin/eu/peernetwork/social/ui/search/member/MemberScreen.kt index f841d294f..39bce7f77 100644 --- a/social/ui/src/main/kotlin/eu/peernetwork/social/ui/search/member/MemberScreen.kt +++ b/social/ui/src/main/kotlin/eu/peernetwork/social/ui/search/member/MemberScreen.kt @@ -1,5 +1,7 @@ package eu.peernetwork.social.ui.search.member +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.input.TextFieldState @@ -14,6 +16,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -79,12 +82,18 @@ fun MemberScreen( .then(modifier), loading = { MemberSkeleton(3, header) }, error = { error -> - MemberError(component.resource().error(error.value)) { - viewModel.search(query.text.toString(), page) + Column { + updatedHeader() + MemberError(component.resource().error(error.value)) { + viewModel.search(query.text.toString(), page) + } } } ) { lazyPagingItems -> - LazyColumn(modifier = Modifier.fillMaxSize()) { + LazyColumn( + contentPadding = PaddingValues(bottom = 48.dp), + modifier = Modifier.fillMaxSize() + ) { item { updatedHeader() } items( count = lazyPagingItems.itemCount, diff --git a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt index fae0236e2..653ebee1c 100644 --- a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt +++ b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/api/TransferApi.kt @@ -15,5 +15,5 @@ interface TransferApi { suspend fun getQuote(token: Token): Quote - suspend fun send(recipient: String, tokens: BigDecimal): Receipt + suspend fun send(recipient: String, tokens: BigDecimal, message: String? = null): Receipt } \ No newline at end of file diff --git a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/interactor/WalletInteractorDelegate.kt b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/interactor/WalletInteractorDelegate.kt index d58f18693..2d9abc611 100644 --- a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/interactor/WalletInteractorDelegate.kt +++ b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/interactor/WalletInteractorDelegate.kt @@ -25,8 +25,8 @@ class WalletInteractorDelegate @Inject constructor( override fun observe(): Flow = mutableWallet - override suspend fun send(recipient: String, token: BigDecimal): Receipt { - val transfer = transactionRepository.send(recipient, token) + override suspend fun send(recipient: String, token: BigDecimal, message: String?): Receipt { + val transfer = transactionRepository.send(recipient, token, message) if (mutableWallet.replayCache.isNotEmpty()) { mutableWallet.firstOrNull()?.let { mutableWallet.tryEmit(it.copy(balance = it.balance - token)) diff --git a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt index ebff3a699..9882dbb0e 100644 --- a/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt +++ b/wallet/data/src/main/kotlin/eu/peernetwork/wallet/data/repository/TransactionRepositoryDelegate.kt @@ -24,7 +24,7 @@ class TransactionRepositoryDelegate @Inject constructor( return api.getQuote(token) } - override suspend fun send(recipient: String, price: BigDecimal): Receipt { - return api.send(recipient, price) + override suspend fun send(recipient: String, price: BigDecimal, message: String?): Receipt { + return api.send(recipient, price, message) } } diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/interactor/WalletInteractor.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/interactor/WalletInteractor.kt index 3d55b4707..92dace3ad 100644 --- a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/interactor/WalletInteractor.kt +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/interactor/WalletInteractor.kt @@ -10,5 +10,5 @@ interface WalletInteractor { fun observe(): Flow - suspend fun send(recipient: String, token: BigDecimal): Receipt + suspend fun send(recipient: String, token: BigDecimal, message: String? = null): Receipt } diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt index b032650fc..c546236ce 100644 --- a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/repository/TransactionRepository.kt @@ -19,5 +19,5 @@ interface TransactionRepository { suspend fun getQuote(token: Token): Quote - suspend fun send(recipient: String, price: BigDecimal): Receipt + suspend fun send(recipient: String, price: BigDecimal, message: String? = null): Receipt } diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransferUsecase.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransferUsecase.kt index fcbe3583a..20d76f249 100644 --- a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransferUsecase.kt +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransferUsecase.kt @@ -10,11 +10,12 @@ class TransferUsecase @Inject constructor( private val interactor: WalletInteractor ) : ParameterizedSuspendableUseCase { override suspend fun invoke(param: Parameter): Receipt { - return interactor.send(param.recipient, param.tokens) + return interactor.send(param.recipient, param.tokens, param.message) } data class Parameter( val recipient: String, - val tokens: BigDecimal + val tokens: BigDecimal, + val message: String? = null, ) } \ No newline at end of file diff --git a/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transfer.graphql b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transfer.graphql index 4b413f174..0c386a4d7 100644 --- a/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transfer.graphql +++ b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transfer.graphql @@ -1,5 +1,5 @@ -mutation ResolveTransfer($recipient: ID!, $numberoftokens: Int!) { - resolveTransfer(recipient: $recipient, numberoftokens: $numberoftokens) { +mutation ResolveTransfer($recipient: ID!, $numberoftokens: Decimal!, $message: String) { + resolveTransferV2(recipient: $recipient, numberoftokens: $numberoftokens, message: $message) { status ResponseCode } diff --git a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt index 9e97d435b..d6747978b 100644 --- a/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegate.kt @@ -63,14 +63,15 @@ class TransferApiDelegate @Inject constructor( } } - override suspend fun send(recipient: String, tokens: BigDecimal): Receipt { + override suspend fun send(recipient: String, tokens: BigDecimal, message: String?): Receipt { val response = client().mutation( ResolveTransferMutation( recipient = recipient, - numberoftokens = tokens.toInt() + message = Optional.presentIfNotNull(message), + numberoftokens = tokens.toString() ) ).execute() - val data = response.getOrThrow().resolveTransfer + val data = response.getOrThrow().resolveTransferV2 response.assertOrThrow(data.status, data.ResponseCode) return Receipt( recipient = recipient, diff --git a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt index 88bac773a..7e1823891 100644 --- a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt +++ b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/api/TransferApiDelegateTest.kt @@ -43,7 +43,7 @@ internal class TransferApiDelegateTest { mockData ).build() - every { mockData.resolveTransfer } returns transfer + every { mockData.resolveTransferV2 } returns transfer coEvery { client.mutation(any()).execute() } returns mockResponse val result = api.send(recipient, tokens) @@ -67,7 +67,7 @@ internal class TransferApiDelegateTest { mockData ).build() - every { mockData.resolveTransfer } returns transfer + every { mockData.resolveTransferV2 } returns transfer coEvery { client.mutation(any()).execute() } returns mockResponse val result = try { diff --git a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/mock/TransferMock.kt b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/mock/TransferMock.kt index dec427b89..2d96853de 100644 --- a/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/mock/TransferMock.kt +++ b/wallet/remote/src/test/kotlin/eu/peernetwork/wallet/remote/mock/TransferMock.kt @@ -4,8 +4,8 @@ import eu.peernetwork.core.remote.model.Status import wallet.wallet.eu.peernetwork.wallet.remote.ResolveTransferMutation object TransferMock { - fun transaction(): ResolveTransferMutation.ResolveTransfer { - return ResolveTransferMutation.ResolveTransfer( + fun transaction(): ResolveTransferMutation.ResolveTransferV2 { + return ResolveTransferMutation.ResolveTransferV2( status = Status.SUCCESS.value, ResponseCode = null, ) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt index c3fa1108c..de483eb07 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt @@ -6,26 +6,32 @@ import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.graphics.Color import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.design.luna.DesignStream import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.wallet.ui.mapper.format +import java.math.BigDecimal import java.math.RoundingMode @Composable @Suppress("UNCHECKED_CAST") fun BalanceOverview( - lastUpdated: State, + color: Color? = null, + offset: BigDecimal = BigDecimal.ZERO, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, + content: @Composable (Balance.Component, BalanceViewModel) -> Unit ) { + val updatedContent by rememberUpdatedState(content) BalanceScreen( provider = provider, viewModelStoreOwner = viewModelStoreOwner, ) { component, viewModel -> val state by viewModel.state.collectAsStateWithLifecycle() - val status by viewModel.status.collectAsStateWithLifecycle() val derivedState = remember { derivedStateOf { when (state) { @@ -48,10 +54,29 @@ fun BalanceOverview( error = { BalanceError(it, component) { viewModel() } } ) { val balance = remember { derivedStateOf { - it.value.balance.setScale(4, RoundingMode.HALF_UP) + (it.value.balance - offset).setScale(8, RoundingMode.HALF_UP) } } - BalancePreview(balance.value.toString()) + BalancePreview( + color = color, + balance = balance.value.format() + ) } + updatedContent(component, viewModel) + } +} + +@Composable +@Suppress("UNCHECKED_CAST") +fun BalanceOverview( + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, +) { + BalanceOverview( + provider = provider, + viewModelStoreOwner = viewModelStoreOwner, + ) { component, viewModel -> + val status by viewModel.status.collectAsStateWithLifecycle() LaunchedEffect(lastUpdated.value) { (status as? BalanceViewModel.Status.Success?)?.let { if (it.data != lastUpdated.value) { diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt index f0843012e..171add1bb 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalancePreview.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -33,6 +34,7 @@ import eu.peernetwork.wallet.ui.R fun BalancePreview( balance: String, modifier: Modifier = Modifier, + color: Color? = null, contentPadding: PaddingValues = PaddingValues(24.dp), ) { BoxWithConstraints { @@ -51,8 +53,12 @@ fun BalancePreview( modifier = Modifier.then(modifier) .fillMaxWidth() .clip(RoundedCornerShape(24.dp)) - .background(MaterialTheme.colorScheme.surfaceDim) - .background(brush = verticalGradient) + .background(color ?: MaterialTheme.colorScheme.surfaceDim) + .then(if (color != null) { + Modifier + } else { + Modifier.background(brush = verticalGradient) + }) .padding(contentPadding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableLabel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableLabel.kt deleted file mode 100644 index ae8d95710..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableLabel.kt +++ /dev/null @@ -1,92 +0,0 @@ -package eu.peernetwork.wallet.ui.compose - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.design.material.DesignCard -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.core.ui.theme.PeerTheme -import eu.peernetwork.wallet.ui.R - -@Composable -fun ExpandableLabel( - label: String, - state: MutableState, - onAnimationEnd: (Boolean) -> Unit = {}, - content: @Composable () -> Unit -) { - val border = MaterialTheme.colorScheme.surfaceVariant - val updatedContent by rememberUpdatedState(content) - DesignCard( - color = MaterialTheme.colorScheme.surfaceDim, - contentPadding = PaddingValues(0.dp), - shape = RoundedCornerShape(24.dp) - ) { - ExpandableOption( - state = state, - onAnimationEnd = onAnimationEnd, - icon = { - Box( - modifier = Modifier - .size(36.dp) - .background(MaterialTheme.colorScheme.background) - .padding(12.dp) - ) { - Icon( - painter = painterResource(R.drawable.ic_transaction), - contentDescription = stringResource(R.string.transfer_label), - modifier = Modifier.align(Alignment.Center) - ) - } - }, - items = { - Box( - modifier = Modifier - .drawBehind { - drawLine( - color = border.copy(alpha = .6f), - start = Offset(0f, 0f), - end = Offset(size.width, 0f), - strokeWidth = 1.dp.toPx() - ) - } - .padding(vertical = 16.dp) - ) { updatedContent() } - } - ) { Text(label, modifier = Modifier.padding(start = 12.dp)) } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewExpandableLabel() { - DesignTheme(isDarkMode = true) { - val showLabel = rememberSaveable { mutableStateOf(false) } - ExpandableLabel("ExpandableLabel", showLabel) { - Box(modifier = Modifier.padding(top = 8.dp, bottom = 16.dp)) { - Text("Content") - } - } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableOption.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableOption.kt deleted file mode 100644 index 50ea17767..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ExpandableOption.kt +++ /dev/null @@ -1,116 +0,0 @@ -package eu.peernetwork.wallet.ui.compose - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.design.material.DesignDetailLayout -import eu.peernetwork.core.ui.design.material.DesignLabel -import eu.peernetwork.core.ui.theme.DesignTheme - -@Composable -fun ExpandableOption( - state: MutableState, - style: TextStyle = MaterialTheme.typography.bodyMedium, - onAnimationEnd: (Boolean) -> Unit = { }, - icon: @Composable () -> Unit, - items: @Composable ColumnScope.() -> Unit, - content: @Composable () -> Unit, -) { - val updatedIcon by rememberUpdatedState(icon) - val updatedItems by rememberUpdatedState(items) - val updatedContent by rememberUpdatedState(content) - CompositionLocalProvider( - LocalContentColor provides MaterialTheme.colorScheme.onBackground, - LocalTextStyle provides style.copy( - color = MaterialTheme.colorScheme.onBackground - ) - ) { - DesignLabel( - visible = state.value, - onAnimationEnd = onAnimationEnd, - label = { - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { updatedItems() } - } - ) { - DesignDetailLayout( - lead = { - Box(modifier = Modifier - .clip(CircleShape) - .wrapContentSize() - .clipToBounds() - ) { updatedIcon() } }, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.clickable(role = Role.Button) { - state.value = !state.value - }.padding(16.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - updatedContent() - Spacer(modifier = Modifier.weight(1f)) - Icon( - painter = painterResource(R.drawable.ic_plus), - contentDescription = null, - modifier = Modifier.size(12.dp) - .graphicsLayer { - rotationZ = if (state.value) { - 45f - } else { - 0f - } - }, - tint = MaterialTheme.colorScheme.outlineVariant - ) - } - } - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewOption() { - DesignTheme { - val state = remember { mutableStateOf(false) } - ExpandableOption(state, icon = { Box(modifier = Modifier - .size(32.dp) - .background(MaterialTheme.colorScheme.background)) }, - items = { Text("Items") } - ) { - Text("Content", modifier = Modifier.padding(start = 12.dp)) - } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/TickButton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/TickButton.kt deleted file mode 100644 index 35344c0af..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/TickButton.kt +++ /dev/null @@ -1,37 +0,0 @@ -package eu.peernetwork.wallet.ui.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition -import eu.peernetwork.wallet.ui.R - -@Composable -fun TickButton( - modifier: Modifier = Modifier, - rawRes: Int = R.raw.tick -) { - var playing by remember { mutableStateOf(false) } - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) - val progress by animateLottieCompositionAsState( - composition = composition, - isPlaying = playing, - iterations = 1, - speed = 1f - ) - LaunchedEffect(Unit) { - playing = true - } - LottieAnimation( - composition = composition, - progress = { progress }, - modifier = modifier - ) -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ToastLayout.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ToastLayout.kt deleted file mode 100644 index a25597402..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/compose/ToastLayout.kt +++ /dev/null @@ -1,87 +0,0 @@ -package eu.peernetwork.wallet.ui.compose - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.airbnb.lottie.compose.LottieAnimation -import com.airbnb.lottie.compose.LottieCompositionSpec -import com.airbnb.lottie.compose.animateLottieCompositionAsState -import com.airbnb.lottie.compose.rememberLottieComposition -import eu.peernetwork.wallet.ui.R - -@Composable -fun BoxScope.ToastLayout( - modifier: Modifier = Modifier, - size: Dp = 320.dp, - rawRes: Int = R.raw.peerrocket, - fillToWidthEdges: Boolean = true, - overscanScale: Float = 1.12f, - anchorBottom: Boolean = true -) { - var isPlaying by remember { mutableStateOf(false) } - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) - val progress by animateLottieCompositionAsState( - composition = composition, - isPlaying = isPlaying, - iterations = 1, - speed = 1f - ) - LaunchedEffect(progress) { - if (progress >= 1f) isPlaying = false - } - val aspect: Float = remember(composition) { - val b = composition?.bounds - val w = b?.width()?.toFloat() ?: 1f - val h = b?.height()?.toFloat() ?: 1f - if (h > 0f) w / h else 1f - } - AnimatedVisibility( - visible = isPlaying && progress < 1f, - enter = fadeIn(animationSpec = tween(160)), - exit = fadeOut(animationSpec = tween(220)), - modifier = modifier.matchParentSize() - ) { - Box( - modifier = Modifier.matchParentSize(), - contentAlignment = if (anchorBottom) Alignment.BottomCenter else Alignment.Center - ) { - val lottieMod = if (fillToWidthEdges) { - Modifier - .fillMaxWidth() - .aspectRatio(aspect, matchHeightConstraintsFirst = false) - .graphicsLayer { - scaleX = overscanScale - scaleY = overscanScale - } - } else { - Modifier.size(size) - } - - LottieAnimation( - composition = composition, - progress = { progress }, - modifier = lottieMod - ) - } - } - LaunchedEffect(Unit) { - isPlaying = true - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt deleted file mode 100644 index 1ee8ca600..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardError.kt +++ /dev/null @@ -1,100 +0,0 @@ -package eu.peernetwork.wallet.ui.dashboard - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.common.interactor.ResourceInteractor -import eu.peernetwork.core.ui.R -import eu.peernetwork.core.ui.design.material.DesignErrorText -import eu.peernetwork.core.ui.design.luna.DesignAvatar -import eu.peernetwork.core.ui.design.material.DesignCard -import eu.peernetwork.core.ui.design.material.DesignDetailLayout -import eu.peernetwork.core.ui.theme.PeerTheme - -@Composable -fun DashboardError( - error: Throwable, - resource: ResourceInteractor, - onRetry: () -> Unit -) { - val errorMessage = stringResource(R.string.unknown_error_message) - DesignCard( - color = MaterialTheme.colorScheme.surfaceVariant, - contentPadding = PaddingValues(0.dp), - shape = RoundedCornerShape(24.dp) - ) { - DesignDetailLayout( - lead = { - DesignAvatar { - Box(modifier = Modifier - .size(36.dp) - .background(MaterialTheme.colorScheme.background), - contentAlignment = Alignment.Center - ) { - Icon( - painter = painterResource(R.drawable.ic_warning), - contentDescription = stringResource(R.string.error_label), - tint = MaterialTheme.colorScheme.tertiary, - modifier = Modifier.padding(8.dp) - ) - } - } }, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 12.dp) - ) { - DesignErrorText( - Throwable(resource.string(error.message ?: errorMessage), error), - textAlign = TextAlign.Start, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.tertiary - ), - modifier = Modifier.weight(1f) - ) - Spacer(modifier = Modifier.width(16.dp)) - IconButton(onRetry) { - Icon( - painter = painterResource(R.drawable.ic_refresh), - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.surfaceDim - ) - } - } - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewErrorLabel() { - PeerTheme { - val resource = remember { object : ResourceInteractor { - override fun getBaseUrl(): String = "" - override fun string(key: String): String = key - } } - DashboardError(RuntimeException(), resource) {} - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt deleted file mode 100644 index 109a4cc09..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardModule.kt +++ /dev/null @@ -1,47 +0,0 @@ -package eu.peernetwork.wallet.ui.dashboard - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import dagger.Module -import dagger.Provides -import dagger.multibindings.IntoMap -import eu.peernetwork.core.ui.annotation.UiBuilder -import eu.peernetwork.core.ui.annotation.UiViewModel -import eu.peernetwork.core.ui.component.UiComponent -import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.factory.UiBuilderFactory -import eu.peernetwork.core.ui.factory.UiViewModelFactory -import eu.peernetwork.wallet.ui.transfer.Transfer -import javax.inject.Provider - -@Module -object DashboardModule { - @Provides - @Dashboard.Scope - fun provideBuilderFactory(factory: Map, - @JvmSuppressWildcards Provider>): UiComponentProvider.Factory { - return UiBuilderFactory(factory) - } - - @Provides - @Dashboard.Scope - fun provideViewModelFactory( - classToViewModel: @JvmSuppressWildcards Map, Provider> - ): ViewModelProvider.Factory { - return UiViewModelFactory(classToViewModel) - } - - @Provides - @IntoMap - @Dashboard.Scope - @UiViewModel(DashboardViewModel::class) - fun viewModel(viewModel: DashboardViewModel): ViewModel = viewModel - - @Provides - @Dashboard.Scope - @IntoMap - @UiBuilder(Transfer.Builder::class) - fun provideTransferBuilder(component: Dashboard.Component): UiComponent.Builder { - return Transfer.Builder(component) - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt deleted file mode 100644 index 2495308d2..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt +++ /dev/null @@ -1,102 +0,0 @@ -package eu.peernetwork.wallet.ui.dashboard - -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.design.luna.DesignAvatar -import eu.peernetwork.core.ui.design.luna.DesignOutlineButton -import eu.peernetwork.core.ui.design.material.DesignCard -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.wallet.ui.model.UiRecipient -import java.util.UUID - -@Composable -fun DashboardPage( - name: String, - state: State, - onClick: () -> Unit, - content: @Composable (DashboardState.Transfer) -> Unit -) { - val updateContent by rememberUpdatedState(content) - Crossfade(state.value) { target -> - when(target) { - DashboardState.Default -> { - DesignOutlineButton( - onClick = onClick, - minHeight = 42.dp, - contentPadding = PaddingValues(vertical = 8.dp, horizontal = 32.dp), - modifier = Modifier - .padding(start = 4.dp) - .fillMaxWidth(), - content = { - Text(text = name, style = MaterialTheme.typography.bodySmall) - } - ) - } - is DashboardState.Transfer -> { updateContent(target) } - } - } -} - -@Preview -@Composable -fun PreviewServicePage() { - DesignTheme(isDarkMode = true) { - Column { - val recipient = UiRecipient( - id = UUID.randomUUID().toString(), - slug = "1234", - username = "johnDoe", - imageUrl = "http://localhost" - ) - DashboardPage( - "ServiceScreen", - remember { mutableStateOf(DashboardState.Transfer(recipient)) }, - {} - ) { - DashboardPage() - } - Spacer(modifier = Modifier.height(16.dp)) - DashboardPage( - "ServiceScreen", - remember { mutableStateOf(DashboardState.Default) }, - {} - ) {} - } - } -} - -@Composable -fun DashboardPage() { - DesignCard( - color = MaterialTheme.colorScheme.surfaceDim, - contentPadding = PaddingValues(16.dp), - shape = RoundedCornerShape(24.dp) - ) { - DesignAvatar { - Box(modifier = Modifier - .size(36.dp) - .background(MaterialTheme.colorScheme.background), - ) - } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt deleted file mode 100644 index 5ffa3f4ec..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardScreen.kt +++ /dev/null @@ -1,116 +0,0 @@ -package eu.peernetwork.wallet.ui.dashboard - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.platform.LocalContext -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.material.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState -import eu.peernetwork.core.ui.design.luna.DesignStreamState -import eu.peernetwork.core.ui.extension.builder -import eu.peernetwork.wallet.ui.model.UiRecipient -import eu.peernetwork.wallet.ui.model.UiTax - -sealed interface DashboardState { - data object Default : DashboardState - data class Transfer(val recipient: UiRecipient) : DashboardState -} - -@Composable -fun DashboardScreen( - provider: UiComponentProvider, - viewModelStoreOwner: ViewModelStoreOwner, - content: @Composable (State>, () -> Unit) -> Unit -) { - val context = LocalContext.current - val component = remember { - provider.builder(Dashboard.Builder::class.java).build(context) - } - val viewModel = viewModel( - modelClass = DashboardViewModel::class.java, - viewModelStoreOwner = viewModelStoreOwner, - factory = component.viewModelFactory() - ) - val state by viewModel.state.collectAsStateWithLifecycle() - val derivedState = remember { derivedStateOf { - when(state) { - DashboardViewModel.State.Empty -> DesignStreamState.Default - DashboardViewModel.State.Loading -> DesignStreamState.Loading - is DashboardViewModel.State.Success -> { - val data = (state as DashboardViewModel.State.Success) - DesignStreamState.Success(data.tax) - } - is DashboardViewModel.State.Error -> { - DesignStreamState.Error((state as DashboardViewModel.State.Error).error) - } - } - } } - val updatedContent by rememberUpdatedState(content) - val handleRefresh by rememberUpdatedState { viewModel.initialize() } - updatedContent(derivedState, handleRefresh) - LaunchedEffect(Unit) { - if (state is DashboardViewModel.State.Empty) { - viewModel.initialize() - } - } -} - -@Composable -fun DashboardScreen( - dashboardState: MutableState, - provider: UiComponentProvider, - viewModelStoreOwner: ViewModelStoreOwner, - onAccountClicked: (String) -> Unit, - onClear: () -> Unit, - onClick: () -> Unit, -) { - val context = LocalContext.current - val component = remember { - provider.builder(Dashboard.Builder::class.java).build(context) - } - val viewModel = viewModel( - modelClass = DashboardViewModel::class.java, - viewModelStoreOwner = viewModelStoreOwner, - factory = component.viewModelFactory() - ) - val state by viewModel.state.collectAsStateWithLifecycle() - val derivedState = remember { derivedStateOf { - when(state) { - DashboardViewModel.State.Empty -> DesignStatefulScaffoldState.Empty - DashboardViewModel.State.Loading -> DesignStatefulScaffoldState.Loading - is DashboardViewModel.State.Success -> { - val data = (state as DashboardViewModel.State.Success) - DesignStatefulScaffoldState.Success(data.tax) - } - is DashboardViewModel.State.Error -> { - DesignStatefulScaffoldState.Error((state as DashboardViewModel.State.Error).error) - } - } - } } - DesignStatefulScaffold( - derivedState, - onRefresh = { viewModel.initialize() }, - placeholder = { DashboardPage() }, - errorContent = { DashboardError(it, component.resource()) { viewModel.initialize() } } - ) { - DashboardTransfer( - tax = ((it.burn + it.peer) * 100).toInt(), - dashboardState, - component, - viewModelStoreOwner, - onAccountClicked, - onClear, - onClick - ) - } -} - diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt deleted file mode 100644 index 1dc686c80..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardTransfer.kt +++ /dev/null @@ -1,74 +0,0 @@ -package eu.peernetwork.wallet.ui.dashboard - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.res.stringResource -import androidx.lifecycle.ViewModelStoreOwner -import eu.peernetwork.wallet.ui.R -import eu.peernetwork.wallet.ui.compose.ExpandableLabel -import eu.peernetwork.wallet.ui.model.UiTransfer -import eu.peernetwork.wallet.ui.transfer.TransferScreen -import eu.peernetwork.wallet.ui.transfer.TransferSheet - -@Composable -fun DashboardTransfer( - tax: Int, - state: MutableState, - component: Dashboard.Component, - viewModelStoreOwner: ViewModelStoreOwner, - onAccountClicked: (String) -> Unit, - onClear: () -> Unit, - onClick: () -> Unit, -) { - val lastState = remember(state.value) { mutableStateOf(state.value) } - val showTransfer = rememberSaveable { mutableStateOf(false) } - val transaction = remember { mutableStateOf(null) } - val handleOnClear by rememberUpdatedState(onClear) - val handleOnAccountClicked by rememberUpdatedState(onAccountClicked) - ExpandableLabel( - stringResource(R.string.transfer_label), - showTransfer, - { - if (lastState.value is DashboardState.Default && !it) { - handleOnClear() - } - state.value = lastState.value - } - ) { - DashboardPage( - stringResource(R.string.recipient_selection_label), - state, - onClick, - ) { - TransferScreen( - tax = tax, - recipient = it.recipient, - provider = component, - viewModelStoreOwner = viewModelStoreOwner, - onClear = { - lastState.value = DashboardState.Default - showTransfer.value = false }, - onTransfer = { transaction.value = it } - ) { handleOnAccountClicked(it.id) } - } - } - (state.value as? DashboardState.Transfer?)?.let { - TransferSheet( - transaction, - component, - viewModelStoreOwner, - it.recipient, - { - transaction.value = null - lastState.value = DashboardState.Default - showTransfer.value = false - } - ) { handleOnAccountClicked(it.id) } - } -} - diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/String.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/String.kt new file mode 100644 index 000000000..0929e252e --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/extension/String.kt @@ -0,0 +1,5 @@ +package eu.peernetwork.wallet.ui.extension + +fun String.normalizeWhitespaces(): String { + return replace(Regex("\\s{2,}"), " ").trim() +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt index da3bbecf1..217b43995 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Amount.kt @@ -6,7 +6,7 @@ import java.math.RoundingMode fun Amount.mapFromDomain(): UiAmount { return UiAmount( - net = net.setScale(2, RoundingMode.HALF_UP), - gross = gross.setScale(2, RoundingMode.HALF_UP) + net = net.setScale(8, RoundingMode.HALF_UP), + gross = gross.setScale(8, RoundingMode.HALF_UP) ) } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt index aec05d153..9c0fa102b 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt @@ -6,9 +6,9 @@ import java.math.RoundingMode fun Fees.mapFromDomain(): UiFees { return UiFees( - total = total.setScale(2, RoundingMode.HALF_UP), - burn = burn.setScale(2, RoundingMode.HALF_UP), - peer = peer.setScale(2, RoundingMode.HALF_UP), - commission = commission.setScale(2, RoundingMode.HALF_UP) + total = total.setScale(8, RoundingMode.HALF_UP), + burn = burn.setScale(8, RoundingMode.HALF_UP), + peer = peer.setScale(8, RoundingMode.HALF_UP), + commission = commission.setScale(8, RoundingMode.HALF_UP) ) } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/String.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/String.kt new file mode 100644 index 000000000..8710c58be --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/String.kt @@ -0,0 +1,11 @@ +package eu.peernetwork.wallet.ui.mapper + +import java.math.BigDecimal + +fun BigDecimal.format(): String { + if (this.compareTo(BigDecimal.ZERO) == 0) { + return "0.00" + } + return this.stripTrailingZeros() + .toPlainString() +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt index de378ed35..ec527511d 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt @@ -2,7 +2,6 @@ package eu.peernetwork.wallet.ui.mapper import eu.peernetwork.core.ui.mapper.annotate import eu.peernetwork.wallet.domain.model.Transaction -import eu.peernetwork.wallet.ui.model.UiTax import eu.peernetwork.wallet.ui.model.UiTransaction import java.text.SimpleDateFormat import java.util.Date @@ -19,7 +18,6 @@ fun Transaction.mapToTransaction(): UiTransaction { category = category.mapFromDomain(), amount = amount.mapFromDomain(), fees = fees?.mapFromDomain(), - tax = UiTax.Free, createdAt = createdAt.toFormattedDate() ) } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt index 6a0ae63a1..4068df8e3 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTax.kt @@ -5,13 +5,4 @@ data class UiTax( val pool: Double, val peer: Double, val percentage: Double -) { - companion object { - val Free = UiTax( - burn = 0.0, - pool = 0.0, - peer = 0.0, - percentage = 0.0 - ) - } -} +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt index 1e4faaa15..602b29db2 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt @@ -10,7 +10,6 @@ data class UiTransaction( val recipient: UiUser, val category: UiCategory, val amount: UiAmount, - val tax: UiTax, val fees: UiFees?, val message: AnnotatedString?, val createdAt: String, diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransferDetail.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransferDetail.kt new file mode 100644 index 000000000..1f5c3d906 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransferDetail.kt @@ -0,0 +1,9 @@ +package eu.peernetwork.wallet.ui.model + +import java.math.BigDecimal + +data class UiTransferDetail( + val amount: BigDecimal, + val message: String, + val recipient: UiRecipient +) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/Rate.kt similarity index 52% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/Rate.kt index d120cd7bb..99c25926d 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/Dashboard.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/Rate.kt @@ -1,30 +1,29 @@ -package eu.peernetwork.wallet.ui.dashboard +package eu.peernetwork.wallet.ui.rate import android.content.Context import androidx.lifecycle.ViewModelProvider import eu.peernetwork.core.common.provider.CoreProvider import eu.peernetwork.core.ui.component.UiComponent -import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.wallet.ui.provider.WalletProvider import eu.peernetwork.wallet.ui.transfer.Transfer -interface Dashboard : WalletProvider, CoreProvider { +interface Rate : WalletProvider, CoreProvider { @javax.inject.Scope @Retention(AnnotationRetention.RUNTIME) annotation class Scope @Scope @dagger.Component( - dependencies = [ Dashboard::class ], - modules = [ DashboardModule::class ] + dependencies = [ Rate::class ], + modules = [ RateModule::class ] ) - interface Component : Dashboard, Transfer, UiComponentProvider { + interface Component : Rate, Transfer { fun viewModelFactory(): ViewModelProvider.Factory } - class Builder(private val dependency: Dashboard): UiComponent.DefaultBuilder() { + class Builder(private val dependency: Rate): UiComponent.DefaultBuilder() { override fun build(context: Context): Component { - return DaggerDashboard_Component.builder().dashboard(dependency).build() + return DaggerRate_Component.builder().rate(dependency).build() } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateModule.kt new file mode 100644 index 000000000..d0a5f19b2 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateModule.kt @@ -0,0 +1,27 @@ +package eu.peernetwork.wallet.ui.rate + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import dagger.Module +import dagger.Provides +import dagger.multibindings.IntoMap +import eu.peernetwork.core.ui.annotation.UiViewModel +import eu.peernetwork.core.ui.factory.UiViewModelFactory +import javax.inject.Provider + +@Module +object RateModule { + @Provides + @Rate.Scope + fun provideViewModelFactory( + classToViewModel: @JvmSuppressWildcards Map, Provider> + ): ViewModelProvider.Factory { + return UiViewModelFactory(classToViewModel) + } + + @Provides + @IntoMap + @Rate.Scope + @UiViewModel(RateViewModel::class) + fun viewModel(viewModel: RateViewModel): ViewModel = viewModel +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateScreen.kt new file mode 100644 index 000000000..61547ac94 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateScreen.kt @@ -0,0 +1,56 @@ +package eu.peernetwork.wallet.ui.rate + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.core.ui.extension.builder +import eu.peernetwork.wallet.ui.model.UiTax + +@Composable +fun RateScreen( + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + content: @Composable (State>, () -> Unit) -> Unit +) { + val context = LocalContext.current + val component = remember { + provider.builder(Rate.Builder::class.java).build(context) + } + val viewModel = viewModel( + modelClass = RateViewModel::class.java, + viewModelStoreOwner = viewModelStoreOwner, + factory = component.viewModelFactory() + ) + val state by viewModel.state.collectAsStateWithLifecycle() + val derivedState = remember { derivedStateOf { + when(state) { + RateViewModel.State.Empty -> DesignStreamState.Default + RateViewModel.State.Loading -> DesignStreamState.Loading + is RateViewModel.State.Success -> { + val data = (state as RateViewModel.State.Success) + DesignStreamState.Success(data.tax) + } + is RateViewModel.State.Error -> { + DesignStreamState.Error((state as RateViewModel.State.Error).error) + } + } + } } + val updatedContent by rememberUpdatedState(content) + val handleRefresh by rememberUpdatedState { viewModel.initialize() } + updatedContent(derivedState, handleRefresh) + LaunchedEffect(Unit) { + if (state is RateViewModel.State.Empty) { + viewModel.initialize() + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModel.kt similarity index 92% rename from wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt rename to wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModel.kt index f3f424534..43dd67f08 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardViewModel.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModel.kt @@ -1,4 +1,4 @@ -package eu.peernetwork.wallet.ui.dashboard +package eu.peernetwork.wallet.ui.rate import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject -class DashboardViewModel @Inject constructor( +class RateViewModel @Inject constructor( private val usecase: TaxUsecase ) : ViewModel() { private val mutableState = MutableStateFlow(State.Empty) diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt index 035a339f3..9eecc5984 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt @@ -4,7 +4,9 @@ import android.content.Context import androidx.lifecycle.ViewModelProvider import eu.peernetwork.core.common.provider.CoreProvider import eu.peernetwork.core.ui.component.UiComponent +import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.wallet.ui.provider.WalletProvider +import eu.peernetwork.wallet.ui.rate.Rate import eu.peernetwork.wallet.ui.transfer.Transfer interface Transactions : WalletProvider, CoreProvider { @@ -17,7 +19,10 @@ interface Transactions : WalletProvider, CoreProvider { dependencies = [ Transactions::class ], modules = [ TransactionsModule::class ] ) - interface Component : Transactions, Transfer { + interface Component : Transactions, + Rate, + Transfer, + UiComponentProvider { fun viewModelFactory(): ViewModelProvider.Factory } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsButton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsButton.kt new file mode 100644 index 000000000..6086f68d3 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsButton.kt @@ -0,0 +1,75 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransactionsButton( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .clickable(onClick = onClick) + .padding(10.dp) + ) { + Icon( + painter = painterResource(R.drawable.ic_transaction), + contentDescription = stringResource(R.string.transfer_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + .padding(12.dp) + ) + Text( + text = stringResource(R.string.transfer_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 10.dp) + .weight(1f) + ) + Icon( + painter = painterResource(R.drawable.ic_proceed), + contentDescription = stringResource(R.string.transfer_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) + .clip(CircleShape) + .padding(6.dp) + ) + } +} + +@Preview +@Composable +fun PreviewTransactionsButton() { + DesignTheme(isDarkMode = true) { + TransactionsButton {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt index eb9170b27..1dea93cf1 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt @@ -39,6 +39,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import eu.peernetwork.core.ui.design.luna.DesignRichText @@ -75,7 +76,11 @@ fun TransactionsItem( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { - updatedLead() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.height(48.dp) + .align(Alignment.Top) + ) { updatedLead() } Column( modifier = Modifier .weight(1f) @@ -86,6 +91,8 @@ fun TransactionsItem( style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, modifier = Modifier.tap(onProfileClicked) ) Text( @@ -105,7 +112,11 @@ fun TransactionsItem( ) } } - updatedTrailing() + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.height(48.dp) + .align(Alignment.Top) + ) { updatedTrailing() } } updatedContent() } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt index 622f151f7..86d953922 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelStoreOwner import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.wallet.ui.R import eu.peernetwork.wallet.ui.extension.route +import eu.peernetwork.wallet.ui.mapper.format import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator.Companion.LocalTransactionsNavigator @Composable @@ -33,6 +34,7 @@ fun TransactionsList( listState: LazyListState, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, + onTransaction: () -> Unit, onClick: (String) -> Unit ) { val navigator = LocalTransactionsNavigator.current @@ -41,8 +43,9 @@ fun TransactionsList( limit = limit, lastUpdated = lastUpdated, provider = provider, + onClick = onTransaction, viewModelStoreOwner = viewModelStoreOwner - ) { component, items -> + ) { component, items, rate -> LazyColumn( state = listState, modifier = Modifier.fillMaxSize(), @@ -72,13 +75,18 @@ fun TransactionsList( } ?: title, description = transaction.message, createAt = transaction.createdAt, - price = "${transaction.amount.gross}", + price = if (isRecipient) { + transaction.amount.net.format() + } else { + transaction.amount.gross.format() + }, expanded = expanded, leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), - contentDescription = transaction.res?.let { stringResource(it) } - ?: title.text, + contentDescription = transaction.res?.let { + stringResource(it) + } ?: title.text, isRecipient = isRecipient ) { if (transaction.icon != null && transaction.res != null) { @@ -110,19 +118,30 @@ fun TransactionsList( ) { TransactionsSummeryItem( title = stringResource(R.string.transaction_amount_label), - price = "${transaction.amount.net}", + price = if (isRecipient) { + transaction.amount.gross.format() + } else { + transaction.amount.net.format() + }, ) transaction.fees?.let { - val peer = (transaction.tax.peer * 100).toInt() - val burn = (transaction.tax.burn * 100).toInt() + val peer = (rate.peer * 100).toInt() + val burn = (rate.burn * 100).toInt() + val invite = (rate.percentage * 100).toInt() TransactionsSummeryItem( title = stringResource(R.string.platform_charge, "$peer"), - price = "${it.peer}", + price = it.peer.format(), ) TransactionsSummeryItem( title = stringResource(R.string.burn_charge, "$burn"), - price = "${it.burn}", + price = it.burn.format(), ) + if (rate.percentage > 0) { + TransactionsSummeryItem( + title = stringResource(R.string.invite_charge, "$invite"), + price = it.commission.format(), + ) + } } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt index 0d4254061..723b86e7e 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsModule.kt @@ -5,12 +5,24 @@ import androidx.lifecycle.ViewModelProvider import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap +import eu.peernetwork.core.ui.annotation.UiBuilder import eu.peernetwork.core.ui.annotation.UiViewModel +import eu.peernetwork.core.ui.component.UiComponent +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.core.ui.factory.UiViewModelFactory +import eu.peernetwork.wallet.ui.rate.Rate import javax.inject.Provider @Module object TransactionsModule { + @Provides + @Transactions.Scope + fun provideBuilderFactory(factory: Map, + @JvmSuppressWildcards Provider>): UiComponentProvider.Factory { + return UiBuilderFactory(factory) + } + @Provides @Transactions.Scope fun provideViewModelFactory( @@ -24,4 +36,12 @@ object TransactionsModule { @Transactions.Scope @UiViewModel(TransactionsViewModel::class) fun viewModel(viewModel: TransactionsViewModel): ViewModel = viewModel + + @Provides + @Transactions.Scope + @IntoMap + @UiBuilder(Rate.Builder::class) + fun provideRateBuilder(component: Transactions.Component): UiComponent.Builder { + return Rate.Builder(component) + } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt index 858829fed..3e4e39ddb 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt @@ -1,5 +1,10 @@ package eu.peernetwork.wallet.ui.transactions +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State @@ -7,7 +12,10 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -15,9 +23,14 @@ import androidx.paging.compose.LazyPagingItems import eu.peernetwork.core.common.paging.Pageable import eu.peernetwork.core.ui.component.UiComponentProvider import eu.peernetwork.core.ui.design.luna.DesignPagingStream +import eu.peernetwork.core.ui.design.luna.DesignStream import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.core.ui.design.material.DesignScaffold import eu.peernetwork.core.ui.extension.builder +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.model.UiTax import eu.peernetwork.wallet.ui.model.UiTransaction +import eu.peernetwork.wallet.ui.rate.RateScreen @Composable @Suppress("UNCHECKED_CAST") @@ -26,7 +39,8 @@ fun TransactionsScreen( lastUpdated: State, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, - content: @Composable (Transactions.Component, LazyPagingItems) -> Unit + onClick: () -> Unit, + content: @Composable (Transactions.Component, LazyPagingItems, UiTax) -> Unit ) { val context = LocalContext.current val component = remember { @@ -53,14 +67,41 @@ fun TransactionsScreen( } } } val updatedContent by rememberUpdatedState(content) - DesignPagingStream( - state = derivedState, - loading = { TransactionsSkeleton(3) }, - error = { TransactionsError(it, component) { - viewModel(Pageable(0, limit)) - } } - ) { items -> - updatedContent(component, items) + RateScreen( + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { service, action -> + DesignStream( + state = service, + loading = { TransactionsSkeleton(3) }, + error = { TransactionsError(it, component) { action() } } + ) { rate -> + DesignScaffold( + alwaysReturn = true, + modifier = Modifier.fillMaxSize(), + header = { TransactionsButton(onClick = onClick) } + ) { + Column { + Text( + text = stringResource(R.string.transactions_label), + color = MaterialTheme.colorScheme.onBackground, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(top = 12.dp) + .padding(horizontal = 8.dp) + .padding(bottom = 10.dp) + ) + DesignPagingStream( + state = derivedState, + loading = { TransactionsSkeleton(3) }, + error = { TransactionsError(it, component) { + viewModel(Pageable(0, limit)) + } } + ) { items -> + updatedContent(component, items, rate.value) + } + } + } + } } LaunchedEffect(lastUpdated.value) { (status as? TransactionsViewModel.Status.Success?)?.let { diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/Transfer.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/Transfer.kt index 38511092f..c3ca6b8d7 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/Transfer.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/Transfer.kt @@ -4,7 +4,10 @@ import android.content.Context import androidx.lifecycle.ViewModelProvider import eu.peernetwork.core.common.provider.CoreProvider import eu.peernetwork.core.ui.component.UiComponent +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.wallet.ui.balance.Balance import eu.peernetwork.wallet.ui.provider.WalletProvider +import eu.peernetwork.wallet.ui.rate.Rate interface Transfer : WalletProvider, CoreProvider { @javax.inject.Scope @@ -16,7 +19,7 @@ interface Transfer : WalletProvider, CoreProvider { dependencies = [ Transfer::class ], modules = [ TransferModule::class ] ) - interface Component : Transfer { + interface Component : Transfer, Balance, Rate, UiComponentProvider { fun viewModelFactory(): ViewModelProvider.Factory } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferAmount.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferAmount.kt new file mode 100644 index 000000000..fef41f545 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferAmount.kt @@ -0,0 +1,107 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignTextField +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransferAmount( + state: TextFieldState, + isLoading: State, + modifier: Modifier = Modifier, + focusRequester: FocusRequester? = null +) { + Column( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.amount_entry), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Box( + contentAlignment = Alignment.CenterEnd, + modifier = Modifier.padding(top = 4.dp) + ) { + DesignTextField( + state = state, + enabled = !isLoading.value, + shape = CircleShape, + hint = stringResource(R.string.min_token_placeholder), + maxLength = 500, + maxLines = 1, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Decimal, + imeAction = ImeAction.Next + ), + contentPadding = PaddingValues( + top = 16.dp, + start = 16.dp, + end = 56.dp, + bottom = 16.dp, + ), + modifier = Modifier.then( + if (focusRequester != null) { + Modifier.focusRequester(focusRequester) + } else { + Modifier + } + ) + ) + Icon( + painter = painterResource(R.drawable.ic_peer_token), + contentDescription = stringResource(R.string.amount_entry), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(16.dp) + .align(Alignment.BottomEnd) + .size(24.dp) + ) + } + } +} + +@Preview +@Composable +fun PreviewTransferAmount() { + val state = remember { TextFieldState() } + val isLoading = remember { mutableStateOf(false) } + DesignTheme(isDarkMode = true) { + TransferAmount(state, isLoading) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferButton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferButton.kt deleted file mode 100644 index a09caa01b..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferButton.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.peernetwork.wallet.ui.transfer - -import androidx.compose.animation.core.Easing -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonColors -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.R - -@Composable -fun TransferButton( - enabled: Boolean, - isLoading: Boolean, - modifier: Modifier = Modifier, - durationMillis: Int = 1000, - easing: Easing = FastOutSlowInEasing, - onSubmit: () -> Unit -) { - val infiniteTransition = rememberInfiniteTransition() - val alpha by infiniteTransition.animateFloat( - initialValue = 1f, - targetValue = 0.3f, - animationSpec = infiniteRepeatable( - animation = tween(durationMillis, easing = easing), - repeatMode = RepeatMode.Reverse - ) - ) - IconButton( - onClick = onSubmit, - enabled = !isLoading && enabled, - modifier = Modifier.then(modifier) - .size(42.dp), - colors = IconButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContentColor = MaterialTheme.colorScheme.outlineVariant, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLowest - ) - ) { - Icon( - painter = painterResource(if (isLoading) { - R.drawable.ic_more - } else { - eu.peernetwork.wallet.ui.R.drawable.ic_transaction - }), - contentDescription = null, - tint = if (isLoading) { - MaterialTheme.colorScheme.outlineVariant - } else { - MaterialTheme.colorScheme.onBackground - }, - modifier = Modifier.size(20.dp) - .graphicsLayer { - this.alpha = if (isLoading) alpha else 1f - } - ) - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferCheckout.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferCheckout.kt new file mode 100644 index 000000000..69afb7d32 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferCheckout.kt @@ -0,0 +1,186 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.design.luna.DesignStream +import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.core.ui.extension.builder +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.balance.BalanceOverview +import eu.peernetwork.wallet.ui.extension.route +import eu.peernetwork.wallet.ui.rate.RateScreen +import eu.peernetwork.wallet.ui.transactions.TransactionsNavigator.Companion.LocalTransactionsNavigator +import eu.peernetwork.wallet.ui.transactions.TransactionsSummeryItem +import java.math.RoundingMode + +@Composable +fun TransferCheckout( + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, + onUserClicked: (String) -> Unit, + onBack: () -> Unit, + onFinish: () -> Unit +) { + val context = LocalContext.current + val navigator = LocalTransactionsNavigator.current + val component = remember { + provider.builder(Transfer.Builder::class.java).build(context) + } + val viewModel = viewModel( + modelClass = TransferViewModel::class.java, + viewModelStoreOwner = viewModelStoreOwner, + factory = component.viewModelFactory() + ) + val state by viewModel.state.collectAsStateWithLifecycle() + val status by viewModel.status.collectAsStateWithLifecycle() + val handleFinish by rememberUpdatedState(onFinish) + val handleUserClick by rememberUpdatedState(onUserClicked) + val streamState = remember { derivedStateOf { + if (status is TransferViewModel.Status.Empty) { + DesignStreamState.Default + } else { + DesignStreamState.Success( + (status as? TransferViewModel.Status.Confirmation?)?.detail + ?: (status as? TransferViewModel.Status.Checkout?)?.detail!! + ) + } + } } + val initialized = remember { mutableStateOf(false) } + val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + val isLoading = remember { derivedStateOf { state is TransferViewModel.State.Loading } } + val isSuccess = remember { derivedStateOf { state is TransferViewModel.State.Success } } + val enabled = remember { derivedStateOf { !isSuccess.value } } + val error = remember { derivedStateOf { (state as? TransferViewModel.State.Error?)?.error } } + DesignStream( + state = streamState, + default = { LaunchedEffect(Unit) { handleFinish() } } + ) { + val price = remember { derivedStateOf { + it.value.amount.setScale(6, RoundingMode.HALF_UP) + } } + RateScreen( + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { service, action -> + DesignStream(state = service) { rate -> + val peer = (rate.value.peer * 100).toInt() + val burn = (rate.value.burn * 100).toInt() + val invite = (rate.value.percentage * 100).toInt() + val total = remember { derivedStateOf { + price.value + + (price.value * rate.value.percentage.toBigDecimal()) + + (price.value * rate.value.burn.toBigDecimal()) + + (price.value * rate.value.peer.toBigDecimal()) + } } + TransferPreview( + price = price.value.toString(), + recipient = it.value.recipient, + message = it.value.message, + enabled = enabled, + isLoading = isLoading, + onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, + onBack = onBack, + onAuthorClicked = { handleUserClick(it.value.recipient.id) }, + onMessageClicked = { spec, value -> navigator.navigate(spec.route(value)) }, + onSend = { + viewModel.transfer( + price = it.value.amount, + recipient = it.value.recipient.id, + message = it.value.message, + ) + }, + rate = { + TransactionsSummeryItem( + title = stringResource(R.string.platform_charge, "$peer"), + price = "${(price.value * rate.value.peer.toBigDecimal()) + .setScale(8, RoundingMode.HALF_UP) + .toPlainString()}", + modifier = Modifier.padding(horizontal = 8.dp) + ) + TransactionsSummeryItem( + title = stringResource(R.string.burn_charge, "$burn"), + price = "${(price.value * rate.value.burn.toBigDecimal()) + .setScale(8, RoundingMode.HALF_UP) + .toPlainString()}", + modifier = Modifier.padding(horizontal = 8.dp) + ) + if (rate.value.percentage > 0) { + TransactionsSummeryItem( + title = stringResource(R.string.invite_charge, "$invite"), + price = "${(price.value * rate.value.percentage.toBigDecimal()) + .setScale(8, RoundingMode.HALF_UP)}", + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + TransactionsSummeryItem( + title = stringResource(R.string.total_amount), + color = MaterialTheme.colorScheme.onBackground, + price = "${total.value.setScale(6, RoundingMode.HALF_UP)}", + modifier = Modifier.padding(horizontal = 8.dp) + .padding(top = 4.dp) + ) + } + ) { + BalanceOverview( + color = MaterialTheme.colorScheme.surfaceContainerLowest, + offset = total.value, + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { component, model -> + LaunchedEffect(lastUpdated.longValue) { + if (initialized.value) { + model() + } else { + initialized.value = true + } + } + TransferSuccess( + showSheet = isSuccess, + modifier = Modifier.navigationBarsPadding() + .padding(16.dp) + ) { + model() + handleFinish() + } + } + } + TransferError( + error = error, + component = component, + modifier = Modifier.navigationBarsPadding() + .padding(16.dp), + onReset = { viewModel.cancel() } + ) { + viewModel.transfer( + price = it.value.amount, + recipient = it.value.recipient.id, + message = it.value.message, + ) + } + } + } + } + LaunchedEffect(Unit) { + (status as? TransferViewModel.Status.Confirmation?)?.let { + viewModel.checkout(it.detail) + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferError.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferError.kt new file mode 100644 index 000000000..eb2feb978 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferError.kt @@ -0,0 +1,162 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignButton +import eu.peernetwork.core.ui.design.luna.DesignStream +import eu.peernetwork.core.ui.design.luna.DesignStreamState +import eu.peernetwork.core.ui.design.luna.designTertiaryButtonColors +import eu.peernetwork.core.ui.design.material.DesignBottomSheetScaffold +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppDarkRed +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransferError( + error: State, + component: Transfer.Component, + modifier: Modifier = Modifier, + onReset: () -> Unit, + onRetry: () -> Unit +) { + val showSheet = remember { derivedStateOf { error.value != null } } + val isVisible = remember(showSheet.value) { mutableStateOf(showSheet.value) } + val handleReset by rememberUpdatedState(onReset) + DesignBottomSheetScaffold( + state = isVisible, + dismissable = true, + onDismiss = { + isVisible.value = false + handleReset() + } + ) { + val streamState = remember { derivedStateOf { + if (error.value == null) { + DesignStreamState.Default + } else { + DesignStreamState.Success(error.value) + } + } } + DesignStream(streamState) { stream -> + val message = stream.value?.message?.let { + component.resource().string(it) + } ?: stringResource(R.string.error_message) + TransferError( + message = message, + modifier = modifier, + onCancel = { isVisible.value = false }, + onRetry = onRetry + ) + } + } +} + +@Composable +fun TransferError( + message: String, + modifier: Modifier = Modifier, + onCancel: () -> Unit, + onRetry: () -> Unit +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(PeerAppDarkRed.copy(alpha = .1f)) + ) { + Icon( + painter = painterResource(R.drawable.ic_report), + contentDescription = stringResource(R.string.error_label), + tint = PeerAppDarkRed, + modifier = Modifier + .padding(8.dp) + .fillMaxSize() + ) + } + Text( + text = stringResource(R.string.error_label), + color = PeerAppDarkRed, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp) + ) + Text( + text = message, + color = MaterialTheme.colorScheme.outline, + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 10.dp) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .padding(top = 20.dp) + .padding(bottom = 6.dp) + ) { + DesignButton( + onClick = onCancel, + minHeight = 48.dp, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.SemiBold + ), + colors = designTertiaryButtonColors(), + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.weight(1f) + ) { Text(stringResource(R.string.cancel_label)) } + DesignButton( + onClick = onRetry, + minHeight = 48.dp, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.SemiBold + ), + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.weight(1f) + ) { Text(stringResource(R.string.retry_again_label)) } + } + } +} + +@Preview +@Composable +fun PreviewTransferError() { + DesignTheme(isDarkMode = true) { + TransferError( + message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + modifier = Modifier.padding(20.dp), + onCancel = {} + ) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferForm.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferForm.kt deleted file mode 100644 index 17de2d0e8..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferForm.kt +++ /dev/null @@ -1,95 +0,0 @@ -package eu.peernetwork.wallet.ui.transfer - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.design.luna.DesignTextField -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.wallet.ui.R -import java.math.BigDecimal - -@Composable -fun TransferForm( - state: TextFieldState, - tax: Int, - isLoading: Boolean = false, - onSubmit: (BigDecimal) -> Unit, -) { - val isValidated = remember { derivedStateOf { state.text.toString().toBigDecimalOrNull() != null } } - val handleSubmission by rememberUpdatedState { - state.text.toString().toBigDecimalOrNull()?.let { - onSubmit(it) - } - } - Column { - Box(contentAlignment = Alignment.CenterEnd) { - DesignTextField( - state = state, - enabled = !isLoading, - shape = RoundedCornerShape(24.dp), - hint = stringResource(R.string.amount_label), - maxLength = 500, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Decimal, - imeAction = ImeAction.Send - ), - onKeyboardAction = KeyboardActions( - onSend = { handleSubmission() } - ), - contentPadding = PaddingValues( - top = 16.dp, - start = 16.dp, - end = 56.dp, - bottom = 16.dp, - ), - ) - TransferButton( - enabled = isValidated.value, - isLoading = isLoading, - modifier = Modifier - .padding(4.dp) - .align(Alignment.BottomEnd), - ) { handleSubmission() } - } - Text( - stringResource(R.string.transfer_disclaimer, tax), - modifier = Modifier - .padding(top = 12.dp) - .padding(horizontal = 16.dp), - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline, - ), - ) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewTransferForm() { - DesignTheme { - val state = remember { TextFieldState() } - Box(Modifier.padding(16.dp)) { - TransferForm(state, 4) {} - } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferMessage.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferMessage.kt new file mode 100644 index 000000000..d79a74ba7 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferMessage.kt @@ -0,0 +1,171 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignRichText +import eu.peernetwork.core.ui.design.luna.DesignTextField +import eu.peernetwork.core.ui.extension.value +import eu.peernetwork.core.ui.mapper.annotate +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppDarkRed +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransferMessage( + state: TextFieldState, + modifier: Modifier = Modifier, + maxLength: Int = 500 +) { + Column( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_message), + contentDescription = stringResource(R.string.transaction_message_label), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onBackground + ) + Text( + text = stringResource(R.string.transaction_message_label), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = stringResource(R.string.message_counter_label, state.text.length, maxLength), + style = MaterialTheme.typography.labelSmall, + color = if (state.text.length >= maxLength) { + PeerAppDarkRed + } else { + MaterialTheme.colorScheme.outline + }, + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + DesignTextField( + state = state, + contentPadding = PaddingValues( + top = 16.dp, + start = 16.dp, + end = 16.dp, + bottom = 56.dp, + ), + maxLength = maxLength, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text), + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth() + .padding(top = 8.dp) + .heightIn(max = 200.dp), + hint = stringResource(R.string.transaction_message_placeholder), + visualTransformation = VisualTransformation { + TransformedText( + text = state.value.annotate(), + offsetMapping = OffsetMapping.Identity + ) + }, + ) + } +} + +@Composable +fun TransferMessage( + message: String, + modifier: Modifier = Modifier, + onClick: (DesignRichText, String) -> Unit = { _,_ -> }, +) { + Column( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(R.drawable.ic_message), + contentDescription = stringResource(R.string.message_title), + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onBackground + ) + Text( + text = stringResource(R.string.message_title), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + Box(modifier = Modifier.padding(top = 8.dp) + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colorScheme.surfaceContainerLow)) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.padding(top = 8.dp) + .padding(horizontal = 8.dp) + .heightIn(min = 56.dp) + ) { + DesignRichText( + text = message.annotate(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onBackground, + maxLines = 4, + onClick = onClick + ) + } + } +} + +@Preview +@Composable +fun PreviewTransferMessage() { + val state = remember { TextFieldState() } + DesignTheme(isDarkMode = true) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + TransferMessage(state) + TransferMessage("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vel egestas urna, vitae molestie neque.") + } + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferModule.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferModule.kt index cbb60826c..2bc4ce5c9 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferModule.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferModule.kt @@ -5,12 +5,25 @@ import androidx.lifecycle.ViewModelProvider import dagger.Module import dagger.Provides import dagger.multibindings.IntoMap +import eu.peernetwork.core.ui.annotation.UiBuilder import eu.peernetwork.core.ui.annotation.UiViewModel +import eu.peernetwork.core.ui.component.UiComponent +import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.core.ui.factory.UiBuilderFactory import eu.peernetwork.core.ui.factory.UiViewModelFactory +import eu.peernetwork.wallet.ui.balance.Balance +import eu.peernetwork.wallet.ui.rate.Rate import javax.inject.Provider @Module object TransferModule { + @Provides + @Transfer.Scope + fun provideBuilderFactory(factory: Map, + @JvmSuppressWildcards Provider>): UiComponentProvider.Factory { + return UiBuilderFactory(factory) + } + @Provides @Transfer.Scope fun provideViewModelFactory( @@ -24,4 +37,20 @@ object TransferModule { @Transfer.Scope @UiViewModel(TransferViewModel::class) fun viewModel(viewModel: TransferViewModel): ViewModel = viewModel -} \ No newline at end of file + + @Provides + @Transfer.Scope + @IntoMap + @UiBuilder(Balance.Builder::class) + fun provideBalanceBuilder(component: Transfer.Component): UiComponent.Builder { + return Balance.Builder(component) + } + + @Provides + @Transfer.Scope + @IntoMap + @UiBuilder(Rate.Builder::class) + fun provideRateBuilder(component: Transfer.Component): UiComponent.Builder { + return Rate.Builder(component) + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPage.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPage.kt new file mode 100644 index 000000000..19cad2f31 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPage.kt @@ -0,0 +1,172 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignButton +import eu.peernetwork.core.ui.design.luna.DesignRefreshScaffold +import eu.peernetwork.core.ui.design.luna.designSecondaryButtonColors +import eu.peernetwork.core.ui.design.material.DesignScaffold +import eu.peernetwork.core.ui.extension.value +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.model.UiRecipient +import java.math.BigDecimal + +@Composable +fun TransferPage( + balance: State, + disable: MutableState, + isLoading: MutableState, + recipient: MutableState, + focusRequester: FocusRequester, + onSearch: () -> Unit, + onRefresh: () -> Unit, + onUserClicked: (String) -> Unit, + onSubmit: (UiRecipient, BigDecimal, String) -> Unit, + content: @Composable () -> Unit, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val animatable = remember { Animatable(0f) } + val handleSubmit by rememberUpdatedState(onSubmit) + val updatedContent by rememberUpdatedState(content) + val isRefreshing = remember { mutableStateOf(false) } + val amount by rememberSaveable(stateSaver = TextFieldState.Saver) { mutableStateOf(TextFieldState()) } + val isEnabled = remember { derivedStateOf { + val currentAmount = amount.value.toBigDecimalOrNull() + recipient.value != null + && currentAmount != null + && currentAmount > BigDecimal.ZERO + && currentAmount <= balance.value + } } + val message by rememberSaveable(stateSaver = TextFieldState.Saver) { mutableStateOf(TextFieldState()) } + DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { + DesignScaffold( + alwaysReturn = true, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 18.dp), + header = { + Box( + modifier = Modifier + .padding(top = 8.dp) + .padding(bottom = 10.dp) + ) { updatedContent() } + } + ) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + .imePadding() + ) { + Crossfade(recipient.value) { targetState -> + targetState?.let { + TransferRecipient( + recipient = it, + onEdit = onSearch, + onClick = onUserClicked + ) + } ?: TransferRecipient(onClick = onSearch) + } + TransferAmount( + state = amount, + isLoading = isLoading, + modifier = Modifier.padding(top = 10.dp), + focusRequester = focusRequester, + ) + TransferMessage( + state = message, + modifier = Modifier.padding(top = 10.dp), + ) + DesignButton( + enabled = isEnabled.value, + onClick = { + recipient.value?.let { + handleSubmit( + it, + amount.value.toBigDecimal(), + message.value + ) + } + }, + minHeight = 48.dp, + contentPadding = PaddingValues(horizontal = 16.dp), + colors = designSecondaryButtonColors(), + modifier = Modifier + .padding(vertical = 16.dp) + .fillMaxWidth() + ) { Text(stringResource(R.string.continue_label)) } + } + } + } + LaunchedEffect(Unit) { + snapshotFlow { disable.value } + .collect { isDisabled -> + if (!isDisabled) { + recipient.value?.let { + focusRequester.requestFocus() + keyboardController?.show() + } + } + } + } + LaunchedEffect(Unit) { + animatable.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 1) + ) + recipient.value?.let { + focusRequester.requestFocus() + } + } +} + +@Preview +@Composable +fun PreviewTransferPage() { + val disable = remember { mutableStateOf(false) } + val balance = remember { mutableStateOf(BigDecimal.ZERO) } + val isLoading = remember { mutableStateOf(false) } + val recipient = remember { mutableStateOf(null) } + val focusRequester = FocusRequester() + DesignTheme(isDarkMode = true) { + TransferPage( + balance = balance, + disable = disable, + isLoading = isLoading, + recipient = recipient, + focusRequester = focusRequester, + onSearch = {}, + onRefresh = {}, + onUserClicked = {}, + onSubmit = { _,_,_ -> } + ) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPreview.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPreview.kt new file mode 100644 index 000000000..dcaf1e934 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferPreview.kt @@ -0,0 +1,135 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.design.luna.DesignButton +import eu.peernetwork.core.ui.design.luna.DesignOutlineButton +import eu.peernetwork.core.ui.design.luna.DesignRefreshScaffold +import eu.peernetwork.core.ui.design.luna.DesignRichText +import eu.peernetwork.core.ui.design.material.DesignScaffold +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R +import eu.peernetwork.wallet.ui.extension.normalizeWhitespaces +import eu.peernetwork.wallet.ui.model.UiRecipient + +@Composable +fun TransferPreview( + price: String, + message: String, + recipient: UiRecipient, + enabled: State, + isLoading: State, + onRefresh: () -> Unit, + onBack: () -> Unit, + onSend: () -> Unit, + onAuthorClicked: () -> Unit, + onMessageClicked: (DesignRichText, String) -> Unit, + rate: @Composable ColumnScope.() -> Unit = {}, + content: @Composable () -> Unit, +) { + val isRefreshing = remember { mutableStateOf(false) } + val updatedContent by rememberUpdatedState(content) + DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { + DesignScaffold( + alwaysReturn = true, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 18.dp), + header = { + Box( + modifier = Modifier + .padding(top = 8.dp) + .padding(bottom = 10.dp) + ) { updatedContent() } + } + ) { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + .imePadding() + ) { + TransferRecipient( + recipient = recipient, + onClick = onAuthorClicked + ) + TransferSummary( + amount = price, + modifier = Modifier.padding(top = 10.dp), + content = rate + ) + if (message.isNotEmpty()) { + TransferMessage( + message = message.normalizeWhitespaces(), + onClick = onMessageClicked, + modifier = Modifier.padding(top = 10.dp), + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(vertical = 16.dp) + ) { + DesignOutlineButton( + onClick = onBack, + minHeight = 48.dp, + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.weight(1f), + ) { Text(stringResource(R.string.back_label)) } + DesignButton( + onClick = onSend, + minHeight = 48.dp, + enabled = !isLoading.value && enabled.value, + isLoading = isLoading.value, + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier.weight(1f), + ) { Text(stringResource(R.string.send_label)) } + } + } + } + } +} + +@Preview +@Composable +fun PreviewTransferPreview() { + DesignTheme(isDarkMode = true) { + val enabled = remember { mutableStateOf(false) } + val isLoading = remember { mutableStateOf(false) } + val recipient = UiRecipient( + id = "1", + username = "constantine", + slug = "1234", + imageUrl = "http:localhost" + ) + TransferPreview( + price = "1 760", + recipient = recipient, + message = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vel egestas urna, vitae molestie neque.", + enabled = enabled, + isLoading = isLoading, + onRefresh = {}, + onBack = {}, + onSend = {}, + onAuthorClicked = {}, + onMessageClicked = { _,_ -> } + ) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferRecipient.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferRecipient.kt index 36f7537ce..2422690d6 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferRecipient.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferRecipient.kt @@ -1,13 +1,15 @@ package eu.peernetwork.wallet.ui.transfer -import android.content.res.Configuration +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -17,91 +19,163 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.design.luna.DesignImage -import eu.peernetwork.core.ui.design.luna.DesignAvatar -import eu.peernetwork.core.ui.design.material.DesignCard -import eu.peernetwork.core.ui.design.material.DesignDetailLayout -import eu.peernetwork.core.ui.extension.annotate +import eu.peernetwork.core.ui.extension.tap import eu.peernetwork.core.ui.theme.DesignTheme import eu.peernetwork.wallet.ui.R import eu.peernetwork.wallet.ui.model.UiRecipient -import java.util.UUID @Composable fun TransferRecipient( recipient: UiRecipient, - onClick: () -> Unit, - icon: @Composable () -> Unit + onClick: () -> Unit ) { - val slugTag = "#${recipient.slug}" - val updatedIcon by rememberUpdatedState(icon) - DesignCard( - shape = CircleShape, - contentPadding = PaddingValues(14.dp), - color = MaterialTheme.colorScheme.surfaceContainerLowest + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(18.dp) ) { - DesignDetailLayout( - lead = { - DesignAvatar { - DesignImage( - label = recipient.username, - imageUrl = recipient.imageUrl, - size = 36.dp, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline, - ), - color = MaterialTheme.colorScheme.background, - modifier = Modifier - .clip(CircleShape) - .wrapContentSize() - .clipToBounds() - .clickable(role = Role.Button, onClick = onClick) - ) - } }, - verticalAlignment = Alignment.CenterVertically - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - "${recipient.username} $slugTag".annotate( - slugTag, - style = MaterialTheme.typography.bodySmall.toSpanStyle().copy( - color = MaterialTheme.colorScheme.outline - ) - ), - modifier = Modifier - .padding(start = 12.dp) - .weight(1f), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.bodyMedium, - ) - updatedIcon() - } + Text( + text = stringResource(R.string.sending_to), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.weight(1f) + ) + Text( + text = buildAnnotatedString { + append(recipient.username) + append(" ") + withStyle(SpanStyle( + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.Medium, + fontSize = MaterialTheme.typography.labelMedium.fontSize + )) { append("#${recipient.slug}") } + }, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.tap(onClick) + ) + } +} + +@Composable +fun TransferRecipient( + recipient: UiRecipient, + onEdit: () -> Unit, + onClick: (String) -> Unit, +) { + val handleClick by rememberUpdatedState(onClick) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Column(modifier = Modifier.weight(1f) + .padding(start = 8.dp)) { + Text( + text = stringResource(R.string.send_to), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = buildAnnotatedString { + append(recipient.username) + append(" ") + withStyle(SpanStyle( + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.Medium, + fontSize = MaterialTheme.typography.labelMedium.fontSize + )) { append("#${recipient.slug}") } + }, + style = MaterialTheme.typography.bodySmall, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.tap { + handleClick(recipient.id) + } + ) } + Icon( + painter = painterResource(R.drawable.ic_edit), + contentDescription = stringResource(R.string.transfer_label), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(vertical = 4.dp) + .size(32.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + .clickable(onClick = onEdit) + .padding(8.dp) + ) } } @Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewTransferRecipient() { - DesignTheme { - val recipient = UiRecipient( - id = UUID.randomUUID().toString(), - slug = "1234", - username = "johnDoe", - imageUrl = "http://localhost" +fun TransferRecipient(onClick: () -> Unit) { + Column( + modifier = Modifier.fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Text( + text = stringResource(R.string.recipient_username), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 8.dp) ) - TransferRecipient(recipient, {}) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 8.dp) + .fillMaxWidth() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + .clickable(onClick = onClick) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.recipient_search), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.weight(1f) + ) Icon( - painter = painterResource(R.drawable.ic_transfer), - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.outline + painter = painterResource(R.drawable.ic_recipient_search), + contentDescription = stringResource(R.string.recipient_search), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(18.dp) + ) + } + } +} + +@Preview +@Composable +fun PreviewTransferRecipient() { + DesignTheme(isDarkMode = true) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + val recipient = UiRecipient( + id = "1", + username = "constantine", + slug = "1234", + imageUrl = "http:localhost" ) + TransferRecipient {} + TransferRecipient(recipient, {}) {} + TransferRecipient(recipient) {} } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferScreen.kt index f225dde1e..3738ddf61 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferScreen.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferScreen.kt @@ -1,55 +1,44 @@ package eu.peernetwork.wallet.ui.transfer -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.luna.DesignImage -import eu.peernetwork.core.ui.design.luna.DesignAvatar -import eu.peernetwork.core.ui.design.material.DesignDetailLayout import eu.peernetwork.core.ui.extension.builder -import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.balance.BalanceOverview +import eu.peernetwork.wallet.ui.balance.BalanceScreen +import eu.peernetwork.wallet.ui.balance.BalanceViewModel import eu.peernetwork.wallet.ui.model.UiRecipient -import eu.peernetwork.wallet.ui.model.UiTransfer -import java.util.UUID +import eu.peernetwork.wallet.ui.model.UiTransferDetail +import java.math.BigDecimal + +sealed interface TransferState { + data object Default : TransferState + data class Transfer(val recipient: UiRecipient) : TransferState +} @Composable fun TransferScreen( - tax: Int, - recipient: UiRecipient, + disable: MutableState, + recipient: MutableState, + focusRequester: FocusRequester, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner, - onClear: () -> Unit, - onTransfer: (UiTransfer) -> Unit, - onRecipientClick: (UiRecipient) -> Unit, + onUserClicked: (String) -> Unit, + onClick: () -> Unit, + onSubmit: () -> Unit ) { val context = LocalContext.current val component = remember { @@ -60,90 +49,56 @@ fun TransferScreen( viewModelStoreOwner = viewModelStoreOwner, factory = component.viewModelFactory() ) - val amount by rememberSaveable(stateSaver = TextFieldState.Saver) { mutableStateOf(TextFieldState()) } - val handleOnTransfer by rememberUpdatedState(onTransfer) - TransferScreen( - recipient = recipient, - onClear = onClear, - onClick = onRecipientClick - ) { - TransferForm(amount, tax) { - viewModel.reset() - handleOnTransfer(UiTransfer(recipient.id, it)) - } - } -} - -@Composable -fun TransferScreen( - recipient: UiRecipient, - modifier: Modifier = Modifier, - onClear: () -> Unit = {}, - onClick: (UiRecipient) -> Unit = {}, - content: @Composable () -> Unit -) { - val handleOnClick by rememberUpdatedState(onClick) - val updatedContent by rememberUpdatedState(content) - Column(modifier) { - DesignDetailLayout( - lead = { - DesignAvatar { - DesignImage( - label = recipient.username, - imageUrl = recipient.imageUrl, - size = 36.dp, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline, - ), - color = MaterialTheme.colorScheme.background, - modifier = Modifier.clip(CircleShape) - .wrapContentSize() - .clipToBounds() - .clickable(role = Role.Button) { - handleOnClick(recipient) - } + val status by viewModel.status.collectAsStateWithLifecycle() + val handleSubmit by rememberUpdatedState(onSubmit) + val isLoading = remember { mutableStateOf(false) } + val initialized = remember { mutableStateOf(false) } + val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } + BalanceScreen( + provider = component, + viewModelStoreOwner = viewModelStoreOwner, + ) { _, model -> + val balance = remember { derivedStateOf { + (model.state.value as? BalanceViewModel.State.Success?)?.wallet?.balance + ?: BigDecimal.ZERO + } } + TransferPage( + balance = balance, + disable = disable, + isLoading = isLoading, + recipient = recipient, + focusRequester = focusRequester, + onSearch = onClick, + onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, + onUserClicked = onUserClicked, + onSubmit = { recipient, amount, message -> + viewModel.proceed( + detail = UiTransferDetail( + amount = amount, + message = message, + recipient = recipient ) - } - }, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 12.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - recipient.username, - modifier = Modifier.padding(start = 12.dp), - style = MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onBackground, - ), - ) - Spacer(modifier = Modifier.weight(1f)) - Icon( - painter = painterResource(eu.peernetwork.core.ui.R.drawable.ic_plus), - contentDescription = null, - modifier = Modifier.size(12.dp).graphicsLayer { - rotationZ = 45f - }.clickable(role = Role.Button, onClick = onClear), - tint = MaterialTheme.colorScheme.outlineVariant ) } + ) { + BalanceOverview( + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) { component, model -> + LaunchedEffect(lastUpdated.longValue) { + if (initialized.value) { + model() + } else { + initialized.value = true + } + } + } } - updatedContent() } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewTransferScreen() { - DesignTheme { - val state = remember { TextFieldState() } - val recipient = UiRecipient( - id = UUID.randomUUID().toString(), - slug = "1234", - username = "johnDoe", - imageUrl = "http://localhost" - ) - TransferScreen(recipient, Modifier.padding(16.dp)) { - TransferForm(state, 4) {} + LaunchedEffect(Unit) { viewModel.reset() } + LaunchedEffect(status) { + if (status is TransferViewModel.Status.Confirmation) { + handleSubmit() } } } diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt deleted file mode 100644 index b0eac96cd..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheet.kt +++ /dev/null @@ -1,217 +0,0 @@ -package eu.peernetwork.wallet.ui.transfer - -import android.content.res.Configuration -import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel -import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.material.DesignStatefulScaffoldState -import eu.peernetwork.core.ui.design.material.DesignBottomSheetScaffold -import eu.peernetwork.core.ui.extension.builder -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.wallet.ui.R -import eu.peernetwork.wallet.ui.model.UiRecipient -import eu.peernetwork.wallet.ui.model.UiTransfer -import java.math.BigDecimal -import java.util.UUID -import eu.peernetwork.wallet.ui.compose.TickButton -import eu.peernetwork.wallet.ui.compose.ToastLayout - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -fun TransferSheet( - transfer: MutableState, - provider: UiComponentProvider, - viewModelStoreOwner: ViewModelStoreOwner, - recipient: UiRecipient, - onFinish: () -> Unit = {}, - onRecipientClick: (UiRecipient) -> Unit -) { - val context = LocalContext.current - val component = remember { - provider.builder(Transfer.Builder::class.java).build(context) - } - val viewModel = viewModel( - modelClass = TransferViewModel::class.java, - viewModelStoreOwner = viewModelStoreOwner, - factory = component.viewModelFactory() - ) - val state by viewModel.state.collectAsStateWithLifecycle() - val derivedState = remember { - derivedStateOf { - when (state) { - TransferViewModel.State.Empty -> DesignStatefulScaffoldState.Empty - TransferViewModel.State.Loading -> DesignStatefulScaffoldState.Loading - is TransferViewModel.State.Success -> { - DesignStatefulScaffoldState.Success( - (state as TransferViewModel.State.Success).transfer - ) - } - is TransferViewModel.State.Error -> DesignStatefulScaffoldState.Error( - (state as TransferViewModel.State.Error).error - ) - } - } - } - val isLoading = remember { derivedStateOf { state is TransferViewModel.State.Loading } } - val isSuccessful = remember { derivedStateOf { derivedState.value is DesignStatefulScaffoldState.Success<*> } } - val error = remember { derivedStateOf { - (derivedState.value as? DesignStatefulScaffoldState.Error?)?.error?.message?.let { - component.resource().string(it) - } - }} - val showRecipient = remember { mutableStateOf(false) } - val showSheet = remember(transfer.value) { mutableStateOf(transfer.value != null) } - val handleOnFinish by rememberUpdatedState(onFinish) - val handleOnRecipientClick by rememberUpdatedState(onRecipientClick) - DesignBottomSheetScaffold( - state = showSheet, - onDismiss = { - if (isSuccessful.value) { - viewModel.reset() - handleOnFinish() - } else if (showRecipient.value) { - showRecipient.value = false - handleOnRecipientClick(recipient) - } - transfer.value = null - }, - ) { - Crossfade(transfer.value) { target -> - if (target != null) { - TransferSheet( - isLoading, - error, - isSuccessful, - recipient, - target.token, - { - showSheet.value = false - showRecipient.value = true } - ) { - if (isSuccessful.value) { - showSheet.value = false - } else { - viewModel.transfer(target.recipient, target.token) - } - } - } - } - } -} - -@Composable -fun TransferSheet( - state: State, - error: State, - isSuccessful: State, - recipient: UiRecipient, - token: BigDecimal, - onClick: () -> Unit = {}, - onSubmit: () -> Unit -) { - Crossfade(isSuccessful.value) { target -> - if (target) { - Box { - TransferSheetScaffold( - state = state, - error = error, - title = stringResource(R.string.sent_label), - recipient = recipient, - token = token, - action = stringResource(R.string.close_label), - onClick = onClick, - onSubmit = onSubmit, - ) { - TickButton( - modifier = Modifier.size(36.dp), - rawRes = R.raw.tick - ) - } - ToastLayout() - } - } else { - TransferSheetScaffold( - state = state, - error = error, - title = stringResource(R.string.recipient_label), - recipient = recipient, - token = token, - action = stringResource(R.string.send_label), - onClick = onClick, - onSubmit = onSubmit, - ) { - Icon( - painter = painterResource(R.drawable.ic_transfer), - contentDescription = null, - modifier = Modifier.size(18.dp), - tint = MaterialTheme.colorScheme.outlineVariant - ) - } - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewTransferSheet() { - DesignTheme { - Column { - val recipient = UiRecipient( - id = UUID.randomUUID().toString(), - slug = "1234", - username = "johnDoe", - imageUrl = "http://localhost" - ) - TransferSheet( - state = remember { mutableStateOf(true) }, - error = remember { mutableStateOf(null) }, - isSuccessful = remember { mutableStateOf(false) }, - recipient = recipient, - token = BigDecimal(1.0), - {} - ) {} - Spacer(modifier = Modifier.height(16.dp)) - TransferSheet( - state = remember { mutableStateOf(false) }, - error = remember { mutableStateOf("Error message...") }, - isSuccessful = remember { mutableStateOf(false) }, - recipient = recipient, - token = BigDecimal(1.0), - {} - ) {} - Spacer(modifier = Modifier.height(16.dp)) - TransferSheet( - state = remember { mutableStateOf(false) }, - error = remember { mutableStateOf(null) }, - isSuccessful = remember { mutableStateOf(true) }, - recipient = recipient, - token = BigDecimal(1.0), - {} - ) {} - } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheetScaffold.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheetScaffold.kt deleted file mode 100644 index a0266a1ca..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSheetScaffold.kt +++ /dev/null @@ -1,136 +0,0 @@ -package eu.peernetwork.wallet.ui.transfer - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import eu.peernetwork.core.ui.design.luna.DesignButton -import eu.peernetwork.core.ui.design.material.DesignLabel -import eu.peernetwork.core.ui.theme.DesignTheme -import eu.peernetwork.wallet.ui.R -import eu.peernetwork.wallet.ui.model.UiRecipient -import java.math.BigDecimal -import java.math.RoundingMode -import java.util.UUID - -@Composable -fun TransferSheetScaffold( - state: State, - error: State, - title: String, - recipient: UiRecipient, - token: BigDecimal, - action: String, - onClick: () -> Unit = {}, - onSubmit: () -> Unit, - icon: @Composable () -> Unit -) { - var errorMessage by remember { mutableStateOf(error.value) } - Column( - modifier = Modifier - .padding( - vertical = 8.dp, - horizontal = 16.dp, - ) - .navigationBarsPadding() - .statusBarsPadding() - ) { - Text( - stringResource(R.string.amount_text), - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.outline - ), - modifier = Modifier.padding(horizontal = 12.dp) - ) - Text( - "${token.setScale(4, RoundingMode.HALF_UP)}", - modifier = Modifier.padding(start = 12.dp), - style = MaterialTheme.typography.titleMedium.copy( - color = MaterialTheme.colorScheme.onBackground, - ), - ) - Spacer(modifier = Modifier.height(6.dp)) - Text( - title, - color = MaterialTheme.colorScheme.outline, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(horizontal = 12.dp) - ) - Spacer(modifier = Modifier.height(8.dp)) - DesignLabel( - visible = error.value != null, - onAnimationEnd = { - if (it) { - errorMessage = error.value - } - }, - label = { - errorMessage?.run { - Text( - text = this, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.error - ), - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) - } - } - ) { TransferRecipient(recipient, onClick, icon) } - Spacer(modifier = Modifier.height(14.dp)) - DesignButton( - onClick = onSubmit, - isLoading = state.value, - enabled = !state.value, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = action, - fontWeight = FontWeight.Bold, - style = MaterialTheme.typography.bodyMedium - ) - } - Spacer(modifier = Modifier.height(24.dp)) - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -fun PreviewTransferSheetScaffold() { - DesignTheme { - val recipient = UiRecipient( - id = UUID.randomUUID().toString(), - slug = "1234", - username = "johnDoe", - imageUrl = "http://localhost" - ) - TransferSheetScaffold( - state = remember { mutableStateOf(true) }, - error = remember { mutableStateOf("Hello, world!") }, - title = stringResource(R.string.recipient_label), - recipient = recipient, - action = stringResource(R.string.close_label), - token = BigDecimal(1.0), - onClick = {}, - onSubmit = {}, - ) { } - } -} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSuccess.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSuccess.kt new file mode 100644 index 000000000..6fbfbc1eb --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSuccess.kt @@ -0,0 +1,136 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import eu.peernetwork.core.ui.design.luna.DesignButton +import eu.peernetwork.core.ui.design.material.DesignBottomSheetScaffold +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppLightGreen +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransferSuccess( + showSheet: State, + modifier: Modifier = Modifier, + onConfirm: () -> Unit, +) { + val handleConfirm by rememberUpdatedState(onConfirm) + val isVisible = remember(showSheet.value) { mutableStateOf(showSheet.value) } + DesignBottomSheetScaffold( + state = isVisible, + dismissable = false, + onDismiss = { + isVisible.value = false + handleConfirm() + } + ) { + TransferSuccess( + modifier = modifier, + onConfirm = { isVisible.value = false } + ) + } +} + +@Composable +fun TransferSuccess( + modifier: Modifier = Modifier, + onConfirm: () -> Unit, +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(PeerAppLightGreen.copy(alpha = .1f)) + ) { + TransferSuccessIcon( + Modifier + .padding(8.dp) + .fillMaxSize() + ) + } + Text( + text = stringResource(R.string.transaction_message), + color = PeerAppLightGreen, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 12.dp) + ) + DesignButton( + onClick = onConfirm, + minHeight = 48.dp, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = FontWeight.SemiBold + ), + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .padding(bottom = 6.dp) + ) { Text(stringResource(R.string.got_it_label)) } + } +} + +@Composable +fun TransferSuccessIcon( + modifier: Modifier = Modifier, + rawRes: Int = R.raw.tick +) { + var playing by remember { mutableStateOf(false) } + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(rawRes)) + val progress by animateLottieCompositionAsState( + composition = composition, + isPlaying = playing, + reverseOnRepeat = true, + iterations = LottieConstants.IterateForever, + speed = 2f + ) + LaunchedEffect(Unit) { playing = true } + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = modifier + ) +} + +@Preview +@Composable +fun PreviewCheckoutModal() { + DesignTheme(isDarkMode = true) { + TransferSuccess(modifier = Modifier.padding(20.dp)) {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSummary.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSummary.kt new file mode 100644 index 000000000..97eb94a3f --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferSummary.kt @@ -0,0 +1,145 @@ +package eu.peernetwork.wallet.ui.transfer + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import eu.peernetwork.core.ui.extension.tap +import eu.peernetwork.core.ui.theme.DesignTheme +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransferSummary( + amount: String, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + val isVisible = rememberSaveable { mutableStateOf(true) } + val rotation = animateFloatAsState( + targetValue = if (isVisible.value) -90f else 0f, + animationSpec = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ), + label = "TransactionsItemTrailingIndication" + ) + val updatedContent by rememberUpdatedState(content) + Column( + modifier = Modifier.then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + .padding(vertical = 4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background) + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.total_amount), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(horizontal = 8.dp) + .weight(1f) + ) + Text( + text = amount, + fontWeight = FontWeight.SemiBold, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Icon( + painter = painterResource(R.drawable.ic_peer_token), + contentDescription = stringResource(R.string.amount_entry), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(20.dp) + ) + } + Row( + modifier = Modifier + .tap { isVisible.value = !isVisible.value } + .padding(horizontal = 8.dp) + .padding(top = 8.dp) + ) { + Text( + text = stringResource(R.string.transfer_fee), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.weight(1f) + ) + Icon( + painter = painterResource(R.drawable.ic_caret_right), + contentDescription = null, + tint = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.size(16.dp) + .graphicsLayer { rotationZ = rotation.value } + ) + } + AnimatedContent( + targetState = isVisible.value, + transitionSpec = { + fadeIn(animationSpec = tween(300)) + + expandVertically( + animationSpec = tween(300), + expandFrom = Alignment.Top + ) togetherWith + fadeOut(animationSpec = tween(200)) + + shrinkVertically( + animationSpec = tween(200), + shrinkTowards = Alignment.Top + ) + }, + ) { visible -> + if (visible) { + Column(modifier = Modifier.padding(vertical = 4.dp)) { + updatedContent() + } + } + } + } +} + +@Preview +@Composable +fun PreviewTransferSummary() { + DesignTheme(isDarkMode = true) { + TransferSummary(amount = "1 760") {} + } +} diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferViewModel.kt index 3da38f202..31f88ef98 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferViewModel.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transfer/TransferViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import eu.peernetwork.wallet.domain.usecase.TransferUsecase import eu.peernetwork.wallet.ui.mapper.mapFromDomain import eu.peernetwork.wallet.ui.model.UiTransfer +import eu.peernetwork.wallet.ui.model.UiTransferDetail import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -15,28 +16,62 @@ import javax.inject.Inject class TransferViewModel @Inject constructor( private val usecase: TransferUsecase ) : ViewModel() { - private val mutableState = MutableStateFlow(State.Empty) + private val _status = MutableStateFlow(Status.Empty) - val state: StateFlow = mutableState.asStateFlow() + private val _state = MutableStateFlow(State.Empty) - fun transfer(recipient: String, price: BigDecimal) { + val state: StateFlow = _state.asStateFlow() + + val status: StateFlow = _status.asStateFlow() + + fun transfer(recipient: String, price: BigDecimal, message: String? = null) { viewModelScope.launch { - mutableState.tryEmit(State.Loading) + _state.tryEmit(State.Loading) try { - val result = usecase(TransferUsecase.Parameter(recipient, price)).mapFromDomain() - mutableState.tryEmit(State.Success(result)) + val result = usecase( + TransferUsecase.Parameter( + recipient = recipient, + tokens = price, + message = message + )).mapFromDomain() + _state.tryEmit(State.Success(result)) } catch (error: Throwable) { - mutableState.tryEmit(State.Error(error)) + _state.tryEmit(State.Error(error)) } } } + fun proceed(detail: UiTransferDetail) { + viewModelScope.launch { + _status.tryEmit(Status.Confirmation(detail)) + } + } + + fun checkout(detail: UiTransferDetail) { + viewModelScope.launch { + _status.tryEmit(Status.Checkout(detail)) + } + } + fun reset() { viewModelScope.launch { - mutableState.tryEmit(State.Empty) + _status.tryEmit(Status.Empty) + _state.tryEmit(State.Empty) + } + } + + fun cancel() { + viewModelScope.launch { + _state.tryEmit(State.Empty) } } + sealed interface Status { + data object Empty: Status + data class Confirmation(val detail: UiTransferDetail): Status + data class Checkout(val detail: UiTransferDetail): Status + } + sealed interface State { data object Empty: State data object Loading: State diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt index 0b2977185..4171a7200 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt @@ -10,10 +10,7 @@ import eu.peernetwork.core.ui.exception.NoContentException import eu.peernetwork.core.ui.usecase.PagingUsecase import eu.peernetwork.wallet.domain.model.Filter import eu.peernetwork.wallet.domain.model.Sort -import eu.peernetwork.wallet.domain.model.Tax -import eu.peernetwork.wallet.domain.usecase.TaxUsecase import eu.peernetwork.wallet.domain.usecase.TransactionsUsecase -import eu.peernetwork.wallet.ui.mapper.mapFromDomain import eu.peernetwork.wallet.ui.mapper.mapToTransaction import eu.peernetwork.wallet.ui.model.UiTransaction import kotlinx.coroutines.flow.Flow @@ -21,12 +18,9 @@ import javax.inject.Inject class TransactionListingUsecase @Inject constructor( private val usecase: TransactionsUsecase, - private val taxUsecase: TaxUsecase, ) : PagingUsecase() { private lateinit var param: Parameter - private lateinit var tax: Tax - override fun invoke(param: Parameter): Flow> { this.param = param return Pager( @@ -56,17 +50,8 @@ class TransactionListingUsecase @Inject constructor( return if (response.items.isEmpty() && currentPage.offset == 0) { LoadResult.Error(NoContentException()) } else { - if (!this::tax.isInitialized) { - try { - tax = taxUsecase() - } catch (error: Throwable) { - return LoadResult.Error(error) - } - } LoadResult.Page( - data = response.items.map { - it.mapToTransaction().copy(tax = tax.mapFromDomain()) - }, + data = response.items.map { it.mapToTransaction() }, prevKey = if (currentOffset <= 0) null else currentOffset - 1, nextKey = if (response.items.isEmpty()) null else currentOffset + response.items.size ) diff --git a/wallet/ui/src/main/res/drawable/ic_edit.xml b/wallet/ui/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..9bbd1315d --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/wallet/ui/src/main/res/drawable/ic_message.xml b/wallet/ui/src/main/res/drawable/ic_message.xml new file mode 100644 index 000000000..709628458 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_message.xml @@ -0,0 +1,19 @@ + + + + + + diff --git a/wallet/ui/src/main/res/drawable/ic_proceed.xml b/wallet/ui/src/main/res/drawable/ic_proceed.xml new file mode 100644 index 000000000..ba1996611 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_proceed.xml @@ -0,0 +1,13 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_recipient_search.xml b/wallet/ui/src/main/res/drawable/ic_recipient_search.xml new file mode 100644 index 000000000..3db9f85f4 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_recipient_search.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_successful.xml b/wallet/ui/src/main/res/drawable/ic_successful.xml new file mode 100644 index 000000000..bb9cc2727 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_successful.xml @@ -0,0 +1,13 @@ + + + + diff --git a/wallet/ui/src/main/res/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index 24ebc031c..ac95d268c 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -3,26 +3,37 @@ Your account Your balance Message: + Message Transfer A fee of %d%% applies Amount Amount to send + Enter Amount + Total amount Token Debit Credit Choose recipient Confirm transaction + Completed Transaction successful + Your transfer was sent successfully. Transaction amount Base amount Transactions + Transfer fee peer tokens to %s Recipient address Transferred to Send + Send to + Sending to Confirm + Got it + Continue Close Cancel + Back You\'re about to make a post You\’re out of free posts! You\'re about to like a post @@ -41,11 +52,19 @@ Current balance %s You have %d Free %s Oops! an error occurred + Something went wrong + We were not able to make your transfer. Please try again later. Retry + Try again Base amount + Recipient username + Search user… %s%% to Peer Bank (platform fee) %s%% Burned (removed from supply) + %s%% to your Inviter + + min: 0.000001 To @%s From @%s @@ -59,6 +78,11 @@ Transaction Pinned post promo + %d/%d + + Add a message (optional) + e.g., Thanks for the coffee! ☕ + Remove Hidden diff --git a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt b/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModelTest.kt similarity index 77% rename from wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt rename to wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModelTest.kt index 69e2e4d99..14964388c 100644 --- a/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/service/ServiceViewModelTest.kt +++ b/wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/rate/RateViewModelTest.kt @@ -1,10 +1,9 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.rate import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.test import eu.peernetwork.wallet.domain.model.Tax import eu.peernetwork.wallet.domain.usecase.TaxUsecase -import eu.peernetwork.wallet.ui.dashboard.DashboardViewModel import eu.peernetwork.wallet.ui.mapper.mapFromDomain import io.mockk.coEvery import io.mockk.mockk @@ -22,7 +21,7 @@ import org.junit.Test import kotlin.test.assertEquals @OptIn(ExperimentalCoroutinesApi::class) -internal class ServiceViewModelTest { +internal class RateViewModelTest { @get:Rule val instantExecutorRule = InstantTaskExecutorRule() @@ -30,12 +29,12 @@ internal class ServiceViewModelTest { private val usecase = mockk() - private lateinit var viewModel: DashboardViewModel + private lateinit var viewModel: RateViewModel @Before fun setup() { Dispatchers.setMain(dispatcher) - viewModel = DashboardViewModel(usecase) + viewModel = RateViewModel(usecase) } @After @@ -57,8 +56,8 @@ internal class ServiceViewModelTest { } viewModel.initialize() viewModel.state.test { - assertEquals(DashboardViewModel.State.Loading, awaitItem()) - assertEquals(DashboardViewModel.State.Success(tax.mapFromDomain()), awaitItem()) + assertEquals(RateViewModel.State.Loading, awaitItem()) + assertEquals(RateViewModel.State.Success(tax.mapFromDomain()), awaitItem()) } } @@ -68,7 +67,7 @@ internal class ServiceViewModelTest { coEvery { usecase() } throws exception viewModel.initialize() viewModel.state.test { - assertEquals(DashboardViewModel.State.Error(exception), awaitItem()) + assertEquals(RateViewModel.State.Error(exception), awaitItem()) } } }