Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions Projects/Detail/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@ let interfaceDependecies: [TargetDependency] = [
path: "../Navigation"),
]

let testsDependecies: [TargetDependency] = [
.project(target: "DependencyInjectionTesting",
path: "../DependencyInjection"),
.project(target: "NavigationTesting",
path: "../Navigation"),
]

let project = Project.templateModule(named: moduleName,
targets: [.source, .interfaces, .test, .testing],
dependencies: dependecies,
testDependencies: testsDependecies,
interfaceDependecies: interfaceDependecies)
22 changes: 22 additions & 0 deletions Projects/Detail/Sources/Coordinator/DetailCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,28 @@ public final class DetailCoordinator: DetailCoordinating {
// MARK: - DetailCoordinating

public func start() {
let resolver = SharedContainer.shared.resolver()
let networkService: NetworkServiceProtocol = resolver.resolve()
let detailService = DetailService(networkService: networkService)
let viewModel = DetailViewModel(service: detailService)
if let exchange {
viewModel.configure(with: exchange)
}
if let exchangeId {
viewModel.configure(with: exchangeId)
}

viewModel.coordinatorDelegate = self
let viewController = DetailViewController(viewModel: viewModel)

navigationController.pushViewController(viewController, animated: true)
}
}

// MARK: - DetailViewModelCoordinatorDelegate

extension DetailCoordinator: DetailViewModelCoordinatorDelegate {
public func didRequestOpenURL(_ url: URL) {
UIApplication.shared.open(url)
}
}
23 changes: 23 additions & 0 deletions Projects/Detail/Sources/Model/Asset.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

struct Asset: Sendable, Equatable {
let id: Int
let name: String
let symbol: String
let priceUsd: Double
}

extension Asset: Codable {
enum CodingKeys: String, CodingKey {
case id, name, symbol
case priceUsd = "price_usd"
}
}
18 changes: 18 additions & 0 deletions Projects/Detail/Sources/Model/DetailViewState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

enum DetailViewState: Equatable {
case loading
case loaded(ExchangeDetailModel)
case loadingAssets
case loadedAssets
case error(String)
case errorLoadAssets(title: String, message: String, code: Int)
}
41 changes: 41 additions & 0 deletions Projects/Detail/Sources/Model/ExchangeAssetsResponse.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation
import HomeInterfaces

struct ExchangeAssetsResponse: Codable, Equatable {
let status: Status
let data: [AssetResponse]
}

struct AssetResponse: Codable, Equatable {
let walletAddress: String
let balance: Double
let currency: Currency

enum CodingKeys: String, CodingKey {
case walletAddress = "wallet_address"
case balance
case currency
}
}

struct Currency: Codable, Equatable {
let cryptoId: Int
let priceUsd: Double
let symbol: String
let name: String

enum CodingKeys: String, CodingKey {
case cryptoId = "crypto_id"
case priceUsd = "price_usd"
case symbol
case name
}
}
40 changes: 40 additions & 0 deletions Projects/Detail/Sources/Model/ExchangeDetailModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation
import HomeInterfaces

struct ExchangeDetailModel: Equatable {
let id: Int
let name: String
let description: String
let logoUrl: String
let spotVolumeUsd: Double?
let makerFee: Double
let takerFee: Double
let dateLaunched: String
let websiteUrl: String?
let twitterUrl: String?
}

extension ExchangeDetailModel {
init(from exchangeDetail: Exchange) {
self.init(
id: exchangeDetail.id,
name: exchangeDetail.name,
description: exchangeDetail.description ?? "No description available",
logoUrl: exchangeDetail.logo,
spotVolumeUsd: exchangeDetail.spotVolumeUsd,
makerFee: exchangeDetail.makerFee,
takerFee: exchangeDetail.takerFee,
dateLaunched: exchangeDetail.dateLaunched,
websiteUrl: exchangeDetail.websiteUrl,
twitterUrl: exchangeDetail.twitterUrl
)
}
}
13 changes: 13 additions & 0 deletions Projects/Detail/Sources/Protocols/DetailServiceProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

protocol DetailServiceProtocol: AnyObject, Sendable {
func fetchExchangeAssets(id: Int) async throws -> [Asset]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

@MainActor
protocol DetailViewModelCoordinatorDelegate: AnyObject {
func didRequestOpenURL(_ url: URL)
}
14 changes: 14 additions & 0 deletions Projects/Detail/Sources/Protocols/DetailViewModelDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

@MainActor
protocol DetailViewModelDelegate: AnyObject {
func didUpdateState(_ state: DetailViewState)
}
21 changes: 21 additions & 0 deletions Projects/Detail/Sources/Protocols/DetailViewModelProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation

@MainActor
protocol DetailViewModelProtocol: AnyObject {
var assets: [Asset] { get }
var delegate: DetailViewModelDelegate? { get set }
var state: DetailViewState { get }

func loadData()
func didTapRetry()
func didTapWebsite()
func didTapTwitter()
}
35 changes: 35 additions & 0 deletions Projects/Detail/Sources/Service/DetailEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import NetworkingInterfaces
import Foundation

enum DetailEndpoint: APIEndpointProtocol {
case fetchAssets(id: Int)

var baseURL: String {
ProcessInfo.processInfo.environment["CM_API_BASE_URL"] ?? ""
}

var path: String {
switch self {
case let .fetchAssets(id):
return "/v1/exchange/assets?id=\(id)"
}
}

var method: NetworkingInterfaces.HTTPMethod {
.get
}

var headers: [String: String]? {
[
"X-CMC_PRO_API_KEY": ProcessInfo.processInfo.environment["CM_API_KEY"] ?? ""
]
}
}
58 changes: 58 additions & 0 deletions Projects/Detail/Sources/Service/DetailService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
//
// Created by Vitor Conceicao.
//
// github.com/vitor-rc1
//
//

import Foundation
import HomeInterfaces
import NetworkingInterfaces

enum DetailServiceError: Error, Equatable {
case decodeFail(String?)
case network(Status)
}

final class DetailService: Sendable {
private nonisolated(unsafe) let networkService: NetworkServiceProtocol

init(networkService: NetworkServiceProtocol) {
self.networkService = networkService
}

private func performRequest<T: Codable>(endpoint: APIEndpointProtocol) async throws -> T {
let (data, httpUrlResponse) = try await networkService.request(endpoint: endpoint)

switch httpUrlResponse.statusCode {
case 200...299:
return try decodeResponse(data)
default:
let error: Status = try decodeResponse(data)
throw DetailServiceError.network(error)
}
}

private func decodeResponse<T: Codable>(_ data: Data) throws -> T {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
}
}

extension DetailService: DetailServiceProtocol {
func fetchExchangeAssets(id: Int) async throws -> [Asset] {
let endpoint = DetailEndpoint.fetchAssets(id: id)

let response: ExchangeAssetsResponse = try await performRequest(endpoint: endpoint)

return response
.data
.map { asset in
Asset(id: asset.currency.cryptoId,
name: asset.currency.name,
symbol: asset.currency.symbol,
priceUsd: asset.currency.priceUsd)
}
}
}
Loading
Loading