Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MOB-1856 - Show tokens in public profile #415

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
be213e9
Created HomeExploreView
Oleg-Pecheneg Feb 23, 2024
1ff440c
Increased tappable area of chat nav title profile selector
Oleg-Pecheneg Feb 23, 2024
eeff3bd
Created explore view model.
Oleg-Pecheneg Feb 23, 2024
7615517
Added explore view to tab bar
Oleg-Pecheneg Feb 23, 2024
3f323be
Prepare mock data
Oleg-Pecheneg Feb 23, 2024
031864e
Implemented UI for followers section
Oleg-Pecheneg Feb 23, 2024
f07f1f2
Added expandable followers/followings sections
Oleg-Pecheneg Feb 23, 2024
4eb07b8
Added separators
Oleg-Pecheneg Feb 23, 2024
a79a132
Moved search functionality to explore section
Oleg-Pecheneg Feb 23, 2024
e1de387
Set explore title visibility
Oleg-Pecheneg Feb 23, 2024
7a2ffbe
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1837-explo…
Oleg-Pecheneg Feb 27, 2024
5abd9be
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1837-explo…
Oleg-Pecheneg Feb 29, 2024
5560cd4
Created UDSegmentedControlView
Oleg-Pecheneg Feb 29, 2024
32c5b8f
Updated segmented control UI
Oleg-Pecheneg Feb 29, 2024
0c47bd0
Added section with trending profiles
Oleg-Pecheneg Feb 29, 2024
ef4d102
Fixed preview target
Oleg-Pecheneg Feb 29, 2024
81ef4de
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1837-explo…
Oleg-Pecheneg Mar 1, 2024
3ea63aa
Implemented UI for followers picker
Oleg-Pecheneg Mar 1, 2024
eb830da
Refactoring.
Oleg-Pecheneg Mar 1, 2024
a0ce2f4
View refactoring
Oleg-Pecheneg Mar 1, 2024
3615c27
Implemented recent section header view
Oleg-Pecheneg Mar 1, 2024
df1bf73
Working on user domains search result
Oleg-Pecheneg Mar 1, 2024
149a00d
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1837-explo…
Oleg-Pecheneg Mar 4, 2024
cb7c944
Updated imge bridge view
Oleg-Pecheneg Mar 4, 2024
385c5f8
Handle domain selection
Oleg-Pecheneg Mar 4, 2024
7419364
Added empty state to search result.
Oleg-Pecheneg Mar 4, 2024
54603fa
Use adjusted key to search for user's domains
Oleg-Pecheneg Mar 4, 2024
b60beae
Public domain view refactoring
Oleg-Pecheneg Mar 4, 2024
44970a3
Rename crypto to addresses
Oleg-Pecheneg Mar 4, 2024
4e759c8
Extraced token ui description from home module
Oleg-Pecheneg Mar 4, 2024
c0b5e8a
Show list of tokens on public profile
Oleg-Pecheneg Mar 4, 2024
9d7d1b4
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1837-explo…
Oleg-Pecheneg Mar 4, 2024
936b405
Merge branch 'dev/feat/MOB-1837-explore-ui' into dev/feat/MOB-1856-pu…
Oleg-Pecheneg Mar 4, 2024
a94106d
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1856-publi…
Oleg-Pecheneg Mar 5, 2024
fe2b74f
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1856-publi…
Oleg-Pecheneg Mar 5, 2024
96b040c
Merge branch 'dev/feat/MOB-1836-explore' into dev/feat/MOB-1856-publi…
Oleg-Pecheneg Mar 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,8 @@ final class PreviewWalletsDataService: WalletsDataServiceProtocol {
func didChangeEnvironment() {

}

func loadBalanceFor(walletAddress: HexAddress) async throws -> [WalletTokenPortfolio] {
MockEntitiesFabric.Wallet.mockEntities()[0].balance
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,8 @@ extension NetworkService {
}

public func fetchPublicProfile(for domainName: DomainName, fields: Set<GetDomainProfileField>) async throws -> SerializedPublicDomainProfile {
.init(profile: .init(displayName: nil,
description: nil,
location: nil,
web2Url: nil,
imagePath: nil,
imageType: nil,
coverPath: nil,
phoneNumber: nil,
domainPurchased: nil),
metadata: MockEntitiesFabric.DomainProfile.createPublicDomainMetadata(domain: domainName, walletAddress: "0x1"),
socialAccounts: nil,
referralCode: nil,
social: nil,
records: nil,
walletBalances: [])
MockEntitiesFabric.DomainProfile.createPublicProfile(domain: domainName,
walletBalance: MockEntitiesFabric.DomainProfile.createPublicProfileWalletBalances())
}

public func refreshDomainBadges(for domain: DomainItem) async throws -> RefreshBadgesResponse {
Expand Down
50 changes: 50 additions & 0 deletions unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// BalanceStringFormatter.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 04.03.2024.
//

import Foundation

struct BalanceStringFormatter {

static func tokensBalanceString(_ balance: Double) -> String {
"$\(balance.formatted(toMaxNumberAfterComa: 2))"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
//
// BalanceTokenUIDescription.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 04.03.2024.
//

import UIKit

struct BalanceTokenUIDescription: Hashable, Identifiable {
var id: String { "\(chain)/\(symbol)" }

let chain: String
let symbol: String
let name: String
let balance: Double
let balanceUsd: Double
var marketUsd: Double?
var marketPctChange24Hr: Double?
var parentSymbol: String?
var logoURL: URL?
var parentLogoURL: URL?
private(set) var isSkeleton: Bool = false

static let iconSize: InitialsView.InitialsSize = .default
static let iconStyle: InitialsView.Style = .gray

init(walletBalance: WalletTokenPortfolio) {
self.chain = walletBalance.symbol
self.symbol = walletBalance.symbol
self.name = walletBalance.name
self.balance = walletBalance.balanceAmt.rounded(toDecimalPlaces: 2)
self.balanceUsd = walletBalance.value.walletUsdAmt
self.marketUsd = walletBalance.value.marketUsdAmt ?? 0
self.marketPctChange24Hr = walletBalance.value.marketPctChange24Hr
}

init(chain: String,
symbol: String,
name: String,
balance: Double,
balanceUsd: Double,
marketUsd: Double? = nil,
marketPctChange24Hr: Double? = nil,
icon: UIImage? = nil) {
self.chain = chain
self.symbol = symbol
self.name = name
self.balance = balance
self.balanceUsd = balanceUsd
self.marketUsd = marketUsd
self.marketPctChange24Hr = marketPctChange24Hr
}

init(chain: String, walletToken: WalletTokenPortfolio.Token, parentSymbol: String, parentLogoURL: URL?) {
self.chain = chain
self.symbol = walletToken.symbol
self.name = walletToken.name
self.balance = walletToken.balanceAmt.rounded(toDecimalPlaces: 2)
self.balanceUsd = walletToken.value?.walletUsdAmt ?? 0
self.marketUsd = walletToken.value?.marketUsdAmt ?? 0
self.marketPctChange24Hr = walletToken.value?.marketPctChange24Hr
self.parentSymbol = parentSymbol
self.logoURL = URL(string: walletToken.logoUrl ?? "")
}

static func extractFrom(walletBalance: WalletTokenPortfolio) -> [BalanceTokenUIDescription] {
let tokenDescription = BalanceTokenUIDescription(walletBalance: walletBalance)
let parentSymbol = walletBalance.symbol
let parentLogoURL = URL(string: walletBalance.logoUrl ?? "")
let chainSymbol = walletBalance.symbol
let subTokenDescriptions = walletBalance.tokens?.map({ BalanceTokenUIDescription(chain: chainSymbol,
walletToken: $0,
parentSymbol: parentSymbol,
parentLogoURL: parentLogoURL) })
.filter({ $0.balanceUsd >= 1 }) ?? []

return [tokenDescription] + subTokenDescriptions
}

static func createSkeletonEntity() -> BalanceTokenUIDescription {
var token = BalanceTokenUIDescription(chain: "ETH", symbol: "000", name: "0000000000000000", balance: 10000, balanceUsd: 10000, marketUsd: 1)
token.isSkeleton = true
return token
}

func loadTokenIcon(iconUpdated: @escaping (UIImage?)->()) {
BalanceTokenUIDescription.loadIconFor(ticker: symbol, logoURL: logoURL, iconUpdated: iconUpdated)
}

func loadParentIcon(iconUpdated: @escaping (UIImage?)->()) {
if let parentSymbol {
BalanceTokenUIDescription.loadIconFor(ticker: parentSymbol, logoURL: parentLogoURL, iconUpdated: iconUpdated)
} else {
iconUpdated(nil)
}
}

static func loadIconFor(ticker: String, logoURL: URL?, iconUpdated: @escaping (UIImage?)->()) {
if let logoURL,
let cachedImage = appContext.imageLoadingService.cachedImage(for: .url(logoURL, maxSize: nil), downsampleDescription: .icon) {
iconUpdated(cachedImage)
return
}

let size = BalanceTokenUIDescription.iconSize
let style = BalanceTokenUIDescription.iconStyle
if let cachedImage = appContext.imageLoadingService.cachedImage(for: .currencyTicker(ticker,
size: size,
style: style),
downsampleDescription: .icon) {
iconUpdated(cachedImage)
return
}
Task { @MainActor in
let initials = await appContext.imageLoadingService.loadImage(from: .initials(ticker,
size: size,
style: style),
downsampleDescription: nil)
iconUpdated(initials)

if let logoURL,
let icon = await appContext.imageLoadingService.loadImage(from: .url(logoURL,
maxSize: nil),
downsampleDescription: .icon) {
iconUpdated(icon)
} else if let icon = await appContext.imageLoadingService.loadImage(from: .currencyTicker(ticker,
size: size,
style: style),
downsampleDescription: .icon) {
iconUpdated(icon)
}
}
}
}

extension Array where Element == BalanceTokenUIDescription {

func totalBalanceUSD() -> Double {
self.reduce(0.0, { $0 + $1.balanceUsd })
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,15 @@ extension MockEntitiesFabric {
walletAddress: String = "0x1",
attributes: PublicDomainProfileAttributes = DomainProfile.createEmptyPublicProfileAttributes(),
socialAccounts: SocialAccounts? = nil,
social: DomainProfileSocialInfo? = nil) -> SerializedPublicDomainProfile {
social: DomainProfileSocialInfo? = nil,
walletBalance: [ProfileWalletBalance]? = nil) -> SerializedPublicDomainProfile {
.init(profile: attributes,
metadata: createPublicDomainMetadata(domain: domain, walletAddress: walletAddress),
socialAccounts: socialAccounts,
referralCode: nil,
social: social,
records: nil,
walletBalances: nil)
walletBalances: walletBalance)
}

static func createEmptyPublicProfileAttributes() -> PublicDomainProfileAttributes {
Expand Down Expand Up @@ -182,5 +183,9 @@ extension MockEntitiesFabric {
meta: .init(totalCount: followerNames.count,
pagination: .init(cursor: nil, take: followerNames.count)))
}

static func createPublicProfileWalletBalances() -> [ProfileWalletBalance] {
[]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ extension String {
static let yours = "YOURS"
static let recent = "RECENT"
static let primary = "PRIMARY"

static let collapse = "COLLAPSE"

//Onboarding
static let alreadyMintedDomain = "ALREADY_MINTED_DOMAIN"
static let mintYourDomain = "MINT_YOUR_DOMAIN"
Expand Down Expand Up @@ -364,6 +365,7 @@ extension String {
static let pluralNFollowing = "SDICT:N_FOLLOWING"
static let pluralNProfilesFound = "SDICT:N_PROFILES_FOUND"
static let pluralNHolders = "SDICT:N_HOLDERS"
static let pluralNAddresses = "SDICT:N_ADDRESSES"

// Errors
static let creationFailed = "CREATION_FAILED"
Expand Down Expand Up @@ -1116,6 +1118,7 @@ extension String {
static let introBalanceBody = "INTRO_BALANCE_BODY"
static let introCollectiblesBody = "INTRO_COLLECTIBLES_BODY"
static let introMessagesBody = "INTRO_MESSAGES_BODY"
static let totalN = "TOTAL_N"

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// PublicProfileSeparatorView.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 04.03.2024.
//

import SwiftUI

struct PublicProfileSeparatorView: View {
var body: some View {
LineView(direction: .horizontal, dashed: true)
.foregroundColor(.white)
.opacity(0.08)
.padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
}
}

#Preview {
PublicProfileSeparatorView()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//
// PublicProfileTokensSectionView.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 04.03.2024.
//

import SwiftUI

struct PublicProfileTokensSectionView: View, ViewAnalyticsLogger {

@EnvironmentObject var viewModel: PublicProfileView.PublicProfileViewModel
@Environment(\.analyticsViewName) var analyticsName

private let minNumberOfVisibleTokens: Int = 2

var body: some View {
if let tokens = viewModel.tokens {
PublicProfileSeparatorView()
titleView(tokens: tokens)
LazyVStack(spacing: 20) {
ForEach(getTokensListForCollapsedState(tokens: tokens)) { token in
HomeWalletTokenRowView(token: token,
secondaryColor: .white.opacity(0.56))
.padding(EdgeInsets(top: -8, leading: 0, bottom: -8, trailing: 0))
}
}
collapseViewIfAvailable(tokens: tokens)
}
}
}

// MARK: - Private methods
private extension PublicProfileTokensSectionView {
func getTokensListForCollapsedState(tokens: [BalanceTokenUIDescription]) -> [BalanceTokenUIDescription] {
if viewModel.isTokensCollapsed {
return Array(tokens.prefix(minNumberOfVisibleTokens))
} else {
return tokens
}
}
}

// MARK: - Private methods
private extension PublicProfileTokensSectionView {
@ViewBuilder
func titleView(tokens: [BalanceTokenUIDescription]) -> some View {
HStack {
PublicProfilePrimaryLargeTextView(text: String.Constants.tokens.localized())
PublicProfileSecondaryLargeTextView(text: "\(tokens.count)")
Spacer()
totalValueView(tokens: tokens)
}
}

@ViewBuilder
func totalValueView(tokens: [BalanceTokenUIDescription]) -> some View {
Text(String.Constants.totalN.localized(BalanceStringFormatter.tokensBalanceString(tokens.totalBalanceUSD())))
.foregroundStyle(Color.white.opacity(0.56))
.font(.currentFont(size: 16, weight: .medium))
}

@ViewBuilder
func collapseViewIfAvailable(tokens: [BalanceTokenUIDescription]) -> some View {
if tokens.count > minNumberOfVisibleTokens {
collapseView()
}
}

var collapseViewTitle: String {
if viewModel.isTokensCollapsed {
return String.Constants.showMore.localized()
}
return String.Constants.collapse.localized()
}

var collapseViewImage: Image {
if viewModel.isTokensCollapsed {
return Image.chevronDown
}
return Image.chevronUp
}

@ViewBuilder
func collapseView() -> some View {
Button {
UDVibration.buttonTap.vibrate()
if viewModel.isTokensCollapsed {
logButtonPressedAnalyticEvents(button: .showAll)
} else {
logButtonPressedAnalyticEvents(button: .hide)
}
withAnimation {
viewModel.isTokensCollapsed.toggle()
}
} label: {
HStack {
collapseViewImage
.resizable()
.squareFrame(20)
Text(collapseViewTitle)
.font(.currentFont(size: 16, weight: .medium))
}
.foregroundStyle(Color.white)
.frame(height: 40)
.frame(maxWidth: .infinity)
.background(Color.white.opacity(0.16))
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
}

#Preview {
PublicProfileTokensSectionView()
}
Loading