From 0cf2f5de7deb5e9bdf95310e147e3f71dfd703a5 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Thu, 22 Jan 2026 16:16:55 +0100 Subject: [PATCH 01/15] chore(user): fix user status crash --- app/build.gradle.kts | 4 ++-- .../user/data/interactor/AuthenticationInteractorDelegate.kt | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7803a29fc..8a3328be6 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 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 { From 7cbcbc1deb2b64526c1aef3cd1022c76c1c47284 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Thu, 22 Jan 2026 16:24:38 +0100 Subject: [PATCH 02/15] chore(wallet): add transaction models and api --- .../wallet/data/api/TransferApi.kt | 7 + .../TransactionRepositoryDelegate.kt | 9 + .../peernetwork/wallet/domain/model/Amount.kt | 8 + .../wallet/domain/model/Category.kt | 5 + .../eu/peernetwork/wallet/domain/model/Fee.kt | 10 + .../peernetwork/wallet/domain/model/Filter.kt | 10 + .../peernetwork/wallet/domain/model/Sort.kt | 6 + .../peernetwork/wallet/domain/model/Status.kt | 7 + .../wallet/domain/model/Transaction.kt | 19 + .../peernetwork/wallet/domain/model/User.kt | 13 + .../repository/TransactionRepository.kt | 11 + .../src/main/graphql/wallet.schema.graphqls | 631 ++++++++++++------ .../wallet/remote/transactions.graphql | 32 + .../wallet/remote/api/TransferApiDelegate.kt | 17 + 14 files changed, 567 insertions(+), 218 deletions(-) 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/Fee.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/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql 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..d419aa580 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Category.kt @@ -0,0 +1,5 @@ +package eu.peernetwork.wallet.domain.model + +data class Category( + val name: String +) diff --git a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fee.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fee.kt new file mode 100644 index 000000000..d75a4c4b5 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fee.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.domain.model + +import java.math.BigDecimal + +data class Fee( + 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..35c8e4ccc --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Filter.kt @@ -0,0 +1,10 @@ +package eu.peernetwork.wallet.domain.model + +sealed interface Filter { + data object None : Filter + data class Attribute( + val type: Transaction.Type, + val start: Long, + val end: Long, + ) +} 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..3ab2494d4 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Transaction.kt @@ -0,0 +1,19 @@ +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 fee: Fee, +) { + 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..b265687b9 --- /dev/null +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/User.kt @@ -0,0 +1,13 @@ +package eu.peernetwork.wallet.domain.model + +data class User( + val id: String, + val slug: Int, + val username: String, + val imageUrl: String, + val following: Boolean, + val followed: Boolean, + val isAccessible: Boolean, + val status: Status, + val timestamp: Long +) 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/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..257d15e7c --- /dev/null +++ b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql @@ -0,0 +1,32 @@ +query TransactionHistory( + $type: TokenMovementFilterType, + $start: String, + $end: String, + $limit: Int, + $offset: Int, + $sort: SortFilterType, +) { + transactionHistory( + type: $type + start_date: null + end_date: null + limit: null + offset: null + sort: null + ) { + meta { + status + ResponseCode + } + affectedRows { + operationid + transactionId + transactionCategory + transactiontype + tokenamount + netTokenAmount + 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..cf6064349 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,37 @@ package eu.peernetwork.wallet.remote.api +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 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 class TransferApiDelegate @Inject constructor( private val client: RequestClient ) : TransferApi { + override suspend fun getAll(filter: Filter, sort: Sort, page: Pageable): Page { + val query = TransactionHistoryQuery( + + ) + val response = client().query(query).executeOrThrow() + val data = response.getOrThrow().transactionHistory + response.assertOrThrow(data.meta?.status, data.meta?.ResponseCode) + TODO("Not yet implemented") + } + override suspend fun getQuote(token: Token): Quote { val response = client().query(GetActionPricesQuery()).execute() val data = response.getOrThrow().getActionPrices From febf6a638a2e3a63a973fefda606c1ccd42d2776 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Mon, 26 Jan 2026 11:44:19 +0100 Subject: [PATCH 03/15] chore(wallet): add transaction model mappers --- README.md | 2 +- .../wallet/domain/model/Category.kt | 14 ++++++++-- .../wallet/domain/model/{Fee.kt => Fees.kt} | 2 +- .../wallet/domain/model/Transaction.kt | 4 +-- .../peernetwork/wallet/domain/model/User.kt | 5 +--- .../wallet/remote/transactions.graphql | 28 +++++++++++++++++++ .../wallet/remote/api/TransferApiDelegate.kt | 10 ++++++- .../wallet/remote/mapper/Amount.kt | 5 ++++ .../wallet/remote/mapper/Category.kt | 18 ++++++++++++ .../peernetwork/wallet/remote/mapper/Fee.kt | 14 ++++++++++ .../wallet/remote/mapper/Status.kt | 13 +++++++++ .../wallet/remote/mapper/Transaction.kt | 21 ++++++++++++++ .../peernetwork/wallet/remote/mapper/User.kt | 26 +++++++++++++++++ 13 files changed, 150 insertions(+), 12 deletions(-) rename wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/{Fee.kt => Fees.kt} (91%) 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/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 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/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 index d419aa580..571320d4a 100644 --- 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 @@ -1,5 +1,13 @@ package eu.peernetwork.wallet.domain.model -data class Category( - val name: String -) +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/Fee.kt b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt similarity index 91% rename from wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fee.kt rename to wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt index d75a4c4b5..d8a652f5b 100644 --- a/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fee.kt +++ b/wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/model/Fees.kt @@ -2,7 +2,7 @@ package eu.peernetwork.wallet.domain.model import java.math.BigDecimal -data class Fee( +data class Fees( val total: BigDecimal, val burn: BigDecimal, val peer: BigDecimal, 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 index 3ab2494d4..5782f1a79 100644 --- 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 @@ -4,10 +4,10 @@ data class Transaction( val id: String, val sender: User, val recipient: User, - val message: String, + val message: String?, val category: Category, val amount: Amount, - val fee: Fee, + val fees: Fees?, ) { enum class Type { TRANSACTION, 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 index b265687b9..316a1ee9f 100644 --- 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 @@ -5,9 +5,6 @@ data class User( val slug: Int, val username: String, val imageUrl: String, - val following: Boolean, - val followed: Boolean, val isAccessible: Boolean, - val status: Status, - val timestamp: Long + val status: Status ) 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 index 257d15e7c..31f24e1c6 100644 --- 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 @@ -25,6 +25,34 @@ query TransactionHistory( 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 cf6064349..a56114ae9 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 @@ -13,6 +13,7 @@ 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.mapToDomain import wallet.wallet.eu.peernetwork.wallet.remote.GetActionPricesQuery import wallet.wallet.eu.peernetwork.wallet.remote.ResolveTransferMutation import wallet.wallet.eu.peernetwork.wallet.remote.TransactionHistoryQuery @@ -29,7 +30,14 @@ class TransferApiDelegate @Inject constructor( val response = client().query(query).executeOrThrow() val data = response.getOrThrow().transactionHistory response.assertOrThrow(data.meta?.status, data.meta?.ResponseCode) - TODO("Not yet implemented") + val transactions = data.affectedRows?.map { transaction -> + transaction.mapToDomain() + } + return Page( + count = transactions?.size ?: 0, + offset = page.offset, + items = transactions ?: emptyList() + ) } override suspend fun getQuote(token: Token): Quote { 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..99eaecfc5 --- /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/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..10487ecb9 --- /dev/null +++ b/wallet/remote/src/main/kotlin/eu/peernetwork/wallet/remote/mapper/Transaction.kt @@ -0,0 +1,21 @@ +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 + +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(), + ) +} 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, + ) +} From 082a84ebd06ca910da9f22ac1fd7a433513cc87c Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Mon, 26 Jan 2026 12:10:03 +0100 Subject: [PATCH 04/15] chore(wallet): refactor service to dashboard --- .../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 | 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 | 4 +- .../peernetwork/app/ui/wallet/WalletModule.kt | 6 +- .../peernetwork/app/ui/wallet/WalletScreen.kt | 20 +++---- .../Service.kt => dashboard/Dashboard.kt} | 14 ++--- .../DashboardError.kt} | 6 +- .../DashboardModule.kt} | 18 +++--- .../DashboardPage.kt} | 24 ++++---- .../DashboardScreen.kt} | 56 +++++++++---------- .../DashboardTransfer.kt} | 18 +++--- .../DashboardViewModel.kt} | 4 +- .../wallet/ui/service/ServiceViewModelTest.kt | 11 ++-- 20 files changed, 113 insertions(+), 114 deletions(-) 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} (97%) 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} (85%) rename wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/{service/ServiceScreen.kt => dashboard/DashboardScreen.kt} (63%) 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%) 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/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..5524720c7 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 @@ -8,7 +8,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.overview.Overview -import eu.peernetwork.wallet.ui.service.Service +import eu.peernetwork.wallet.ui.dashboard.Dashboard import eu.peernetwork.wallet.ui.transfer.Transfer interface Wallet : ApplicationProvider { @@ -23,7 +23,7 @@ interface Wallet : ApplicationProvider { dependencies = [ Wallet::class ], modules = [ WalletModule::class ] ) - interface Component : Wallet, Overview, Transfer, Member, Profile, Service, UiComponentProvider + interface Component : Wallet, Overview, Transfer, Member, Profile, Dashboard, 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..d8143c5a4 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 @@ -10,7 +10,7 @@ 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.dashboard.Dashboard import eu.peernetwork.wallet.ui.transfer.Transfer import javax.inject.Provider @@ -42,9 +42,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 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..4f304e939 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,7 +9,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf @@ -23,8 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner 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.luna.DesignRefreshScaffold import eu.peernetwork.core.ui.design.material.DesignTitle import eu.peernetwork.core.ui.design.material.DesignTitleBarHost import eu.peernetwork.core.ui.extension.builder @@ -33,8 +31,8 @@ import eu.peernetwork.social.ui.search.member.MemberModal import eu.peernetwork.user.domain.model.Account 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 @Composable @@ -53,8 +51,8 @@ fun WalletScreen( } val service = remember(recipient.value) { mutableStateOf(recipient.value?.let { - ServiceState.Transfer(it) - } ?: ServiceState.Default) + DashboardState.Transfer(it) + } ?: DashboardState.Default) } val showSheet = rememberSaveable { mutableStateOf(false) } val lastUpdated = remember { mutableLongStateOf(System.currentTimeMillis()) } @@ -66,8 +64,8 @@ fun WalletScreen( onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, header = { OverviewScreen(lastUpdated, component, viewModelStoreOwner) } ) { - ServiceScreen( - serviceState = service, + DashboardScreen( + dashboardState = service, provider = component, viewModelStoreOwner = viewModelStoreOwner, onAccountClicked = { @@ -103,8 +101,8 @@ fun WalletScreen( ) { val updatedHeader by rememberUpdatedState(header) val updatedContent by rememberUpdatedState(content) - val state = remember { derivedStateOf { DesignStatefulScaffoldState.Success(Unit) } } - DesignRefreshableScaffold(state, onRefresh = onRefresh) { + val isRefreshing = remember { mutableStateOf(false) } + DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { Column(modifier = Modifier.padding(horizontal = 18.dp, vertical = 8.dp) .fillMaxSize() .verticalScroll(rememberScrollState()) 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 97% 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..aada2a424 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 @@ -31,7 +31,7 @@ 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 85% 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..e90f24ae5 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,4 +1,4 @@ -package eu.peernetwork.wallet.ui.service +package eu.peernetwork.wallet.ui.dashboard import android.content.res.Configuration import androidx.compose.animation.Crossfade @@ -31,16 +31,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,13 +53,13 @@ fun ServicePage( } ) } - is ServiceState.Transfer -> { updateContent(target) } + is DashboardState.Transfer -> { updateContent(target) } } } } @Composable -fun ServicePage() { +fun DashboardPage() { DesignCard( color = MaterialTheme.colorScheme.surfaceDim, contentPadding = PaddingValues(16.dp), @@ -85,17 +85,17 @@ 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) }, {} ) {} } 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 63% 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..aa5875338 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 @@ -20,37 +20,37 @@ 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/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 1f964f898a3638989c5ad287dd1e07622ce887f5 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Tue, 27 Jan 2026 10:03:31 +0100 Subject: [PATCH 05/15] chore(wallet): implement transaction list style --- app/build.gradle.kts | 4 +- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 10 +- .../peernetwork/app/ui/wallet/WalletModule.kt | 9 + .../core/ui/design/luna/DesignAvatar.kt | 3 +- .../ui/confirmation/ConfirmationPricing.kt | 2 +- .../wallet/ui/transactions/Transactions.kt | 29 +++ .../ui/transactions/TransactionsAvatar.kt | 63 +++++ .../ui/transactions/TransactionsItem.kt | 229 ++++++++++++++++++ .../ui/transactions/TransactionsModule.kt | 27 +++ .../ui/transactions/TransactionsSummery.kt | 79 ++++++ .../ui/transactions/TransactionsViewModel.kt | 6 + .../ui/src/main/res/drawable/ic_forward.xml | 9 + wallet/ui/src/main/res/drawable/ic_right.xml | 9 + .../{ic_peer_token.xml => ic_token.xml} | 0 wallet/ui/src/main/res/values/strings.xml | 6 + 15 files changed, 479 insertions(+), 6 deletions(-) create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt 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/TransactionsItem.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/TransactionsSummery.kt create mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt create mode 100644 wallet/ui/src/main/res/drawable/ic_forward.xml create mode 100644 wallet/ui/src/main/res/drawable/ic_right.xml rename wallet/ui/src/main/res/drawable/{ic_peer_token.xml => ic_token.xml} (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8a3328be6..216adaa13 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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/ui/wallet/Wallet.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt index 5524720c7..96ba9c938 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,6 +9,7 @@ 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.dashboard.Dashboard +import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer interface Wallet : ApplicationProvider { @@ -23,7 +24,14 @@ interface Wallet : ApplicationProvider { dependencies = [ Wallet::class ], modules = [ WalletModule::class ] ) - interface Component : Wallet, Overview, Transfer, Member, Profile, Dashboard, UiComponentProvider + interface Component : Wallet, + Overview, + Transfer, + Member, + Profile, + Dashboard, + Transactions, + 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 d8143c5a4..c578d6cb5 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,6 +11,7 @@ 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.dashboard.Dashboard +import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer import javax.inject.Provider @@ -62,4 +63,12 @@ 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) + } } 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/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt index 5405a7d13..03e6c64ad 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt @@ -48,7 +48,7 @@ fun ConfirmationPricing( fontWeight = FontWeight.SemiBold ) Icon( - painter = painterResource(R.drawable.ic_peer_token), + painter = painterResource(R.drawable.ic_token), contentDescription = label, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(24.dp) 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 new file mode 100644 index 000000000..035a339f3 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/Transactions.kt @@ -0,0 +1,29 @@ +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 Transactions : WalletProvider, CoreProvider { + @javax.inject.Scope + @Retention(AnnotationRetention.RUNTIME) + annotation class Scope + + @Scope + @dagger.Component( + dependencies = [ Transactions::class ], + modules = [ TransactionsModule::class ] + ) + interface Component : Transactions, Transfer { + fun viewModelFactory(): ViewModelProvider.Factory + } + + class Builder(private val dependency: Transactions): UiComponent.DefaultBuilder() { + override fun build(context: Context): Component { + 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..f76b8a0bd --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt @@ -0,0 +1,63 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +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.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +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.theme.DesignTheme +import eu.peernetwork.core.ui.theme.PeerAppDarkRed +import eu.peernetwork.wallet.ui.R + +@Composable +fun TransactionsAvatar( + icon: Painter, + color: Color, + content: @Composable () -> Unit +) { + val updatedContent by rememberUpdatedState(content) + DesignAvatar( + icon = { + Icon( + painter = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.size(10.dp) + .clip(CircleShape) + .background(color) + .padding(1.dp) + ) + }, + ) { updatedContent() } +} + +@Composable +@Preview +fun PreviewTransactionsAvatar() { + DesignTheme(isDarkMode = true) { + TransactionsAvatar( + icon = painterResource(R.drawable.ic_forward), + color = MaterialTheme.colorScheme.primary, + ) { + Icon( + painter = painterResource(R.drawable.ic_love), + contentDescription = null, + tint = PeerAppDarkRed, + modifier = Modifier.background(MaterialTheme.colorScheme.background) + .padding(8.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..0cb65e777 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt @@ -0,0 +1,229 @@ +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.Box +import androidx.compose.foundation.layout.Column +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.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.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.DesignText +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, + 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) + .padding(10.dp) + .clickable(onClick = onClick) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + updatedLead() + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 10.dp) + ) { + Text( + text = title, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.SemiBold + ) + Text( + text = createAt, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline + ) + DesignText( + text = description, + maxLines = 1, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.SemiBold + ) + } + updatedTrailing() + } + updatedContent() + } +} + +@Composable +fun TransactionsItem( + title: AnnotatedString, + description: AnnotatedString, + price: String, + createAt: String, + modifier: Modifier = Modifier, + onClick: () -> Unit, + leading: @Composable () -> Unit, + content: @Composable () -> Unit +) { + val handleClick by rememberUpdatedState(onClick) + val updatedContent by rememberUpdatedState(content) + val isVisible = rememberSaveable { mutableStateOf(false) } + TransactionsItem( + title = title, + description = description, + createAt = createAt, + modifier = modifier, + leading = leading, + trailing = { + TransactionsItemTrailing( + price = price, + isVisible = isVisible + ) + }, + onClick = { + isVisible.value = !isVisible.value + handleClick() + } + ) { + 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 { + Box(modifier = Modifier.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.bodySmall, + color = MaterialTheme.colorScheme.onBackground, + fontWeight = FontWeight.SemiBold + ) + Icon( + painter = painterResource(R.drawable.ic_token), + contentDescription = price, + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .padding(start = 4.dp) + .size(16.dp) + ) + Icon( + painter = painterResource(R.drawable.ic_right), + contentDescription = null, + tint = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.size(16.dp) + .graphicsLayer { rotationZ = rotation.value } + ) + } +} + +@Composable +@Preview +fun PreviewTransactionsItem() { + DesignTheme(isDarkMode = true) { + 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", + leading = { + TransactionsAvatar( + icon = painterResource(R.drawable.ic_forward), + color = MaterialTheme.colorScheme.primary + ) { + Icon( + painter = painterResource(R.drawable.ic_love), + contentDescription = null, + tint = PeerAppDarkRed, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(8.dp) + ) + } + }, + onClick = {} + ) { + Box(modifier = Modifier + .fillMaxWidth() + .height(100.dp)) + } + } +} 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/TransactionsSummery.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt new file mode 100644 index 000000000..d4df2912f --- /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.labelMedium +) { + 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_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/TransactionsViewModel.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt new file mode 100644 index 000000000..58bdc2d0f --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsViewModel.kt @@ -0,0 +1,6 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.lifecycle.ViewModel +import javax.inject.Inject + +class TransactionsViewModel @Inject constructor() : ViewModel() diff --git a/wallet/ui/src/main/res/drawable/ic_forward.xml b/wallet/ui/src/main/res/drawable/ic_forward.xml new file mode 100644 index 000000000..33d3e78d1 --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_forward.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_right.xml b/wallet/ui/src/main/res/drawable/ic_right.xml new file mode 100644 index 000000000..5141c4c0e --- /dev/null +++ b/wallet/ui/src/main/res/drawable/ic_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/ui/src/main/res/drawable/ic_peer_token.xml b/wallet/ui/src/main/res/drawable/ic_token.xml similarity index 100% rename from wallet/ui/src/main/res/drawable/ic_peer_token.xml rename to wallet/ui/src/main/res/drawable/ic_token.xml diff --git a/wallet/ui/src/main/res/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index ae8a9f8cd..df0fe3067 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -2,13 +2,19 @@ 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 peer tokens to %s Recipient address Transferred to From f61238906bc8920e38fb9bda39a80e33bfa6844f Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Tue, 27 Jan 2026 10:10:46 +0100 Subject: [PATCH 06/15] chore(ui): remove unused core ui components --- .../app/ui/onboarding/OnboardingScreen.kt | 2 +- .../ui/design/compose/DesignErrorLabel.kt | 96 -------------- .../ui/design/compose/DesignPagingScaffold.kt | 87 ------------ .../compose/DesignRefreshableScaffold.kt | 125 ------------------ .../{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 +- .../wallet/ui/dashboard/DashboardError.kt | 2 +- .../wallet/ui/dashboard/DashboardScreen.kt | 4 +- .../wallet/ui/overview/OverviewError.kt | 2 +- .../wallet/ui/overview/OverviewScreen.kt | 4 +- .../wallet/ui/transfer/TransferSheet.kt | 2 +- 17 files changed, 24 insertions(+), 332 deletions(-) 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%) 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/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/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/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 index aada2a424..1ee8ca600 100644 --- 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 @@ -24,7 +24,7 @@ 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 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 index aa5875338..5ffa3f4ec 100644 --- 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 @@ -13,8 +13,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.luna.DesignStreamState import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.wallet.ui.model.UiRecipient 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 index 2e6675728..0de44a8e2 100644 --- 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 @@ -17,7 +17,7 @@ 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.exception.NoContentException import eu.peernetwork.core.ui.theme.PeerTheme 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 index 3cd3a5193..632122ac5 100644 --- 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 @@ -17,8 +17,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.core.ui.theme.DesignTheme import eu.peernetwork.core.ui.theme.PeerAppGreen 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 From 111503d999f323a762166ddb92472d7f473d4a55 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Tue, 27 Jan 2026 15:05:43 +0100 Subject: [PATCH 07/15] chore(wallet): bind balance design implementation --- .../ads/ui/checkout/CheckoutError.kt | 1 - .../ads/ui/overview/OverviewError.kt | 32 +++--- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 3 +- .../peernetwork/app/ui/wallet/WalletModule.kt | 8 +- .../peernetwork/app/ui/wallet/WalletScreen.kt | 4 +- .../wallet/ui/balance/BalanceError.kt | 108 ++++++++++++++++++ .../wallet/ui/balance/BalanceOverview.kt | 59 ++++++++++ .../wallet/ui/balance/BalancePreview.kt | 85 ++++++++++++++ .../wallet/ui/balance/BalanceScreen.kt | 67 ++++++----- .../wallet/ui/balance/BalanceSkeleton.kt | 76 ++++++++++++ .../wallet/ui/dashboard/DashboardPage.kt | 35 +++--- 11 files changed, 413 insertions(+), 65 deletions(-) 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 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/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt index 96ba9c938..af6a53940 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 @@ -7,6 +7,7 @@ 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.balance.Balance import eu.peernetwork.wallet.ui.overview.Overview import eu.peernetwork.wallet.ui.dashboard.Dashboard import eu.peernetwork.wallet.ui.transactions.Transactions @@ -25,10 +26,10 @@ interface Wallet : ApplicationProvider { modules = [ WalletModule::class ] ) interface Component : Wallet, - Overview, Transfer, Member, Profile, + Balance, Dashboard, Transactions, 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 c578d6cb5..6b97fa466 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 @@ -9,7 +9,7 @@ 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.balance.Balance import eu.peernetwork.wallet.ui.dashboard.Dashboard import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer @@ -27,9 +27,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 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 4f304e939..349137145 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 @@ -29,8 +29,8 @@ 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.dashboard.DashboardScreen import eu.peernetwork.wallet.ui.dashboard.DashboardState import eu.peernetwork.wallet.ui.saveable.UiRecipientSaver @@ -62,7 +62,7 @@ fun WalletScreen( ) { controller -> WalletScreen( onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, - header = { OverviewScreen(lastUpdated, component, viewModelStoreOwner) } + header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) } ) { DashboardScreen( dashboardState = service, 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..0d5709ce9 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceOverview.kt @@ -0,0 +1,59 @@ +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 +fun BalanceOverview( + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner, +) { + 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 + ) + } + } + } + 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) { + if (state !is BalanceViewModel.State.Success) { + viewModel() + } + } + } +} 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..d27bd6eec 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.Success) { + 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..26d69d66a --- /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(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 + ) { + DesignSkeleton( + modifier.fillMaxWidth(fraction = .4f) + .height(36.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/dashboard/DashboardPage.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/dashboard/DashboardPage.kt index e90f24ae5..2495308d2 100644 --- 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 @@ -1,6 +1,5 @@ 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 @@ -58,24 +57,8 @@ fun DashboardPage( } } +@Preview @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), - ) - } - } -} - -@Composable -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) fun PreviewServicePage() { DesignTheme(isDarkMode = true) { Column { @@ -101,3 +84,19 @@ fun PreviewServicePage() { } } } + +@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), + ) + } + } +} From e15b6518f4518a87017c319e563b6e0b8a506b6a Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Tue, 27 Jan 2026 15:10:36 +0100 Subject: [PATCH 08/15] chore(wallet): remove redundant overview component --- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 1 - .../wallet/ui/balance/BalanceOverview.kt | 4 +- .../wallet/ui/balance/BalanceSkeleton.kt | 2 +- .../wallet/ui/overview/Overview.kt | 28 ---- .../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 ------- 9 files changed, 2 insertions(+), 438 deletions(-) delete mode 100644 wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/Overview.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 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 af6a53940..2dd923d4b 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 @@ -8,7 +8,6 @@ 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.overview.Overview import eu.peernetwork.wallet.ui.dashboard.Dashboard import eu.peernetwork.wallet.ui.transactions.Transactions import eu.peernetwork.wallet.ui.transfer.Transfer 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 0d5709ce9..3bd7b892f 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 @@ -51,9 +51,7 @@ fun BalanceOverview( BalancePreview(balance.value.toString()) } LaunchedEffect(lastUpdated.value) { - if (state !is BalanceViewModel.State.Success) { - viewModel() - } + 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 index 26d69d66a..9eb4ed205 100644 --- 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 @@ -54,7 +54,7 @@ fun BalanceSkeleton( ) { DesignSkeleton( modifier.fillMaxWidth(fraction = .4f) - .height(36.dp), + .height(24.dp), color = MaterialTheme.colorScheme.onBackground, ) DesignSkeleton( 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/overview/Overview.kt deleted file mode 100644 index 0a3724aa7..000000000 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/overview/Overview.kt +++ /dev/null @@ -1,28 +0,0 @@ -package eu.peernetwork.wallet.ui.overview - -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 - -interface Overview : WalletProvider, CoreProvider { - @javax.inject.Scope - @Retention(AnnotationRetention.RUNTIME) - annotation class Scope - - @Scope - @dagger.Component( - dependencies = [ Overview::class ], - modules = [ OverviewModule::class ] - ) - interface Component : Overview { - fun viewModelFactory(): ViewModelProvider.Factory - } - - class Builder(private val dependency: Overview) : UiComponent.DefaultBuilder() { - override fun build(context: Context): Component { - return DaggerOverview_Component.builder().overview(dependency).build() - } - } -} 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 0de44a8e2..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.material.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 632122ac5..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.material.DesignStatefulScaffold -import eu.peernetwork.core.ui.design.material.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 - } -} From 1319bdeca950dbcf8ff66c54fe7954e3413e594a Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Tue, 27 Jan 2026 22:12:13 +0100 Subject: [PATCH 09/15] chore(wallet): bind transaction listing --- .../peernetwork/app/ui/wallet/WalletPage.kt | 63 +++++++++++++ .../peernetwork/app/ui/wallet/WalletScreen.kt | 50 +++-------- ...eractorUsecase.kt => EngagementUsecase.kt} | 4 +- .../blog/ui/comment/CommentViewModel.kt | 11 ++- .../blog/ui/usecase/InteractionUsecase.kt | 6 +- .../wallet/domain/model/Transaction.kt | 1 + .../domain/usecase/TransactionsUsecase.kt | 24 +++++ .../wallet/remote/transactions.graphql | 12 +-- .../peernetwork/wallet/remote/mapper/Fee.kt | 2 +- .../wallet/remote/mapper/Transaction.kt | 1 + .../wallet/remote/mapper/Wallet.kt | 11 ++- .../eu/peernetwork/wallet/ui/mapper/Amount.kt | 12 +++ .../peernetwork/wallet/ui/mapper/Category.kt | 35 ++++++++ .../eu/peernetwork/wallet/ui/mapper/Fees.kt | 13 +++ .../eu/peernetwork/wallet/ui/mapper/Status.kt | 12 +++ .../wallet/ui/mapper/Transaction.kt | 19 ++++ .../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 ++ .../wallet/ui/model/UiTransaction.kt | 15 ++++ .../eu/peernetwork/wallet/ui/model/UiUser.kt | 10 +++ .../ui/transactions/TransactionsError.kt | 88 +++++++++++++++++++ .../ui/transactions/TransactionsItem.kt | 26 +++--- .../ui/transactions/TransactionsList.kt | 50 +++++++++++ .../ui/transactions/TransactionsScreen.kt | 66 ++++++++++++++ .../ui/transactions/TransactionsSkeleton.kt | 43 +++++++++ .../ui/transactions/TransactionsViewModel.kt | 52 ++++++++++- .../ui/usecase/TransactionListingUsecase.kt | 66 ++++++++++++++ wallet/ui/src/main/res/values/strings.xml | 11 +++ 31 files changed, 686 insertions(+), 70 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%) create mode 100644 wallet/domain/src/main/kotlin/eu/peernetwork/wallet/domain/usecase/TransactionsUsecase.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 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/TransactionsList.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/usecase/TransactionListingUsecase.kt 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..6aa22327d --- /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 = false, + modifier = Modifier.fillMaxSize() + .padding(horizontal = 18.dp) + .padding(vertical = 8.dp), + header = { updatedHeader() } + ) { + DesignScaffold( + alwaysReturn = true, + modifier = Modifier.fillMaxSize(), + header = { + Box(modifier = Modifier.padding(top= 16.dp)) { + 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 349137145..144448ba7 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,28 +1,16 @@ 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.material3.Text import androidx.compose.runtime.Composable -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 eu.peernetwork.core.ui.R import eu.peernetwork.core.ui.component.UiComponentProvider -import eu.peernetwork.core.ui.design.luna.DesignRefreshScaffold import eu.peernetwork.core.ui.design.material.DesignTitle import eu.peernetwork.core.ui.design.material.DesignTitleBarHost import eu.peernetwork.core.ui.extension.builder @@ -34,6 +22,7 @@ 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 @Composable fun WalletScreen( @@ -60,17 +49,23 @@ fun WalletScreen( account = account, provider = component ) { controller -> - WalletScreen( + WalletPage( onRefresh = { lastUpdated.longValue = System.currentTimeMillis() }, - header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) } + header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) }, + transactions = { + TransactionsList( + limit = postLimit, + lastUpdated = lastUpdated, + provider = component, + viewModelStoreOwner = viewModelStoreOwner + ) + } ) { DashboardScreen( dashboardState = service, provider = component, viewModelStoreOwner = viewModelStoreOwner, - onAccountClicked = { - controller.navigateIfNecessary("profile/${it}") - }, + onAccountClicked = { controller.navigateIfNecessary("profile/${it}") }, onClear = { recipient.value = null } ) { showSheet.value = true } } @@ -92,24 +87,3 @@ fun WalletScreen( } } } - -@Composable -fun WalletScreen( - onRefresh: () -> Unit, - header: @Composable () -> Unit, - content: @Composable () -> Unit, -) { - val updatedHeader by rememberUpdatedState(header) - val updatedContent by rememberUpdatedState(content) - val isRefreshing = remember { mutableStateOf(false) } - DesignRefreshScaffold(isRefreshing, 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/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 index 5782f1a79..20cf03183 100644 --- 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 @@ -8,6 +8,7 @@ data class Transaction( val category: Category, val amount: Amount, val fees: Fees?, + val createdAt: String ) { enum class Type { TRANSACTION, 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/eu/peernetwork/wallet/remote/transactions.graphql b/wallet/remote/src/main/graphql/wallet/eu/peernetwork/wallet/remote/transactions.graphql index 31f24e1c6..19141d48c 100644 --- 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 @@ -7,12 +7,12 @@ query TransactionHistory( $sort: SortFilterType, ) { transactionHistory( - type: $type - start_date: null - end_date: null - limit: null - offset: null - sort: null + type: $type, + start_date: $start, + end_date: $end, + limit: $limit, + offset: $offset, + sort: $sort ) { meta { status 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 index 99eaecfc5..19885c12a 100644 --- 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 @@ -9,6 +9,6 @@ fun TransactionHistoryQuery.Fees.mapFromDomain(): Fees { total = total.toBigDecimalOrNull() ?: BigDecimal.ZERO, burn = burn.toBigDecimalOrNull() ?: BigDecimal.ZERO, peer = peer.toBigDecimalOrNull() ?: BigDecimal.ZERO, - commission = inviter.toBigDecimalOrNull() ?: BigDecimal.ZERO, + commission = inviter?.toBigDecimalOrNull() ?: BigDecimal.ZERO, ) } 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 index 10487ecb9..4ddbfcd8d 100644 --- 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 @@ -17,5 +17,6 @@ fun TransactionHistoryQuery.AffectedRow.mapToDomain(): Transaction { gross = tokenamount.mapToAmount(), ), fees = fees?.mapFromDomain(), + createdAt = createdat ) } 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/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..7271ceb5c --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt @@ -0,0 +1,35 @@ +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 -> R.string.user_transaction_label + 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 + } +} 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..1f0151e0c --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt @@ -0,0 +1,13 @@ +package eu.peernetwork.wallet.ui.mapper + +import eu.peernetwork.wallet.domain.model.Fees +import eu.peernetwork.wallet.ui.model.UiFees + +fun Fees.mapFromDomain(): UiFees { + return UiFees( + total = total, + burn = burn, + peer = peer, + commission = commission + ) +} 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/Transaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt new file mode 100644 index 000000000..9fff5ddb5 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Transaction.kt @@ -0,0 +1,19 @@ +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.UiTransaction + +fun Transaction.mapToTransaction(): UiTransaction { + return UiTransaction( + id = id, + res = category.toResource(), + sender = sender.mapFromDomain(), + recipient = recipient.mapFromDomain(), + message = message?.annotate(), + category = category.mapFromDomain(), + amount = amount.mapFromDomain(), + fees = fees?.mapFromDomain(), + createdAt = createdAt + ) +} 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/UiTransaction.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt new file mode 100644 index 000000000..2abfe8335 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/model/UiTransaction.kt @@ -0,0 +1,15 @@ +package eu.peernetwork.wallet.ui.model + +import androidx.compose.ui.text.AnnotatedString + +data class UiTransaction( + val id: String, + val res: Int, + val sender: UiUser, + val recipient: UiUser, + val category: UiCategory, + val amount: UiAmount, + 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/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 index 0cb65e777..e368ac2f2 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 @@ -45,8 +45,8 @@ import eu.peernetwork.wallet.ui.R @Composable fun TransactionsItem( - title: AnnotatedString, - description: AnnotatedString, + title: String, + description: AnnotatedString?, createAt: String, modifier: Modifier = Modifier, onClick: () -> Unit, @@ -86,13 +86,15 @@ fun TransactionsItem( style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.outline ) - DesignText( - text = description, - maxLines = 1, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.SemiBold - ) + description?.let { + DesignText( + text = it, + maxLines = 1, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.SemiBold + ) + } } updatedTrailing() } @@ -102,8 +104,8 @@ fun TransactionsItem( @Composable fun TransactionsItem( - title: AnnotatedString, - description: AnnotatedString, + title: String, + description: AnnotatedString?, price: String, createAt: String, modifier: Modifier = Modifier, @@ -200,7 +202,7 @@ fun TransactionsItemTrailing( fun PreviewTransactionsItem() { DesignTheme(isDarkMode = true) { TransactionsItem( - title = buildAnnotatedString { append("To @removed") }, + title = "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", 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..48b3a0c20 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsList.kt @@ -0,0 +1,50 @@ +package eu.peernetwork.wallet.ui.transactions + +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.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModelStoreOwner +import eu.peernetwork.core.ui.component.UiComponentProvider + +@Composable +fun TransactionsList( + limit: Int, + lastUpdated: State, + provider: UiComponentProvider, + viewModelStoreOwner: ViewModelStoreOwner +) { + TransactionsScreen( + limit = limit, + lastUpdated = lastUpdated, + provider = provider, + viewModelStoreOwner = viewModelStoreOwner + ) { component, items -> + LazyColumn( + 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 -> + TransactionsItem( + title = stringResource(transaction.res), + description = transaction.message, + createAt = transaction.createdAt, + price = transaction.amount.net.toString(), + onClick = {}, + leading = {}, + modifier = Modifier.padding(bottom = 10.dp) + ) {} + } + } + } + } +} 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..a5d7b12d1 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsScreen.kt @@ -0,0 +1,66 @@ +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 +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 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() }, + error = { TransactionsError(it, component) { + viewModel(Pageable(0, limit)) + } } + ) { items -> + updatedContent(component, items) + } + LaunchedEffect(lastUpdated.value) { + viewModel(Pageable(0, limit)) + } +} 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..ff660ddf0 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSkeleton.kt @@ -0,0 +1,43 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.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(modifier: Modifier = Modifier) { + Row( + modifier = Modifier + .then(modifier) + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(MaterialTheme.colorScheme.surfaceDim) + .padding(10.dp) + ) { + Box(modifier = Modifier.size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.background)) + } +} + +@Composable +@Preview +fun PreviewTransactionsSkeleton() { + DesignTheme(isDarkMode = true) { + TransactionsSkeleton( + modifier = Modifier.padding(16.dp) + ) + } +} 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 index 58bdc2d0f..f17e36c88 100644 --- 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 @@ -1,6 +1,56 @@ 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() : ViewModel() +class TransactionsViewModel @Inject constructor( + private val usecase: TransactionListingUsecase +) : ViewModel() { + private val _state = MutableStateFlow(State.Default) + + val state: StateFlow = _state.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)) + } } + } + } + + 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/usecase/TransactionListingUsecase.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt new file mode 100644 index 000000000..edba1c09b --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/usecase/TransactionListingUsecase.kt @@ -0,0 +1,66 @@ +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.usecase.TransactionsUsecase +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 +) : PagingUsecase() { + private lateinit var param: Parameter + + 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 { + LoadResult.Page( + 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 + ) + } + } + + data class Parameter( + val filter: Filter, + val sort: Sort, + val page: Pageable + ) +} diff --git a/wallet/ui/src/main/res/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index df0fe3067..4b9f4bb7f 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -15,6 +15,7 @@ Transaction successful Transaction amount Base amount + Transactions peer tokens to %s Recipient address Transferred to @@ -41,4 +42,14 @@ You have %d Free %s Oops! an error occurred Retry + + To %s + Daily Mint + Extra post + Purchase + Extra like + Post dislike + Extra comment + Transaction + Pinned post promo From cfa7e79ce3ac842d162e9e5529ed698a364304e6 Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Wed, 28 Jan 2026 08:55:45 +0100 Subject: [PATCH 10/15] chore(wallet): implement transaction listing display --- .../peernetwork/app/ui/wallet/WalletPage.kt | 9 ++++--- .../peernetwork/app/ui/wallet/WalletScreen.kt | 1 + .../ui/confirmation/ConfirmationPricing.kt | 2 +- .../peernetwork/wallet/ui/mapper/Category.kt | 15 ++++++++++++ .../wallet/ui/mapper/Transaction.kt | 1 + .../wallet/ui/model/UiTransaction.kt | 1 + .../ui/transactions/TransactionsAvatar.kt | 12 +++++++--- .../ui/transactions/TransactionsItem.kt | 23 +++++++++--------- .../ui/transactions/TransactionsList.kt | 24 ++++++++++++++++++- .../ui/transactions/TransactionsSummery.kt | 2 +- .../{ic_right.xml => ic_caret_right.xml} | 0 .../{ic_token.xml => ic_peer_token.xml} | 0 ..._forward.xml => ic_transfer_direction.xml} | 0 13 files changed, 70 insertions(+), 20 deletions(-) rename wallet/ui/src/main/res/drawable/{ic_right.xml => ic_caret_right.xml} (100%) rename wallet/ui/src/main/res/drawable/{ic_token.xml => ic_peer_token.xml} (100%) rename wallet/ui/src/main/res/drawable/{ic_forward.xml => ic_transfer_direction.xml} (100%) 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 6aa22327d..76f13ac20 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 @@ -33,9 +33,12 @@ fun WalletPage( DesignScaffold( alwaysReturn = false, modifier = Modifier.fillMaxSize() - .padding(horizontal = 18.dp) - .padding(vertical = 8.dp), - header = { updatedHeader() } + .padding(horizontal = 18.dp), + header = { + Box(modifier = Modifier.padding(top = 8.dp)) { + updatedHeader() + } + } ) { DesignScaffold( alwaysReturn = true, 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 144448ba7..ab6113abb 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 @@ -54,6 +54,7 @@ fun WalletScreen( header = { BalanceOverview(lastUpdated, component, viewModelStoreOwner) }, transactions = { TransactionsList( + uuid = account.id, limit = postLimit, lastUpdated = lastUpdated, provider = component, diff --git a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt index 03e6c64ad..5405a7d13 100644 --- a/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/confirmation/ConfirmationPricing.kt @@ -48,7 +48,7 @@ fun ConfirmationPricing( fontWeight = FontWeight.SemiBold ) Icon( - painter = painterResource(R.drawable.ic_token), + painter = painterResource(R.drawable.ic_peer_token), contentDescription = label, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(24.dp) 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 index 7271ceb5c..737488e23 100644 --- 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 @@ -33,3 +33,18 @@ fun Category.toResource(): Int { Category.DEFAULT -> R.string.default_transaction_label } } + +@StringRes +fun Category.toIcon(): Int { + return when (this) { + Category.AD -> R.drawable.ic_love + Category.MINT -> R.drawable.ic_love + Category.POST -> R.drawable.ic_love + Category.PURCHASE -> R.drawable.ic_love + Category.TRANSFER -> R.drawable.ic_love + Category.COMMENT -> R.drawable.ic_love + Category.LIKE -> R.drawable.ic_love + Category.DISLIKE -> R.drawable.ic_hate + Category.DEFAULT -> R.drawable.ic_love + } +} 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 9fff5ddb5..cdbc8cdcf 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 @@ -8,6 +8,7 @@ fun Transaction.mapToTransaction(): UiTransaction { return UiTransaction( id = id, res = category.toResource(), + icon = category.toIcon(), sender = sender.mapFromDomain(), recipient = recipient.mapFromDomain(), message = message?.annotate(), 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 2abfe8335..f744963ad 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 @@ -5,6 +5,7 @@ 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, 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 index f76b8a0bd..caad2f9f9 100644 --- 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 @@ -12,6 +12,7 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -25,6 +26,8 @@ import eu.peernetwork.wallet.ui.R fun TransactionsAvatar( icon: Painter, color: Color, + contentDescription: String? = null, + isRecipient: Boolean = false, content: @Composable () -> Unit ) { val updatedContent by rememberUpdatedState(content) @@ -32,12 +35,15 @@ fun TransactionsAvatar( icon = { Icon( painter = icon, - contentDescription = null, + contentDescription = contentDescription, tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.size(10.dp) + modifier = Modifier.size(12.dp) .clip(CircleShape) .background(color) .padding(1.dp) + .graphicsLayer { + rotationZ = if (isRecipient) 180f else 0f + } ) }, ) { updatedContent() } @@ -48,7 +54,7 @@ fun TransactionsAvatar( fun PreviewTransactionsAvatar() { DesignTheme(isDarkMode = true) { TransactionsAvatar( - icon = painterResource(R.drawable.ic_forward), + icon = painterResource(R.drawable.ic_transfer_direction), color = MaterialTheme.colorScheme.primary, ) { Icon( 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 e368ac2f2..211b986ee 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 @@ -62,8 +62,8 @@ fun TransactionsItem( .then(modifier) .clip(RoundedCornerShape(16.dp)) .background(MaterialTheme.colorScheme.surfaceDim) - .padding(10.dp) .clickable(onClick = onClick) + .padding(10.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -77,20 +77,21 @@ fun TransactionsItem( ) { Text( text = title, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold ) Text( text = createAt, - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline ) - description?.let { + if (description != null + && description.isNotEmpty()) { DesignText( - text = it, + text = description, maxLines = 1, - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline, fontWeight = FontWeight.SemiBold ) @@ -175,20 +176,20 @@ fun TransactionsItemTrailing( Row(verticalAlignment = Alignment.CenterVertically) { Text( text = price, - style = MaterialTheme.typography.bodySmall, + style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onBackground, fontWeight = FontWeight.SemiBold ) Icon( - painter = painterResource(R.drawable.ic_token), + painter = painterResource(R.drawable.ic_peer_token), contentDescription = price, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier .padding(start = 4.dp) - .size(16.dp) + .size(20.dp) ) Icon( - painter = painterResource(R.drawable.ic_right), + painter = painterResource(R.drawable.ic_caret_right), contentDescription = null, tint = MaterialTheme.colorScheme.outlineVariant, modifier = Modifier.size(16.dp) @@ -208,7 +209,7 @@ fun PreviewTransactionsItem() { createAt = "10 Jun 2025, 04:20", leading = { TransactionsAvatar( - icon = painterResource(R.drawable.ic_forward), + icon = painterResource(R.drawable.ic_transfer_direction), color = MaterialTheme.colorScheme.primary ) { Icon( 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 48b3a0c20..731c88858 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 @@ -1,19 +1,25 @@ 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.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModelStoreOwner import eu.peernetwork.core.ui.component.UiComponentProvider +import eu.peernetwork.wallet.ui.R @Composable fun TransactionsList( + uuid: String, limit: Int, lastUpdated: State, provider: UiComponentProvider, @@ -40,7 +46,23 @@ fun TransactionsList( createAt = transaction.createdAt, price = transaction.amount.net.toString(), onClick = {}, - leading = {}, + leading = { + TransactionsAvatar( + icon = painterResource(R.drawable.ic_transfer_direction), + color = MaterialTheme.colorScheme.primary, + contentDescription = stringResource(transaction.res), + isRecipient = transaction.recipient.id == uuid + ) { + Icon( + painter = painterResource(transaction.icon), + contentDescription = stringResource(transaction.res), + tint = MaterialTheme.colorScheme.onBackground, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(8.dp) + ) + } + }, modifier = Modifier.padding(bottom = 10.dp) ) {} } 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 index d4df2912f..ed417770e 100644 --- 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 @@ -47,7 +47,7 @@ fun TransactionsSummeryItem( fontWeight = priceColor?.let { FontWeight.Medium } ?: FontWeight.SemiBold ) Icon( - painter = painterResource(R.drawable.ic_token), + painter = painterResource(R.drawable.ic_peer_token), contentDescription = stringResource(R.string.token_label), tint = priceColor ?: MaterialTheme.colorScheme.onBackground, modifier = Modifier.padding(start = 2.dp) diff --git a/wallet/ui/src/main/res/drawable/ic_right.xml b/wallet/ui/src/main/res/drawable/ic_caret_right.xml similarity index 100% rename from wallet/ui/src/main/res/drawable/ic_right.xml rename to wallet/ui/src/main/res/drawable/ic_caret_right.xml diff --git a/wallet/ui/src/main/res/drawable/ic_token.xml b/wallet/ui/src/main/res/drawable/ic_peer_token.xml similarity index 100% rename from wallet/ui/src/main/res/drawable/ic_token.xml rename to wallet/ui/src/main/res/drawable/ic_peer_token.xml diff --git a/wallet/ui/src/main/res/drawable/ic_forward.xml b/wallet/ui/src/main/res/drawable/ic_transfer_direction.xml similarity index 100% rename from wallet/ui/src/main/res/drawable/ic_forward.xml rename to wallet/ui/src/main/res/drawable/ic_transfer_direction.xml From 6bfa146cea278fe84ef9d3e0dc5649e10694f5dd Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Wed, 28 Jan 2026 15:03:00 +0100 Subject: [PATCH 11/15] chore(wallet): bind transaction history user --- .../peernetwork/app/ui/wallet/WalletPage.kt | 15 +- .../peernetwork/app/ui/wallet/WalletScreen.kt | 17 ++- .../peernetwork/wallet/domain/model/Filter.kt | 13 +- .../wallet/remote/api/TransferApiDelegate.kt | 17 ++- .../wallet/remote/mapper/Filter.kt | 17 +++ .../peernetwork/wallet/remote/mapper/Sort.kt | 11 ++ .../peernetwork/wallet/ui/mapper/Category.kt | 18 ++- .../wallet/ui/model/UiTransaction.kt | 4 +- .../ui/transactions/TransactionsAvatar.kt | 132 ++++++++++++++++-- .../ui/transactions/TransactionsItem.kt | 1 - .../ui/transactions/TransactionsList.kt | 53 +++++-- .../ui/transactions/TransactionsUsername.kt | 64 +++++++++ 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 ++ wallet/ui/src/main/res/values/strings.xml | 10 +- .../ui/overview/OverviewViewModelTest.kt | 75 ---------- 19 files changed, 363 insertions(+), 136 deletions(-) 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/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt 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 delete mode 100644 wallet/ui/src/test/kotlin/eu/peernetwork/wallet/ui/overview/OverviewViewModelTest.kt 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 76f13ac20..b169d2742 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 @@ -31,23 +31,20 @@ fun WalletPage( val isRefreshing = remember { mutableStateOf(false) } DesignRefreshScaffold(isRefreshing, onRefresh = onRefresh) { DesignScaffold( - alwaysReturn = false, + alwaysReturn = true, modifier = Modifier.fillMaxSize() .padding(horizontal = 18.dp), header = { - Box(modifier = Modifier.padding(top = 8.dp)) { - updatedHeader() - } + Box( + modifier = Modifier.padding(top = 8.dp) + .padding(bottom = 16.dp) + ) { updatedHeader() } } ) { DesignScaffold( alwaysReturn = true, modifier = Modifier.fillMaxSize(), - header = { - Box(modifier = Modifier.padding(top= 16.dp)) { - updatedContent() - } - } + header = { updatedContent() } ) { Column { Text( 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 ab6113abb..8bc0dc486 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,10 +1,12 @@ package eu.peernetwork.app.ui.wallet +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableLongStateOf 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.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -23,6 +25,7 @@ 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 kotlinx.coroutines.launch @Composable fun WalletScreen( @@ -32,6 +35,7 @@ fun WalletScreen( viewModelStoreOwner: ViewModelStoreOwner ) { val context = LocalContext.current + val scope = rememberCoroutineScope() val component = remember { provider.builder(Wallet.Builder::class.java).build(context) } @@ -43,8 +47,9 @@ fun WalletScreen( DashboardState.Transfer(it) } ?: DashboardState.Default) } + val listState = rememberLazyListState() val showSheet = rememberSaveable { mutableStateOf(false) } - val lastUpdated = remember { mutableLongStateOf(System.currentTimeMillis()) } + val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } WalletNavigation( account = account, provider = component @@ -57,6 +62,7 @@ fun WalletScreen( uuid = account.id, limit = postLimit, lastUpdated = lastUpdated, + listState = listState, provider = component, viewModelStoreOwner = viewModelStoreOwner ) @@ -70,7 +76,14 @@ fun WalletScreen( onClear = { recipient.value = null } ) { showSheet.value = true } } - DesignTitleBarHost("WalletScreen") { + DesignTitleBarHost( + tag = "WalletScreen", + listener = { + scope.launch { + listState.animateScrollToItem(0) + } + } + ) { titleBar { DesignTitle { Text(stringResource(R.string.wallet_label)) 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 index 35c8e4ccc..db2efa889 100644 --- 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 @@ -1,10 +1,9 @@ package eu.peernetwork.wallet.domain.model -sealed interface Filter { - data object None : Filter - data class Attribute( - val type: Transaction.Type, - val start: Long, - val end: Long, - ) +sealed class Filter( + val type: Transaction.Type?, + val start: String?, + val end: String?, +) { + data object None : Filter(null, null, null) } 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 a56114ae9..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,5 +1,6 @@ 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 @@ -13,25 +14,35 @@ 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() + 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, 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/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Category.kt index 737488e23..42f1c4081 100644 --- 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 @@ -20,13 +20,13 @@ fun Category.mapFromDomain(): UiCategory { } @StringRes -fun Category.toResource(): Int { +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 -> R.string.user_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 @@ -35,16 +35,14 @@ fun Category.toResource(): Int { } @StringRes -fun Category.toIcon(): Int { +fun Category.toIcon(): Int? { return when (this) { - Category.AD -> R.drawable.ic_love - Category.MINT -> R.drawable.ic_love - Category.POST -> R.drawable.ic_love - Category.PURCHASE -> R.drawable.ic_love - Category.TRANSFER -> R.drawable.ic_love - Category.COMMENT -> R.drawable.ic_love + 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 - Category.DEFAULT -> R.drawable.ic_love + else -> R.drawable.ic_mint } } 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 f744963ad..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 @@ -4,8 +4,8 @@ import androidx.compose.ui.text.AnnotatedString data class UiTransaction( val id: String, - val res: Int, - val icon: Int, + val res: Int?, + val icon: Int?, val sender: UiUser, val recipient: UiUser, val category: UiCategory, 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 index caad2f9f9..292916a55 100644 --- 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 @@ -1,31 +1,41 @@ 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.Color 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, - color: Color, contentDescription: String? = null, isRecipient: Boolean = false, content: @Composable () -> Unit @@ -39,7 +49,7 @@ fun TransactionsAvatar( tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(12.dp) .clip(CircleShape) - .background(color) + .background(MaterialTheme.colorScheme.primary) .padding(1.dp) .graphicsLayer { rotationZ = if (isRecipient) 180f else 0f @@ -49,21 +59,115 @@ fun TransactionsAvatar( ) { updatedContent() } } +@Composable +fun TransactionsUserAvatar( + 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) { + TransactionsUserAvatar( + 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 { + TransactionsUserAvatar( + user = user, + onClick = onClick + ) + } +} + +@Composable +fun TransactionsUserAvatar( + 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) { - TransactionsAvatar( - icon = painterResource(R.drawable.ic_transfer_direction), - color = MaterialTheme.colorScheme.primary, - ) { - Icon( - painter = painterResource(R.drawable.ic_love), - contentDescription = null, - tint = PeerAppDarkRed, - modifier = Modifier.background(MaterialTheme.colorScheme.background) - .padding(8.dp) - ) + 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) + ) + } + TransactionsUserAvatar( + user = UiUser( + id = "123", + username = "test", + slug = 1234, + imageUrl = "http:localhost", + isAccessible = false, + status = UiStatus.ILLEGAL + ), + isVisible = isVisible, + ) {} + TransactionsUserAvatar( + 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/TransactionsItem.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt index 211b986ee..b030b488f 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 @@ -210,7 +210,6 @@ fun PreviewTransactionsItem() { leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), - color = MaterialTheme.colorScheme.primary ) { Icon( painter = painterResource(R.drawable.ic_love), 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 731c88858..cfda83427 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 @@ -5,10 +5,13 @@ 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.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -22,6 +25,7 @@ fun TransactionsList( uuid: String, limit: Int, lastUpdated: State, + listState: LazyListState, provider: UiComponentProvider, viewModelStoreOwner: ViewModelStoreOwner ) { @@ -32,6 +36,7 @@ fun TransactionsList( viewModelStoreOwner = viewModelStoreOwner ) { component, items -> LazyColumn( + state = listState, modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 36.dp), ) { @@ -40,8 +45,25 @@ fun TransactionsList( key = { index -> items[index]?.id?.let { "$it;$index" } ?: index } ) { index -> items[index]?.let { transaction -> + val isVisible = remember { mutableStateOf(false) } + val isRecipient = transaction.recipient.id == uuid + val profile = if (isRecipient) { + transaction.sender + } else { + transaction.recipient + } + val title = if (isRecipient) { + stringResource(R.string.received_from, profile.username) + } else { + stringResource(R.string.sent_to, profile.username) + } TransactionsItem( - title = stringResource(transaction.res), + title = transaction.res?.let { stringResource(it) } + ?: transactionsUsername( + isVisible = isVisible, + isRecipient = isRecipient, + user = profile + ), description = transaction.message, createAt = transaction.createdAt, price = transaction.amount.net.toString(), @@ -49,18 +71,25 @@ fun TransactionsList( leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), - color = MaterialTheme.colorScheme.primary, - contentDescription = stringResource(transaction.res), - isRecipient = transaction.recipient.id == uuid + contentDescription = transaction.res?.let { stringResource(it) } + ?: title, + isRecipient = isRecipient ) { - Icon( - painter = painterResource(transaction.icon), - contentDescription = stringResource(transaction.res), - tint = MaterialTheme.colorScheme.onBackground, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .padding(8.dp) - ) + 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 { + TransactionsUserAvatar( + user = profile, + isVisible = isVisible, + ) { isVisible.value = true } + } } }, modifier = Modifier.padding(bottom = 10.dp) 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..2f5200fd4 --- /dev/null +++ b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsUsername.kt @@ -0,0 +1,64 @@ +package eu.peernetwork.wallet.ui.transactions + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.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, +): String { + return if (user.status == UiStatus.ILLEGAL) { + stringResource(R.string.illegal_username) + } else if (!user.isAccessible) { + if (isVisible.value) { + if (isRecipient) { + stringResource(R.string.received_from, user.username) + } else { + stringResource(R.string.sent_to, user.username) + } + } else { + stringResource(R.string.hidden_username) + } + } else { + if (isRecipient) { + stringResource(R.string.received_from, user.username) + } else { + stringResource(R.string.sent_to, user.username) + } + } +} + +@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/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/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index 4b9f4bb7f..1c7ef403d 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -43,13 +43,21 @@ Oops! an error occurred Retry + To @%s + From @%s To %s Daily Mint Extra post Purchase Extra like - Post dislike + 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()) - } - } -} From 292fa3e4ea73abaf467b68ae7c9812a1e4523abe Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Wed, 28 Jan 2026 17:15:16 +0100 Subject: [PATCH 12/15] chore(wallet): bind transaction details and actions --- .../app/interactor/NavigationInteractor.kt | 27 +++++- .../eu/peernetwork/app/ui/wallet/Wallet.kt | 2 + .../peernetwork/app/ui/wallet/WalletModule.kt | 9 ++ .../app/ui/wallet/WalletNavigation.kt | 29 +++++- .../peernetwork/app/ui/wallet/WalletScreen.kt | 95 ++++++++++--------- .../wallet/ui/balance/BalanceOverview.kt | 9 +- .../wallet/ui/balance/BalanceScreen.kt | 2 +- .../wallet/ui/balance/BalanceViewModel.kt | 17 ++++ .../ui/extension/TransactionsNavigator.kt | 12 +++ .../eu/peernetwork/wallet/ui/mapper/Fees.kt | 9 +- .../eu/peernetwork/wallet/ui/mapper/Tax.kt | 8 +- .../wallet/ui/mapper/Transaction.kt | 2 + .../eu/peernetwork/wallet/ui/model/UiTax.kt | 11 ++- .../wallet/ui/model/UiTransaction.kt | 1 + .../ui/transactions/TransactionsAvatar.kt | 12 +-- .../ui/transactions/TransactionsItem.kt | 40 +++++--- .../ui/transactions/TransactionsList.kt | 54 +++++++---- .../ui/transactions/TransactionsNavigator.kt | 22 +++++ .../ui/transactions/TransactionsScreen.kt | 9 +- .../ui/transactions/TransactionsSummery.kt | 2 +- .../ui/transactions/TransactionsViewModel.kt | 17 ++++ .../ui/usecase/TransactionListingUsecase.kt | 19 +++- wallet/ui/src/main/res/values/strings.xml | 4 + 23 files changed, 314 insertions(+), 98 deletions(-) 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/transactions/TransactionsNavigator.kt 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/wallet/Wallet.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/Wallet.kt index 2dd923d4b..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,6 +3,7 @@ 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 @@ -31,6 +32,7 @@ interface Wallet : ApplicationProvider { Balance, Dashboard, Transactions, + Search, UiComponentProvider class Builder(private val dependency: Wallet): UiComponent.DefaultBuilder() { 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 6b97fa466..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,6 +4,7 @@ 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 @@ -71,4 +72,12 @@ object WalletModule { 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/WalletScreen.kt b/app/src/main/kotlin/eu/peernetwork/app/ui/wallet/WalletScreen.kt index 8bc0dc486..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 @@ -3,6 +3,7 @@ package eu.peernetwork.app.ui.wallet import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -11,6 +12,8 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.material.DesignTitle @@ -25,6 +28,7 @@ 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 @@ -48,56 +52,61 @@ fun WalletScreen( } ?: DashboardState.Default) } val listState = rememberLazyListState() + val controller = rememberNavController() val showSheet = rememberSaveable { mutableStateOf(false) } val lastUpdated = rememberSaveable { mutableLongStateOf(System.currentTimeMillis()) } - WalletNavigation( - account = account, - 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, + 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 - ) + viewModelStoreOwner = viewModelStoreOwner, + onAccountClicked = { controller.navigateIfNecessary("profile/${it}") }, + onClear = { recipient.value = null } + ) { showSheet.value = true } } - ) { - 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) + DesignTitleBarHost( + tag = "WalletScreen", + listener = { + scope.launch { + listState.animateScrollToItem(0) + } } - } - ) { - titleBar { - DesignTitle { - Text(stringResource(R.string.wallet_label)) + ) { + 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 + } } } } 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 3bd7b892f..c3fa1108c 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 @@ -14,6 +14,7 @@ import eu.peernetwork.core.ui.design.luna.DesignStreamState import java.math.RoundingMode @Composable +@Suppress("UNCHECKED_CAST") fun BalanceOverview( lastUpdated: State, provider: UiComponentProvider, @@ -24,6 +25,7 @@ fun BalanceOverview( viewModelStoreOwner = viewModelStoreOwner, ) { component, viewModel -> val state by viewModel.state.collectAsStateWithLifecycle() + val status by viewModel.status.collectAsStateWithLifecycle() val derivedState = remember { derivedStateOf { when (state) { @@ -51,7 +53,12 @@ fun BalanceOverview( BalancePreview(balance.value.toString()) } LaunchedEffect(lastUpdated.value) { - viewModel() + (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/BalanceScreen.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceScreen.kt index d27bd6eec..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 @@ -73,7 +73,7 @@ fun BalanceScreen( error = error ) { updatedContent(it) } LaunchedEffect(lastUpdated.value) { - if (state !is BalanceViewModel.State.Success) { + if (state is BalanceViewModel.State.Empty) { viewModel() } } 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/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/Fees.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/mapper/Fees.kt index 1f0151e0c..aec05d153 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 @@ -2,12 +2,13 @@ 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, - burn = burn, - peer = peer, - commission = commission + 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/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 index cdbc8cdcf..f665978fd 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,6 +2,7 @@ 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 fun Transaction.mapToTransaction(): UiTransaction { @@ -15,6 +16,7 @@ fun Transaction.mapToTransaction(): UiTransaction { category = category.mapFromDomain(), amount = amount.mapFromDomain(), fees = fees?.mapFromDomain(), + tax = UiTax.Free, createdAt = createdAt ) } 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 index 602b29db2..1e4faaa15 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,6 +10,7 @@ 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/transactions/TransactionsAvatar.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsAvatar.kt index 292916a55..41ed655e7 100644 --- 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 @@ -60,7 +60,7 @@ fun TransactionsAvatar( } @Composable -fun TransactionsUserAvatar( +fun TransactionsProfile( user: UiUser, isVisible: MutableState, onClick: () -> Unit @@ -80,7 +80,7 @@ fun TransactionsUserAvatar( } } else if (!user.isAccessible) { if (isVisible.value) { - TransactionsUserAvatar( + TransactionsProfile( user = user, onClick = onClick ) @@ -100,7 +100,7 @@ fun TransactionsUserAvatar( } } } else { - TransactionsUserAvatar( + TransactionsProfile( user = user, onClick = onClick ) @@ -108,7 +108,7 @@ fun TransactionsUserAvatar( } @Composable -fun TransactionsUserAvatar( +fun TransactionsProfile( user: UiUser, onClick: () -> Unit ) { @@ -146,7 +146,7 @@ fun PreviewTransactionsAvatar() { .padding(8.dp) ) } - TransactionsUserAvatar( + TransactionsProfile( user = UiUser( id = "123", username = "test", @@ -157,7 +157,7 @@ fun PreviewTransactionsAvatar() { ), isVisible = isVisible, ) {} - TransactionsUserAvatar( + TransactionsProfile( user = UiUser( id = "123", username = "test", 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 b030b488f..b70a16dd9 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 @@ -11,8 +11,10 @@ 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 @@ -38,7 +40,8 @@ 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.DesignText +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 @@ -50,6 +53,8 @@ fun TransactionsItem( createAt: String, modifier: Modifier = Modifier, onClick: () -> Unit, + onProfileClicked: () -> Unit, + onMessageClicked: (DesignRichText, String) -> Unit, leading: @Composable () -> Unit, trailing: @Composable () -> Unit, content: @Composable () -> Unit, @@ -79,7 +84,8 @@ fun TransactionsItem( text = title, style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onBackground, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, + modifier = Modifier.tap(onProfileClicked) ) Text( text = createAt, @@ -88,12 +94,13 @@ fun TransactionsItem( ) if (description != null && description.isNotEmpty()) { - DesignText( + DesignRichText( text = description, maxLines = 1, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.SemiBold, + onClick = onMessageClicked ) } } @@ -109,18 +116,20 @@ fun TransactionsItem( description: AnnotatedString?, price: String, createAt: String, + onProfileClicked: () -> Unit, + onMessageClicked: (DesignRichText, String) -> Unit, modifier: Modifier = Modifier, - onClick: () -> Unit, leading: @Composable () -> Unit, - content: @Composable () -> Unit + content: @Composable ColumnScope.() -> Unit ) { - val handleClick by rememberUpdatedState(onClick) val updatedContent by rememberUpdatedState(content) val isVisible = rememberSaveable { mutableStateOf(false) } TransactionsItem( title = title, description = description, createAt = createAt, + onProfileClicked = onProfileClicked, + onMessageClicked = onMessageClicked, modifier = modifier, leading = leading, trailing = { @@ -129,10 +138,7 @@ fun TransactionsItem( isVisible = isVisible ) }, - onClick = { - isVisible.value = !isVisible.value - handleClick() - } + onClick = { isVisible.value = !isVisible.value } ) { AnimatedContent( targetState = isVisible.value, @@ -150,8 +156,13 @@ fun TransactionsItem( }, ) { visible -> if (visible) { - Column { - Box(modifier = Modifier.height(1.dp) + 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() } @@ -207,6 +218,8 @@ fun PreviewTransactionsItem() { 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 = { _,_ -> }, leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), @@ -221,7 +234,6 @@ fun PreviewTransactionsItem() { ) } }, - onClick = {} ) { Box(modifier = Modifier .fillMaxWidth() 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 cfda83427..89d2dcc3d 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 @@ -10,8 +10,10 @@ 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.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -19,6 +21,8 @@ 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( @@ -27,8 +31,11 @@ fun TransactionsList( lastUpdated: State, listState: LazyListState, provider: UiComponentProvider, - viewModelStoreOwner: ViewModelStoreOwner + viewModelStoreOwner: ViewModelStoreOwner, + onClick: (String) -> Unit ) { + val navigator = LocalTransactionsNavigator.current + val handleClick by rememberUpdatedState(onClick) TransactionsScreen( limit = limit, lastUpdated = lastUpdated, @@ -52,22 +59,16 @@ fun TransactionsList( } else { transaction.recipient } - val title = if (isRecipient) { - stringResource(R.string.received_from, profile.username) - } else { - stringResource(R.string.sent_to, profile.username) - } + val title = transactionsUsername( + isVisible = isVisible, + isRecipient = isRecipient, + user = profile + ) TransactionsItem( - title = transaction.res?.let { stringResource(it) } - ?: transactionsUsername( - isVisible = isVisible, - isRecipient = isRecipient, - user = profile - ), + title = transaction.res?.let { stringResource(it) } ?: title, description = transaction.message, createAt = transaction.createdAt, - price = transaction.amount.net.toString(), - onClick = {}, + price = "${transaction.amount.net}", leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), @@ -85,15 +86,34 @@ fun TransactionsList( .padding(8.dp) ) } else { - TransactionsUserAvatar( + TransactionsProfile( user = profile, isVisible = isVisible, - ) { isVisible.value = true } + ) { handleClick(profile.id) } } } }, + onProfileClicked = { handleClick(profile.id) }, + onMessageClicked = { spec, value -> navigator.navigate(spec.route(value)) }, modifier = Modifier.padding(bottom = 10.dp) - ) {} + ) { + TransactionsSummeryItem( + title = stringResource(R.string.base_amount), + price = "${transaction.amount.gross}", + ) + 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/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 index a5d7b12d1..f7a564b06 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 @@ -20,6 +20,7 @@ import eu.peernetwork.core.ui.extension.builder import eu.peernetwork.wallet.ui.model.UiTransaction @Composable +@Suppress("UNCHECKED_CAST") fun TransactionsScreen( limit: Int, lastUpdated: State, @@ -37,6 +38,7 @@ fun TransactionsScreen( 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 @@ -61,6 +63,11 @@ fun TransactionsScreen( updatedContent(component, items) } LaunchedEffect(lastUpdated.value) { - viewModel(Pageable(0, limit)) + (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/TransactionsSummery.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsSummery.kt index ed417770e..d76c63876 100644 --- 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 @@ -28,7 +28,7 @@ fun TransactionsSummeryItem( modifier: Modifier = Modifier, priceColor: Color? = null, color: Color = priceColor ?: MaterialTheme.colorScheme.outline, - style: TextStyle = MaterialTheme.typography.labelMedium + style: TextStyle = MaterialTheme.typography.bodySmall ) { Row( modifier = modifier, 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 index f17e36c88..495a6344c 100644 --- 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 @@ -24,8 +24,12 @@ class TransactionsViewModel @Inject constructor( ) : 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, @@ -47,6 +51,19 @@ class TransactionsViewModel @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 Default: 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 edba1c09b..0b2977185 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,17 +10,23 @@ 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 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( @@ -50,8 +56,17 @@ 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() }, + 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 ) diff --git a/wallet/ui/src/main/res/values/strings.xml b/wallet/ui/src/main/res/values/strings.xml index 1c7ef403d..24ebc031c 100644 --- a/wallet/ui/src/main/res/values/strings.xml +++ b/wallet/ui/src/main/res/values/strings.xml @@ -43,6 +43,10 @@ Oops! an error occurred Retry + Base amount + %s%% to Peer Bank (platform fee) + %s%% Burned (removed from supply) + To @%s From @%s To %s From 3fb13fd69e18f7328513300d8cfc6090df4975be Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Wed, 28 Jan 2026 20:17:38 +0100 Subject: [PATCH 13/15] chore(wallet): implement moderation on transaction history --- .../wallet/domain/model/Transaction.kt | 2 +- .../wallet/remote/mapper/Transaction.kt | 11 +++++++- .../remote/api/TransferApiDelegateTest.kt | 2 +- .../wallet/ui/balance/BalanceSkeleton.kt | 2 +- .../wallet/ui/mapper/Transaction.kt | 10 ++++++- .../ui/transactions/TransactionsItem.kt | 13 +++++---- .../ui/transactions/TransactionsList.kt | 13 +++++++-- .../ui/transactions/TransactionsScreen.kt | 2 +- .../ui/transactions/TransactionsSkeleton.kt | 27 +++++++++++++++---- 9 files changed, 64 insertions(+), 18 deletions(-) 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 index 20cf03183..2248d3632 100644 --- 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 @@ -8,7 +8,7 @@ data class Transaction( val category: Category, val amount: Amount, val fees: Fees?, - val createdAt: String + val createdAt: Long ) { enum class Type { TRANSACTION, 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 index 4ddbfcd8d..99e303e56 100644 --- 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 @@ -4,6 +4,9 @@ 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( @@ -17,6 +20,12 @@ fun TransactionHistoryQuery.AffectedRow.mapToDomain(): Transaction { gross = tokenamount.mapToAmount(), ), fees = fees?.mapFromDomain(), - createdAt = createdat + 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/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/BalanceSkeleton.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/balance/BalanceSkeleton.kt index 9eb4ed205..2a213f9a7 100644 --- 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 @@ -28,7 +28,7 @@ import eu.peernetwork.core.ui.theme.DesignTheme @SuppressLint("UnusedBoxWithConstraintsScope") fun BalanceSkeleton( modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(24.dp), + contentPadding: PaddingValues = PaddingValues(28.dp), ) { BoxWithConstraints { val vertical = with(LocalDensity.current) { maxHeight.toPx() * .15f } 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 f665978fd..de378ed35 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 @@ -4,6 +4,9 @@ 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( @@ -17,6 +20,11 @@ fun Transaction.mapToTransaction(): UiTransaction { amount = amount.mapFromDomain(), fees = fees?.mapFromDomain(), tax = UiTax.Free, - createdAt = createdAt + 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/transactions/TransactionsItem.kt b/wallet/ui/src/main/kotlin/eu/peernetwork/wallet/ui/transactions/TransactionsItem.kt index b70a16dd9..93e9f9135 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 @@ -25,11 +25,12 @@ 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.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -116,6 +117,7 @@ fun TransactionsItem( description: AnnotatedString?, price: String, createAt: String, + expanded: MutableState, onProfileClicked: () -> Unit, onMessageClicked: (DesignRichText, String) -> Unit, modifier: Modifier = Modifier, @@ -123,7 +125,6 @@ fun TransactionsItem( content: @Composable ColumnScope.() -> Unit ) { val updatedContent by rememberUpdatedState(content) - val isVisible = rememberSaveable { mutableStateOf(false) } TransactionsItem( title = title, description = description, @@ -135,13 +136,13 @@ fun TransactionsItem( trailing = { TransactionsItemTrailing( price = price, - isVisible = isVisible + isVisible = expanded ) }, - onClick = { isVisible.value = !isVisible.value } + onClick = { expanded.value = !expanded.value } ) { AnimatedContent( - targetState = isVisible.value, + targetState = expanded.value, transitionSpec = { fadeIn(animationSpec = tween(300)) + expandVertically( @@ -213,6 +214,7 @@ fun TransactionsItemTrailing( @Preview fun PreviewTransactionsItem() { DesignTheme(isDarkMode = true) { + val expanded = remember { mutableStateOf(false) } TransactionsItem( title = "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!") }, @@ -220,6 +222,7 @@ fun PreviewTransactionsItem() { createAt = "10 Jun 2025, 04:20", onProfileClicked = {}, onMessageClicked = { _,_ -> }, + expanded = expanded, leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), 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 89d2dcc3d..4a447ed9c 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 @@ -14,6 +14,7 @@ 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.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -52,7 +53,8 @@ fun TransactionsList( key = { index -> items[index]?.id?.let { "$it;$index" } ?: index } ) { index -> items[index]?.let { transaction -> - val isVisible = remember { mutableStateOf(false) } + val isVisible = rememberSaveable { mutableStateOf(false) } + val expanded = rememberSaveable { mutableStateOf(false) } val isRecipient = transaction.recipient.id == uuid val profile = if (isRecipient) { transaction.sender @@ -69,6 +71,7 @@ fun TransactionsList( description = transaction.message, createAt = transaction.createdAt, price = "${transaction.amount.net}", + expanded = expanded, leading = { TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), @@ -93,7 +96,13 @@ fun TransactionsList( } } }, - onProfileClicked = { 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) ) { 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 f7a564b06..858829fed 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 @@ -55,7 +55,7 @@ fun TransactionsScreen( val updatedContent by rememberUpdatedState(content) DesignPagingStream( state = derivedState, - loading = { TransactionsSkeleton() }, + loading = { TransactionsSkeleton(3) }, error = { TransactionsError(it, component) { viewModel(Pageable(0, limit)) } } 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 index ff660ddf0..05e50db27 100644 --- 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 @@ -2,10 +2,13 @@ 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 @@ -16,6 +19,20 @@ 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( @@ -26,9 +43,11 @@ fun TransactionsSkeleton(modifier: Modifier = Modifier) { .background(MaterialTheme.colorScheme.surfaceDim) .padding(10.dp) ) { - Box(modifier = Modifier.size(36.dp) + Box(modifier = Modifier.padding(vertical = 4.dp) + .size(36.dp) .clip(CircleShape) - .background(MaterialTheme.colorScheme.background)) + .background(MaterialTheme.colorScheme.background) + ) } } @@ -36,8 +55,6 @@ fun TransactionsSkeleton(modifier: Modifier = Modifier) { @Preview fun PreviewTransactionsSkeleton() { DesignTheme(isDarkMode = true) { - TransactionsSkeleton( - modifier = Modifier.padding(16.dp) - ) + TransactionsSkeleton(3) } } From 58751a750673e1010f8f9edc61e196d9677b6aec Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Thu, 29 Jan 2026 10:03:44 +0100 Subject: [PATCH 14/15] chore(wallet): fix transaction details display --- .../main/kotlin/eu/peernetwork/app/ui/wallet/WalletPage.kt | 2 +- .../peernetwork/wallet/ui/transactions/TransactionsList.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 b169d2742..1f2867c39 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 @@ -37,7 +37,7 @@ fun WalletPage( header = { Box( modifier = Modifier.padding(top = 8.dp) - .padding(bottom = 16.dp) + .padding(bottom = 10.dp) ) { updatedHeader() } } ) { 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 4a447ed9c..5803020f5 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 @@ -70,7 +70,7 @@ fun TransactionsList( title = transaction.res?.let { stringResource(it) } ?: title, description = transaction.message, createAt = transaction.createdAt, - price = "${transaction.amount.net}", + price = "${transaction.amount.gross}", expanded = expanded, leading = { TransactionsAvatar( @@ -107,8 +107,8 @@ fun TransactionsList( modifier = Modifier.padding(bottom = 10.dp) ) { TransactionsSummeryItem( - title = stringResource(R.string.base_amount), - price = "${transaction.amount.gross}", + title = stringResource(R.string.transaction_amount_label), + price = "${transaction.amount.net}", ) transaction.fees?.let { val peer = (transaction.tax.peer * 100).toInt() From 79a633aa57ea37f4a764aff87be6508dcc36d16b Mon Sep 17 00:00:00 2001 From: conxtantyn Date: Thu, 29 Jan 2026 10:19:50 +0100 Subject: [PATCH 15/15] chore(wallet): add slug to user transaction detail --- .../ui/transactions/TransactionsItem.kt | 6 +-- .../ui/transactions/TransactionsList.kt | 8 ++-- .../ui/transactions/TransactionsUsername.kt | 48 +++++++++++++------ 3 files changed, 42 insertions(+), 20 deletions(-) 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 93e9f9135..eb9170b27 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 @@ -49,7 +49,7 @@ import eu.peernetwork.wallet.ui.R @Composable fun TransactionsItem( - title: String, + title: AnnotatedString, description: AnnotatedString?, createAt: String, modifier: Modifier = Modifier, @@ -113,7 +113,7 @@ fun TransactionsItem( @Composable fun TransactionsItem( - title: String, + title: AnnotatedString, description: AnnotatedString?, price: String, createAt: String, @@ -216,7 +216,7 @@ fun PreviewTransactionsItem() { DesignTheme(isDarkMode = true) { val expanded = remember { mutableStateOf(false) } TransactionsItem( - title = "To @removed", + 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", 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 5803020f5..622f151f7 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 @@ -12,12 +12,12 @@ 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.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 @@ -67,7 +67,9 @@ fun TransactionsList( user = profile ) TransactionsItem( - title = transaction.res?.let { stringResource(it) } ?: title, + title = transaction.res?.let { + buildAnnotatedString { append(stringResource(it)) } + } ?: title, description = transaction.message, createAt = transaction.createdAt, price = "${transaction.amount.gross}", @@ -76,7 +78,7 @@ fun TransactionsList( TransactionsAvatar( icon = painterResource(R.drawable.ic_transfer_direction), contentDescription = transaction.res?.let { stringResource(it) } - ?: title, + ?: title.text, isRecipient = isRecipient ) { if (transaction.icon != null && transaction.res != null) { 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 index 2f5200fd4..7bb4f2460 100644 --- 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 @@ -2,11 +2,17 @@ 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 @@ -19,25 +25,39 @@ fun transactionsUsername( user: UiUser, isRecipient: Boolean, isVisible: MutableState, -): String { - return if (user.status == UiStatus.ILLEGAL) { - stringResource(R.string.illegal_username) - } else if (!user.isAccessible) { - if (isVisible.value) { - if (isRecipient) { - stringResource(R.string.received_from, user.username) +): 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 { - stringResource(R.string.sent_to, user.username) + append(stringResource(R.string.hidden_username)) } } else { - stringResource(R.string.hidden_username) + Label(user, isRecipient) } + } +} + +@Composable +private fun AnnotatedString.Builder.Label( + user: UiUser, + isRecipient: Boolean +) { + append(if (isRecipient) { + stringResource(R.string.received_from, user.username) } else { - if (isRecipient) { - stringResource(R.string.received_from, user.username) - } else { - stringResource(R.string.sent_to, user.username) - } + 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}") } }