Full Code of pitt500/OnlineStoreTCA for AI

main 89a5e4eaab96 cached
37 files
139.6 KB
35.8k tokens
1 requests
Download .txt
Repository: pitt500/OnlineStoreTCA
Branch: main
Commit: 89a5e4eaab96
Files: 37
Total size: 139.6 KB

Directory structure:
gitextract_gp_koxcg/

├── .gitignore
├── OnlineStoreTCA/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   ├── bag.imageset/
│   │   │   └── Contents.json
│   │   ├── jacket.imageset/
│   │   │   └── Contents.json
│   │   └── tshirt.imageset/
│   │       └── Contents.json
│   ├── CartList/
│   │   ├── Cart/
│   │   │   ├── CartCell.swift
│   │   │   ├── CartItem.swift
│   │   │   └── CartItemDomain.swift
│   │   ├── CartListDomain.swift
│   │   ├── CartListView.swift
│   │   └── Test/
│   │       └── CartListDomainTest.swift
│   ├── Network/
│   │   ├── APIClient.swift
│   │   └── DataLoadingStatus.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── Products/
│   │   ├── AddToCart/
│   │   │   ├── AddToCartButton.swift
│   │   │   ├── AddToCartDomain.swift
│   │   │   ├── PlusMinusButton.swift
│   │   │   └── Test/
│   │   │       └── AddToCartDomainTest.swift
│   │   ├── Product/
│   │   │   ├── Product.swift
│   │   │   ├── ProductCell.swift
│   │   │   ├── ProductDomain.swift
│   │   │   └── Test/
│   │   │       └── ProductDomainTest.swift
│   │   └── ProductList/
│   │       ├── ErrorView.swift
│   │       ├── ProductListDomain.swift
│   │       ├── ProductListView.swift
│   │       └── Test/
│   │           └── ProductListDomainTest.swift
│   ├── Profile/
│   │   ├── ProfileDomain.swift
│   │   ├── ProfileView.swift
│   │   └── UserProfile.swift
│   └── Root/
│       ├── OnlineStoreTCAApp.swift
│       ├── RootDomain.swift
│       └── RootView.swift
├── OnlineStoreTCA.xcodeproj/
│   └── project.pbxproj
├── OnlineStoreTCATests/
│   └── OnlineStoreTCATests.swift
└── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,macos
# Edit at https://www.toptal.com/developers/gitignore?templates=xcode,swift,macos

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon


# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

### macOS Patch ###
# iCloud generated files
*.icloud

### Swift ###
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore

## User settings
xcuserdata/

## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout

## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3

## Obj-C/Swift specific
*.hmap

## App packaging
*.ipa
*.dSYM.zip
*.dSYM

## Playgrounds
timeline.xctimeline
playground.xcworkspace

# Swift Package Manager
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm

.build/

# CocoaPods
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
# Pods/
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace

# Carthage
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts

Carthage/Build/

# Accio dependency management
Dependencies/
.accio/

# fastlane
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control

fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

# Code Injection
# After new code Injection tools there's a generated folder /iOSInjectionProject
# https://github.com/johnno1962/injectionforxcode

iOSInjectionProject/

### Xcode ###

## Xcode 8 and earlier

### Xcode Patch ###
*.xcodeproj/*
!*.xcodeproj/project.pbxproj
!*.xcodeproj/xcshareddata/
!*.xcworkspace/contents.xcworkspacedata
/*.gcno
**/xcshareddata/WorkspaceSettings.xcsettings

# End of https://www.toptal.com/developers/gitignore/api/xcode,swift,macos


================================================
FILE: OnlineStoreTCA/Assets.xcassets/AccentColor.colorset/Contents.json
================================================
{
  "colors" : [
    {
      "idiom" : "universal"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images" : [
    {
      "idiom" : "iphone",
      "scale" : "2x",
      "size" : "20x20"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x",
      "size" : "20x20"
    },
    {
      "idiom" : "iphone",
      "scale" : "2x",
      "size" : "29x29"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x",
      "size" : "29x29"
    },
    {
      "idiom" : "iphone",
      "scale" : "2x",
      "size" : "40x40"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x",
      "size" : "40x40"
    },
    {
      "idiom" : "iphone",
      "scale" : "2x",
      "size" : "60x60"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x",
      "size" : "60x60"
    },
    {
      "idiom" : "ipad",
      "scale" : "1x",
      "size" : "20x20"
    },
    {
      "idiom" : "ipad",
      "scale" : "2x",
      "size" : "20x20"
    },
    {
      "idiom" : "ipad",
      "scale" : "1x",
      "size" : "29x29"
    },
    {
      "idiom" : "ipad",
      "scale" : "2x",
      "size" : "29x29"
    },
    {
      "idiom" : "ipad",
      "scale" : "1x",
      "size" : "40x40"
    },
    {
      "idiom" : "ipad",
      "scale" : "2x",
      "size" : "40x40"
    },
    {
      "idiom" : "ipad",
      "scale" : "1x",
      "size" : "76x76"
    },
    {
      "idiom" : "ipad",
      "scale" : "2x",
      "size" : "76x76"
    },
    {
      "idiom" : "ipad",
      "scale" : "2x",
      "size" : "83.5x83.5"
    },
    {
      "idiom" : "ios-marketing",
      "scale" : "1x",
      "size" : "1024x1024"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Assets.xcassets/bag.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "bag.jpg",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Assets.xcassets/jacket.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "jacket.jpg",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Assets.xcassets/tshirt.imageset/Contents.json
================================================
{
  "images" : [
    {
      "filename" : "tshirt.jpg",
      "idiom" : "universal",
      "scale" : "1x"
    },
    {
      "idiom" : "universal",
      "scale" : "2x"
    },
    {
      "idiom" : "universal",
      "scale" : "3x"
    }
  ],
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/CartList/Cart/CartCell.swift
================================================
//
//  CartCell.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 22/08/22.
//

import SwiftUI
import ComposableArchitecture

struct CartCell: View {
    let store: StoreOf<CartItemDomain>
    
    var body: some View {
        WithPerceptionTracking {
            VStack {
                HStack {
                    AsyncImage(
                        url: URL(
                            string: store.cartItem.product.imageString
                        )
                    ) {
                        $0
                            .resizable()
                            .aspectRatio(contentMode: .fit)
                            .frame(width: 100, height: 100)
                    } placeholder: {
                        ProgressView()
                            .frame(width: 100, height: 100)
                    }
                    VStack(alignment: .leading) {
                        Text(store.cartItem.product.title)
                            .lineLimit(3)
                            .minimumScaleFactor(0.5)
                        HStack {
                            Text("$\(store.cartItem.product.price.description)")
                                .font(.custom("AmericanTypewriter", size: 25))
                                .fontWeight(.bold)
                        }
                    }
                    
                }
                ZStack {
                    Group {
                        Text("Quantity: ")
                        +
                        Text("\(store.cartItem.quantity)")
                            .fontWeight(.bold)
                    }
                    .font(.custom("AmericanTypewriter", size: 25))
                    HStack {
                        Spacer()
                        Button {
                            store.send(
                                .deleteCartItem(
                                    product: store.cartItem.product
                                )
                            )
                        } label: {
                            Image(systemName: "trash.fill")
                                .foregroundColor(.red)
                                .padding()
                        }
                    }
                }
            }
            .font(.custom("AmericanTypewriter", size: 20))
            .padding([.bottom, .top], 10)
        }
    }
}

struct CartCell_Previews: PreviewProvider {
    static var previews: some View {
        CartCell(
            store: Store(
                initialState: CartItemDomain.State(
                    id: UUID(),
                    cartItem: CartItem.sample.first!
                ),
                reducer: { CartItemDomain() }
            )
        )
        .previewLayout(.fixed(width: 300, height: 300))
    }
}


================================================
FILE: OnlineStoreTCA/CartList/Cart/CartItem.swift
================================================
//
//  CartItem.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 18/08/22.
//

import Foundation

struct CartItem: Equatable {
    let product: Product
    let quantity: Int
}

extension CartItem: Encodable {
    private enum CartItemsKey: String, CodingKey {
        case productId
        case quantity
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CartItemsKey.self)
        try container.encode(product.id, forKey: .productId)
        try container.encode(quantity, forKey: .quantity)
    }
}

extension CartItem {
    static var sample: [CartItem] {
        [
            .init(
                product: Product.sample[0],
                quantity: 3
            ),
            .init(
                product: Product.sample[1],
                quantity: 1
            ),
            .init(
                product: Product.sample[2],
                quantity: 1
            ),
        ]
    }
}


================================================
FILE: OnlineStoreTCA/CartList/Cart/CartItemDomain.swift
================================================
//
//  CartItemDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 22/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct CartItemDomain {
    @ObservableState
    struct State: Equatable, Identifiable {
        let id: UUID
        let cartItem: CartItem
    }
    
    enum Action: Equatable {
        case deleteCartItem(product: Product)
    }
    
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
            case .deleteCartItem:
                return .none
        }
    }
}


================================================
FILE: OnlineStoreTCA/CartList/CartListDomain.swift
================================================
//
//  CartListDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 18/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct CartListDomain {
    @ObservableState
    struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        var dataLoadingStatus = DataLoadingStatus.notStarted
        var cartItems: IdentifiedArrayOf<CartItemDomain.State> = []
        var totalPrice: Double = 0.0
        var isPayButtonDisable = false
        
        var totalPriceString: String {
            let roundedValue = round(totalPrice * 100) / 100.0
            return "$\(roundedValue)"
        }
        
        var isRequestInProcess: Bool {
            dataLoadingStatus == .loading
        }
    }
    
    enum Action: Equatable {
        case alert(PresentationAction<Alert>)
        case didPressCloseButton
        case cartItem(IdentifiedActionOf<CartItemDomain>)
        case getTotalPrice
        case didPressPayButton
        case didReceivePurchaseResponse(TaskResult<String>)
        
		@CasePathable
        enum Alert: Equatable {
            case didConfirmPurchase
            case didCancelConfirmation
            case dismissSuccessAlert
            case dismissErrorAlert
        }
    }
    
    @Dependency(\.apiClient.sendOrder) var sendOrder
    
    private func verifyPayButtonVisibility(
        state: inout State
    ) -> Effect<Action> {
        state.isPayButtonDisable = state.totalPrice == 0.0
        return .none
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                case let .alert(.presented(alertAction)):
                    switch alertAction {
                        case .didConfirmPurchase:
                            state.dataLoadingStatus = .loading
                            let items = state.cartItems.map { $0.cartItem }
                            return .run { send in
                                await send(
                                    .didReceivePurchaseResponse(
                                        TaskResult{
                                            try await sendOrder(items)
                                        }
                                    )
                                )
                            }
                        case .didCancelConfirmation:
                            state.alert = nil
                            return .none
                        case .dismissSuccessAlert:
                            state.alert = nil
                            return .none
                        case .dismissErrorAlert:
                            state.alert = nil
                            return .none
                    }
				case .alert:
					return .none
                case .didPressCloseButton:
                    return .none
                case let .cartItem(.element(id: id, action: action)):
                    switch action {
                        case .deleteCartItem:
                            state.cartItems.remove(id: id)
                            return.send(.getTotalPrice)
                    }
                case .getTotalPrice:
                    let items = state.cartItems.map { $0.cartItem }
                    state.totalPrice = items.reduce(0.0, {
                        $0 + ($1.product.price * Double($1.quantity))
                    })
                    return verifyPayButtonVisibility(state: &state)
                case .didPressPayButton:
                    state.alert = .confirmationAlert(totalPriceString: state.totalPriceString)
                    return .none
                case .didReceivePurchaseResponse(.success(let message)):
                    state.dataLoadingStatus = .success
                    state.alert = .successAlert
                    print("Success: ", message)
                    return .none
                case .didReceivePurchaseResponse(.failure(let error)):
                    state.dataLoadingStatus = .error
                    state.alert = .errorAlert
                    print("Error sending your order:", error.localizedDescription)
                    return .none
            }
        }
		.ifLet(\.$alert, action: \.alert)
        .forEach(\.cartItems, action: \.cartItem) {
            CartItemDomain()
        }
    }
}

extension AlertState where Action == CartListDomain.Action.Alert {
    static func confirmationAlert(totalPriceString: String) -> AlertState {
        AlertState {
            TextState("Confirm your purchase")
        } actions: {
            ButtonState(action: .didConfirmPurchase, label: { TextState("Pay \(totalPriceString)") })
            ButtonState(role: .cancel, action: .didCancelConfirmation, label: { TextState("Cancel") })
        } message: {
            TextState("Do you want to proceed with your purchase of \(totalPriceString)?")
        }
    }
    
    static var successAlert: AlertState {
        AlertState {
            TextState("Thank you!")
        } actions: {
            ButtonState(action: .dismissSuccessAlert, label: { TextState("Done") })
        } message: {
            TextState("Your order is in process.")
        }
    }
    
    static var errorAlert: AlertState {
        AlertState {
            TextState("Oops!")
        } actions: {
            ButtonState(action: .dismissErrorAlert, label: { TextState("Done") })
        } message: {
            TextState("Unable to send order, try again later.")
        }
    }
}



================================================
FILE: OnlineStoreTCA/CartList/CartListView.swift
================================================
//
//  CartListView.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 18/08/22.
//

import SwiftUI
import ComposableArchitecture

struct CartListView: View {
    let store: StoreOf<CartListDomain>
    
    var body: some View {
        WithPerceptionTracking {
            ZStack {
                NavigationStack {
                    Group {
                        if store.cartItems.isEmpty {
                            Text("Oops, your cart is empty! \n")
                                .font(.custom("AmericanTypewriter", size: 25))
                        } else {
                            List {
                                ForEach(
                                    store.scope(state: \.cartItems, action: \.cartItem),
                                    id: \.id
                                ) { store in
                                    CartCell(store: store)
                                }
                            }
                            .safeAreaInset(edge: .bottom) {
                                Button {
                                    store.send(.didPressPayButton)
                                } label: {
                                    HStack(alignment: .center) {
                                        Spacer()
                                        Text("Pay \(store.totalPriceString)")
                                            .font(.custom("AmericanTypewriter", size: 30))
                                            .foregroundColor(.white)
                                        
                                        Spacer()
                                    }
                                    
                                }
                                .frame(maxWidth: .infinity, minHeight: 60)
                                .background(
                                    store.isPayButtonDisable
                                    ? .gray
                                    : .blue
                                )
                                .cornerRadius(10)
                                .padding()
                                .disabled(store.isPayButtonDisable)
                            }
							.alert(
								store: store.scope(
									state: \.$alert,
									action: \.alert
								)
							)
                        }
                    }
                    .navigationTitle("Cart")
                    .toolbar {
                        ToolbarItem(placement: .navigationBarLeading) {
                            Button {
                                store.send(.didPressCloseButton)
                            } label: {
                                Text("Close")
                            }
                        }
                    }
                    .onAppear {
                        store.send(.getTotalPrice)
                    }
                }
                if store.isRequestInProcess {
                    Color.black.opacity(0.2)
                        .ignoresSafeArea()
                    ProgressView()
                }
            }
        }
    }
}

struct CartListView_Previews: PreviewProvider {
    static var previews: some View {
        CartListView(
            store: Store(
                initialState: CartListDomain.State(
                    cartItems: IdentifiedArrayOf(
                        uniqueElements: CartItem.sample
                            .map {
                                CartItemDomain.State(
                                    id: UUID(),
                                    cartItem: $0
                                )
                            }
                    )
                ),
                reducer: { CartListDomain() },
                withDependencies: {
                    $0.apiClient.sendOrder = { _ in "OK" }
                }
            )
        )
    }
}


================================================
FILE: OnlineStoreTCA/CartList/Test/CartListDomainTest.swift
================================================
//
//  CartListDomainTest.swift
//  OnlineStoreTCATests
//
//  Created by Pedro Rojas on 30/08/22.
//

import ComposableArchitecture
import XCTest

@testable import OnlineStoreTCA

@MainActor
class CartListDomainTest: XCTestCase {
    func testRemoveItemFromCart() async {
        let cartItemId1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let cartItemId2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        let itemQuantity = 2
        
        let cartItems: IdentifiedArrayOf<CartItemDomain.State> = [
            .init(
                id: cartItemId1,
                cartItem: CartItem.init(
                    product: Product.sample[0],
                    quantity: itemQuantity
                )
            ),
            .init(
                id: cartItemId2,
                cartItem: CartItem.init(
                    product: Product.sample[1],
                    quantity: itemQuantity
                )
            ),
        ]
        
        let store = TestStore(
            initialState: CartListDomain.State(cartItems: cartItems),
            reducer: { CartListDomain() }
        )
        
		await store.send(\.cartItem[id: cartItemId1].deleteCartItem, Product.sample[0]) {
            $0.cartItems = [
                .init(
                    id: cartItemId2,
                    cartItem: CartItem.init(
                        product: Product.sample[1],
                        quantity: itemQuantity
                    )
                )
            ]
        }
        
        let expectedPrice = Product.sample[1].price * Double(itemQuantity)
        await store.receive(\.getTotalPrice) {
            $0.totalPrice = expectedPrice
        }
    }
    
    func testRemoveAllItemsFromCart() async {
        let cartItemId1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let cartItemId2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        let itemQuantity = 2
        
        let cartItems: IdentifiedArrayOf<CartItemDomain.State> = [
            .init(
                id: cartItemId1,
                cartItem: CartItem.init(
                    product: Product.sample[0],
                    quantity: itemQuantity
                )
            ),
            .init(
                id: cartItemId2,
                cartItem: CartItem.init(
                    product: Product.sample[1],
                    quantity: itemQuantity
                )
            ),
        ]
        
        let store = TestStore(
            initialState: CartListDomain.State(cartItems: cartItems),
            reducer: { CartListDomain() }
        )
        
		await store.send(\.cartItem[id: cartItemId1].deleteCartItem, Product.sample[0]) {
            $0.cartItems = [
                .init(
                    id: cartItemId2,
                    cartItem: CartItem.init(
                        product: Product.sample[1],
                        quantity: itemQuantity
                    )
                )
            ]
        }
        
        let expectedPrice = Product.sample[1].price * Double(itemQuantity)
        await store.receive(\.getTotalPrice) {
            $0.totalPrice = expectedPrice
        }
        
		await store.send(\.cartItem[id: cartItemId2].deleteCartItem, Product.sample[1]) {
            $0.cartItems = []
        }
        
        await store.receive(\.getTotalPrice) {
            $0.totalPrice = 0
            $0.isPayButtonDisable = true
        }
        
    }
    
    func testSendOrderSuccessfully() async {
        let cartItemId1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let cartItemId2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        let itemQuantity = 2
        
        let cartItems: IdentifiedArrayOf<CartItemDomain.State> = [
            .init(
                id: cartItemId1,
                cartItem: CartItem.init(
                    product: Product.sample[0],
                    quantity: itemQuantity
                )
            ),
            .init(
                id: cartItemId2,
                cartItem: CartItem.init(
                    product: Product.sample[1],
                    quantity: itemQuantity
                )
            ),
        ]
        
        let store = TestStore(
            initialState: CartListDomain.State(cartItems: cartItems),
            reducer: { CartListDomain() }
        ) {
            $0.apiClient.sendOrder = { _ in "Success" }
        }
        
		await store.send(\.didPressPayButton) {
			$0.alert = .confirmationAlert(totalPriceString: "$0.0")
		}
		
		await store.send(\.alert.didConfirmPurchase) {
			$0.alert = nil
            $0.dataLoadingStatus = .loading
        }
        
        
        await store.receive(\.didReceivePurchaseResponse, .success("Success")) {
            $0.dataLoadingStatus = .success
            $0.alert = .successAlert
        }
    }
    
    func testSendOrderWithError() async {
        let cartItemId1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let cartItemId2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        let itemQuantity = 2
        
        let cartItems: IdentifiedArrayOf<CartItemDomain.State> = [
            .init(
                id: cartItemId1,
                cartItem: CartItem.init(
                    product: Product.sample[0],
                    quantity: itemQuantity
                )
            ),
            .init(
                id: cartItemId2,
                cartItem: CartItem.init(
                    product: Product.sample[1],
                    quantity: itemQuantity
                )
            ),
        ]
        
        let store = TestStore(
            initialState: CartListDomain.State(cartItems: cartItems),
            reducer: { CartListDomain() }
        ) {
            $0.apiClient.sendOrder = { _ in throw APIClient.Failure() }
        }
		
		await store.send(\.didPressPayButton) {
			$0.alert = .confirmationAlert(totalPriceString: "$0.0")
		}
        
        await store.send(\.alert.didConfirmPurchase) {
            $0.dataLoadingStatus = .loading
			$0.alert = nil
        }
        
        
        await store.receive(\.didReceivePurchaseResponse, .failure(APIClient.Failure())) {
            $0.dataLoadingStatus = .error
            $0.alert = .errorAlert
        }
    }
}


================================================
FILE: OnlineStoreTCA/Network/APIClient.swift
================================================
//
//  APIClient.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 23/08/22.
//

import Foundation
import ComposableArchitecture
import DependenciesMacros

extension DependencyValues {
    var apiClient: APIClient {
        get { self[APIClient.self] }
        set { self[APIClient.self] = newValue }
    }
}


@DependencyClient
struct APIClient {
    var fetchProducts:  @Sendable () async throws -> [Product]
    var sendOrder:  @Sendable ([CartItem]) async throws -> String
    var fetchUserProfile:  @Sendable () async throws -> UserProfile
    
    struct Failure: Error, Equatable {}
}

extension APIClient: TestDependencyKey {
    static let testValue = Self()
}

// This is the "live" fact dependency that reaches into the outside world to fetch the data from network.
// Typically this live implementation of the dependency would live in its own module so that the
// main feature doesn't need to compile it.
extension APIClient: DependencyKey {
    static let liveValue = Self(
        fetchProducts: {
            let (data, _) = try await URLSession.shared
                .data(from: URL(string: "https://fakestoreapi.com/products")!)
            let products = try JSONDecoder().decode([Product].self, from: data)
            return products
        },
        sendOrder: { cartItems in
            let payload = try JSONEncoder().encode(cartItems)
            var urlRequest = URLRequest(url: URL(string: "https://fakestoreapi.com/carts")!)
            urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
            urlRequest.httpMethod = "POST"
            
            let (data, response) = try await URLSession.shared.upload(for: urlRequest, from: payload)
            
            guard let httpResponse = (response as? HTTPURLResponse) else {
                throw Failure()
            }
            
            return "Status: \(httpResponse.statusCode)"
        },
        fetchUserProfile: {
            let (data, _) = try await URLSession.shared
                .data(from: URL(string: "https://fakestoreapi.com/users/1")!)
            let profile = try JSONDecoder().decode(UserProfile.self, from: data)
            return profile
        }
    )
}


================================================
FILE: OnlineStoreTCA/Network/DataLoadingStatus.swift
================================================
//
//  DataLoadingStatus.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 28/08/22.
//

import Foundation

enum DataLoadingStatus {
    case notStarted
    case loading
    case success
    case error
}


================================================
FILE: OnlineStoreTCA/Preview Content/Preview Assets.xcassets/Contents.json
================================================
{
  "info" : {
    "author" : "xcode",
    "version" : 1
  }
}


================================================
FILE: OnlineStoreTCA/Products/AddToCart/AddToCartButton.swift
================================================
//
//  AddToCartButton.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 20/08/22.
//

import SwiftUI
import ComposableArchitecture

struct AddToCartButton: View {
    let store: StoreOf<AddToCartDomain>
    
    var body: some View {
        WithPerceptionTracking {
            if store.count > 0 {
                PlusMinusButton(store: self.store)
            } else {
                Button {
                    store.send(.didTapPlusButton)
                } label: {
                    Text("Add to Cart")
                        .padding(10)
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .buttonStyle(.plain)
            }
        }
    }
}

struct AddToCartButton_Previews: PreviewProvider {
    static var previews: some View {
        AddToCartButton(
            store: Store(
                initialState: AddToCartDomain.State(),
                reducer: { AddToCartDomain() }
            )
        )
    }
}


================================================
FILE: OnlineStoreTCA/Products/AddToCart/AddToCartDomain.swift
================================================
//
//  PlusMinusDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 20/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct AddToCartDomain {
    @ObservableState
    struct State: Equatable {
        var count = 0
    }
    
    enum Action: Equatable {
        case didTapPlusButton
        case didTapMinusButton
    }
    
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
            case .didTapPlusButton:
                state.count += 1
                return .none
            case .didTapMinusButton:
                state.count -= 1
                return .none
        }
    }
}


================================================
FILE: OnlineStoreTCA/Products/AddToCart/PlusMinusButton.swift
================================================
//
//  PlusMinusButton.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 20/08/22.
//

import SwiftUI
import ComposableArchitecture

struct PlusMinusButton: View {
    let store: StoreOf<AddToCartDomain>
    
    var body: some View {
        WithPerceptionTracking {
            HStack {
                Button {
                    store.send(.didTapMinusButton)
                } label: {
                    Text("-")
                        .padding(10)
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .buttonStyle(.plain)
                
                Text(store.count.description)
                    .padding(5)
                
                Button {
                    store.send(.didTapPlusButton)
                } label: {
                    Text("+")
                        .padding(10)
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .buttonStyle(.plain)
            }
        }
    }
}

struct PlusMinusButton_Previews: PreviewProvider {
    static var previews: some View {
        PlusMinusButton(
            store: Store(
                initialState: AddToCartDomain.State(),
                reducer: { AddToCartDomain() }
            )
        )
    }
}


================================================
FILE: OnlineStoreTCA/Products/AddToCart/Test/AddToCartDomainTest.swift
================================================
//
//  AddToCartDomainTest.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 26/08/22.
//

import ComposableArchitecture
import XCTest

@testable import OnlineStoreTCA

@MainActor
class AddToCartDomainTest: XCTestCase {
    
    func testIncreaseCounterTappingPlusButtonOnce() async {
        let store = TestStore(
            initialState: AddToCartDomain.State(),
            reducer: { AddToCartDomain() }
        )
        
        
        await store.send(\.didTapPlusButton) {
            $0.count = 1
        }
    }
    
    func testIncreaseCounterTappingPlusButtonThreeTimes() async {
        let store = TestStore(
            initialState: AddToCartDomain.State(),
            reducer: { AddToCartDomain() }
        )
        
        
        await store.send(\.didTapPlusButton) { $0.count = 1 }
        await store.send(\.didTapPlusButton) { $0.count = 2 }
        await store.send(\.didTapPlusButton) { $0.count = 3 }
    }
    
    func testDecreaseCounterTappingPlusButtonOnce() async {
        let store = TestStore(
            initialState: AddToCartDomain.State(),
            reducer: { AddToCartDomain() }
        )
        
        await store.send(\.didTapMinusButton) {
            $0.count = -1
        }
    }
    
    func testDecreaseCounterTappingPlusButtonThreeTimes() async {
        let store = TestStore(
            initialState: AddToCartDomain.State(),
            reducer: { AddToCartDomain() }
        )
        
        await store.send(\.didTapMinusButton) { $0.count = -1 }
        await store.send(\.didTapMinusButton) { $0.count = -2 }
        await store.send(\.didTapMinusButton) { $0.count = -3 }
    }
    
    func testUpdatingCounterTappingPlusAndMinusButtons() async {
        let store = TestStore(
            initialState: AddToCartDomain.State(),
            reducer: { AddToCartDomain() }
        )
        
        await store.send(\.didTapMinusButton) { $0.count = -1 }
        await store.send(\.didTapPlusButton) { $0.count = 0 }
        await store.send(\.didTapMinusButton) { $0.count = -1 }
    }
}


================================================
FILE: OnlineStoreTCA/Products/Product/Product.swift
================================================
//
//  Product.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 17/08/22.
//

import Foundation

struct Product: Equatable, Identifiable {
    let id: Int
    let title: String
    let price: Double // Update to Currency
    let description: String
    let category: String // Update to enum
    let imageString: String
    
    // Add rating later...
}
extension Product: Decodable {
    enum ProductKeys: String, CodingKey {
        case id
        case title
        case price
        case description
        case category
        case image
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: ProductKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.title = try container.decode(String.self, forKey: .title)
        self.price = try container.decode(Double.self, forKey: .price)
        self.description = try container.decode(String.self, forKey: .description)
        self.category = try container.decode(String.self, forKey: .category)
        self.imageString = try container.decode(String.self, forKey: .image)
    }
}

extension Product {
    static var sample: [Product] {
        [
            .init(
                id: 1,
                title: "Mens Casual Premium Slim Fit T-Shirts",
                price: 22.3,
                description: "Slim-fitting style, contrast raglan long sleeve, three-button henley placket, light weight & soft fabric for breathable and comfortable wearing. And Solid stitched shirts with round neck made for durability and a great fit for casual fashion wear and diehard baseball fans. The Henley style round neckline includes a three-button placket.",
                category: "men's clothing",
                imageString: "https://fakestoreapi.com/img/71-3HjGNDUL._AC_SY879._SX._UX._SY._UY_.jpg"
            ),
            .init(
                id: 2,
                title: "Fjallraven - Foldsack No. 1 Backpack, Fits 15 Laptops",
                price: 109.95,
                description: "Your perfect pack for everyday use and walks in the forest. Stash your laptop (up to 15 inches) in the padded sleeve, your everyday",
                category: "men's clothing",
                imageString: "https://fakestoreapi.com/img/81fPKd-2AYL._AC_SL1500_.jpg"
            ),
            .init(
                id: 3,
                title: "Mens Cotton Jacket",
                price: 55.99,
                description: "Great outerwear jackets for Spring/Autumn/Winter, suitable for many occasions, such as working, hiking, camping, mountain/rock climbing, cycling, traveling or other outdoors. Good gift choice for you or your family member. A warm hearted love to Father, husband or son in this thanksgiving or Christmas Day.",
                category: "men's clothing",
                imageString: "https://fakestoreapi.com/img/71li-ujtlUL._AC_UX679_.jpg"
            )
        ]
    }
}


================================================
FILE: OnlineStoreTCA/Products/Product/ProductCell.swift
================================================
//
//  ProductCell.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 20/08/22.
//

import SwiftUI
import ComposableArchitecture

struct ProductCell: View {
    let store: StoreOf<ProductDomain>
    
    var body: some View {
        WithPerceptionTracking {
            VStack {
                AsyncImage(
                    url: URL(string: store.product.imageString)
                ) { image in
                    image
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(height: 300)
                } placeholder: {
                    ProgressView()
                        .frame(height: 300)
                }
                
                VStack(alignment: .leading) {
                    Text(store.product.title)
                    HStack {
                        Text("$\(store.product.price.description)")
                            .font(.custom("AmericanTypewriter", size: 25))
                            .fontWeight(.bold)
                        Spacer()
                        AddToCartButton(
                            store: self.store.scope(
                                state: \.addToCartState,
                                action: \.addToCart
                            )
                        )
                    }
                }
                .font(.custom("AmericanTypewriter", size: 20))
            }
            .padding(20)
        }
    }
}

struct ProductCell_Previews: PreviewProvider {
    static var previews: some View {
        ProductCell(
            store: Store(
                initialState: ProductDomain.State(
                    id: UUID(),
                    product: Product.sample[0]
                ),
                reducer: { ProductDomain() }
            )
        )
        .previewLayout(.fixed(width: 300, height: 300))
    }
}


================================================
FILE: OnlineStoreTCA/Products/Product/ProductDomain.swift
================================================
//
//  ProductDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 21/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct ProductDomain {
    @ObservableState
    struct State: Equatable, Identifiable {
        let id: UUID
        let product: Product
        var addToCartState = AddToCartDomain.State()
        
        var count: Int {
            get { addToCartState.count }
            set { addToCartState.count = newValue }
        }
    }
    
    enum Action: Equatable {
        case addToCart(AddToCartDomain.Action)
    }
    
    var body: some ReducerOf<Self> {
        Scope(state: \.addToCartState, action: \.addToCart) {
            AddToCartDomain()
        }
        Reduce { state, action in
            switch action {
                case .addToCart(.didTapPlusButton):
                    return .none
                case .addToCart(.didTapMinusButton):
                    state.addToCartState.count = max(0, state.addToCartState.count)
                    return .none
            }
        }
    }
}


================================================
FILE: OnlineStoreTCA/Products/Product/Test/ProductDomainTest.swift
================================================
//
//  ProductDomainTest.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 27/08/22.
//

import ComposableArchitecture
import XCTest

@testable import OnlineStoreTCA

@MainActor
class ProductDomainTest: XCTestCase {
    func testIncreaseProductCounterTappingPlusButtonOnce() async {
        let product = Product(
            id: 1,
            title: "ProductDemo",
            price: 10.5,
            description: "Hi mom!",
            category: "Category1",
            imageString: "image"
        )
        let store = TestStore(
            initialState: ProductDomain.State(
                id: UUID(),
                product: product
            ),
            reducer: { ProductDomain() }
        )
        
        await store.send(\.addToCart.didTapPlusButton) {
            $0.addToCartState = AddToCartDomain.State(count: 1)
        }
    }
    
    func testIncreaseProductCounterTappingPlusButtonThreeTimes() async {
        let product = Product(
            id: 1,
            title: "ProductDemo",
            price: 10.5,
            description: "Hi mom!",
            category: "Category1",
            imageString: "image"
        )
        let store = TestStore(
            initialState: ProductDomain.State(
                id: UUID(),
                product: product
            ),
            reducer: { ProductDomain() }
        )
        
        await store.send(\.addToCart.didTapPlusButton) {
            $0.addToCartState = AddToCartDomain.State(count: 1)
        }
        
        await store.send(\.addToCart.didTapPlusButton) {
            $0.addToCartState = AddToCartDomain.State(count: 2)
        }
        
        await store.send(\.addToCart.didTapPlusButton) {
            $0.addToCartState = AddToCartDomain.State(count: 3)
        }
    }
    
    
    func testIncreaseProductCounterTappingMinusButtonOnce() async {
        let product = Product(
            id: 1,
            title: "ProductDemo",
            price: 10.5,
            description: "Hi mom!",
            category: "Category1",
            imageString: "image"
        )
        let store = TestStore(
            initialState: ProductDomain.State(
                id: UUID(),
                product: product
            ),
            reducer: { ProductDomain() }
        )
        
        await store.send(\.addToCart.didTapMinusButton)
    }
    
    func testIncreaseProductCounterTappingMinusButtonThreeTimes() async {
        let product = Product(
            id: 1,
            title: "ProductDemo",
            price: 10.5,
            description: "Hi mom!",
            category: "Category1",
            imageString: "image"
        )
        let store = TestStore(
            initialState: ProductDomain.State(
                id: UUID(),
                product: product
            ),
            reducer: { ProductDomain() }
        )
        
        // No changes expected!
        await store.send(\.addToCart.didTapMinusButton)
        await store.send(\.addToCart.didTapMinusButton)
        await store.send(\.addToCart.didTapMinusButton)
    }
    
    func testIncreaseProductCounterTappingMinusTwoTimesAndPlusOnce() async {
        let product = Product(
            id: 1,
            title: "ProductDemo",
            price: 10.5,
            description: "Hi mom!",
            category: "Category1",
            imageString: "image"
        )
        let store = TestStore(
            initialState: ProductDomain.State(
                id: UUID(),
                product: product
            ),
            reducer: { ProductDomain() }
        )
        
        // No changes expected!
        await store.send(\.addToCart.didTapMinusButton)
        await store.send(\.addToCart.didTapMinusButton)
        
        // Change expected!
        await store.send(\.addToCart.didTapPlusButton) {
            $0.addToCartState = AddToCartDomain.State(count: 1)
        }
    }
    
}


================================================
FILE: OnlineStoreTCA/Products/ProductList/ErrorView.swift
================================================
//
//  ErrorView.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 25/08/22.
//

import SwiftUI

struct ErrorView: View {
    let message: String
    let retryAction: () -> Void
    
    var body: some View {
        VStack {
            Text(":(")
                .font(.custom("AmericanTypewriter", size: 50))
            Text("")
            Text(message)
                .font(.custom("AmericanTypewriter", size: 25))
            Button {
                retryAction()
            } label: {
                Text("Retry")
                    .font(.custom("AmericanTypewriter", size: 25))
                    .foregroundColor(.white)
            }
            .frame(width: 100, height: 60)
            .background(.blue)
            .cornerRadius(10)
            .padding()
            
        }
    }
}

struct ErrorView_Previews: PreviewProvider {
    static var previews: some View {
        ErrorView(
            message: "Oops, we couldn't fetch product list",
            retryAction: {}
        )
        
    }
}


================================================
FILE: OnlineStoreTCA/Products/ProductList/ProductListDomain.swift
================================================
//
//  ProductListDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 17/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct ProductListDomain {
    @ObservableState
    struct State: Equatable {
        var dataLoadingStatus = DataLoadingStatus.notStarted
        @Presents var cartState: CartListDomain.State?
        var productList: IdentifiedArrayOf<ProductDomain.State> = []
        
        var shouldShowError: Bool {
            dataLoadingStatus == .error
        }
        
        var isLoading: Bool {
            dataLoadingStatus == .loading
        }
    }
    
    enum Action: Equatable {
        case fetchProducts
        case fetchProductsResponse(TaskResult<[Product]>)
        case setCartView(isPresented: Bool)
        case cart(PresentationAction<CartListDomain.Action>)
        case product(IdentifiedActionOf<ProductDomain>)
        case resetProduct(product: Product)
        case closeCart
    }
    
    @Dependency(\.apiClient.fetchProducts) var fetchProducts
    @Dependency(\.uuid) var uuid
	
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                case .fetchProducts:
                    if state.dataLoadingStatus == .success || state.dataLoadingStatus == .loading {
                        return .none
                    }
                    
                    state.dataLoadingStatus = .loading
                    return .run { send in
                        await send(
                            .fetchProductsResponse(
                                TaskResult { try await self.fetchProducts() }
                            )
                        )
                    }
                case .fetchProductsResponse(.success(let products)):
                    state.dataLoadingStatus = .success
                    state.productList = IdentifiedArrayOf(
                        uniqueElements: products.map {
                            ProductDomain.State(
                                id: uuid(),
                                product: $0
                            )
                        }
                    )
                    return .none
                case .fetchProductsResponse(.failure(let error)):
                    state.dataLoadingStatus = .error
                    print(error)
                    print("Error getting products, try again later.")
                    return .none
				case .cart(.presented(let action)):
                    switch action {
                        case .didPressCloseButton:
                            return closeCart(state: &state)
                        case .alert(.presented(.dismissSuccessAlert)):
							resetProductsToZero(state: &state)
							
							return .run { send in
								await send(.closeCart)
							}
                        case .cartItem(.element(id: _, action: let action)):
                            switch action {
                                case .deleteCartItem(let product):
                                    return .send(.resetProduct(product: product))
                            }
                        default:
                            return .none
                    }
				case .cart(.dismiss):
					return .none
                case .closeCart:
                    return closeCart(state: &state)
                case .resetProduct(let product):
                    
                    guard let index = state.productList.firstIndex(
                        where: { $0.product.id == product.id }
                    )
                    else { return .none }
                    let productStateId = state.productList[index].id
                    
                    state.productList[id: productStateId]?.addToCartState.count = 0
                    return .none
                case .setCartView(let isPresented):
                    state.cartState = isPresented
                    ? CartListDomain.State(
                        cartItems: IdentifiedArrayOf(
                            uniqueElements: state
                                .productList
                                .compactMap { state in
                                    state.count > 0
                                    ? CartItemDomain.State(
                                        id: uuid(),
                                        cartItem: CartItem(
                                            product: state.product,
                                            quantity: state.count
                                        )
                                    )
                                    : nil
                                }
                        )
                    )
                    : nil
                    return .none
                case .product:
                    return .none
            }
        }
        .forEach(\.productList, action: \.product) {
            ProductDomain()
        }
        .ifLet(\.$cartState, action: \.cart) {
            CartListDomain()
        }
    }
    
    private func closeCart(
        state: inout State
    ) -> Effect<Action> {
        state.cartState = nil
        
        return .none
    }
    
    private func resetProductsToZero(
        state: inout State
    ) {
        for id in state.productList.map(\.id) {
            state.productList[id: id]?.count = 0
        }
    }
}


================================================
FILE: OnlineStoreTCA/Products/ProductList/ProductListView.swift
================================================
//
//  ProductListView.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 17/08/22.
//

import SwiftUI
import ComposableArchitecture

struct ProductListView: View {
    @Perception.Bindable var store: StoreOf<ProductListDomain>
    
    var body: some View {
        WithPerceptionTracking {
            NavigationView {
                Group {
                    if store.isLoading {
                        ProgressView()
                            .frame(width: 100, height: 100)
                    } else if store.shouldShowError {
                        ErrorView(
                            message: "Oops, we couldn't fetch product list",
                            retryAction: { store.send(.fetchProducts) }
                        )
                        
                    } else {
                        List {
                            ForEach(
                                store.scope(
                                    state: \.productList,
                                    action: \.product
                                ),
                                id: \.id
                            ) { store in
                                WithPerceptionTracking {
                                    ProductCell(store: store)
                                }
                            }
                        }
                    }
                }
                .task {
                    store.send(.fetchProducts)
                }
                .navigationTitle("Products")
                .toolbar {
                    ToolbarItem(placement: .navigationBarTrailing) {
                        Button {
                            store.send(.setCartView(isPresented: true))
                        } label: {
                            Text("Go to Cart")
                        }
                    }
                }
				.sheet(
					item: $store.scope(
						state: \.cartState,
						action: \.cart
					)
				) { store in
					CartListView(store: store)
				}
			}
        }
    }
}

struct ProductListView_Previews: PreviewProvider {
    static var previews: some View {
        ProductListView(
            store: Store(
                initialState: ProductListDomain.State()
            ) {
                ProductListDomain()
            } withDependencies: {
                $0.apiClient.fetchProducts = { Product.sample }
                $0.apiClient.sendOrder = { _ in "OK" }
            }
        )
    }
}


================================================
FILE: OnlineStoreTCA/Products/ProductList/Test/ProductListDomainTest.swift
================================================
//
//  ProductListDomainTest.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 27/08/22.
//

import ComposableArchitecture
import XCTest

@testable import OnlineStoreTCA

@MainActor
class ProductListDomainTest: XCTestCase {
    
    override func setUp() async throws {
        UUID.uuIdTestCounter = 0
    }
    
    func testFetchProductsSuccess() async {
        let products: [Product] = [
            .init(
                id: 1,
                title: "ProductDemo",
                price: 10.5,
                description: "Hi mom!",
                category: "Category1",
                imageString: "image"
            ),
            .init(
                id: 2,
                title: "AnotherProduct",
                price: 99.99,
                description: "Hi Dad!",
                category: "Category2",
                imageString: "image2"
            ),
        ]
        
        let store = TestStore(
            initialState: ProductListDomain.State(),
            reducer: { ProductListDomain() }
        ) {
            $0.apiClient.fetchProducts = { products }
            $0.uuid = .incrementing
        }
        
        let productId1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let productId2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        
        let identifiedArray = IdentifiedArrayOf(
            uniqueElements: [
                ProductDomain.State(
                    id: productId1,
                    product: products[0]
                ),
                ProductDomain.State(
                    id: productId2,
                    product: products[1]
                ),
            ]
        )
        
        await store.send(\.fetchProducts) {
            $0.dataLoadingStatus = .loading
        }
        
        await store.receive(\.fetchProductsResponse, .success(products)) {
            $0.productList = identifiedArray
            $0.dataLoadingStatus = .success
        }
    }
    
    func testFetchProductsFailure() async {
        let error = APIClient.Failure()
        let store = TestStore(
            initialState: ProductListDomain.State(),
            reducer: { ProductListDomain() }
        ) {
            $0.apiClient.fetchProducts = { throw error }
            $0.uuid = .incrementing
        }
        
        await store.send(\.fetchProducts) {
            $0.dataLoadingStatus = .loading
        }
        
        await store.receive(\.fetchProductsResponse, .failure(error)) {
            $0.productList = []
            $0.dataLoadingStatus = .error
        }
    }
    
    func testResetProductsToZeroAfterPayingOrder() async {
        let products: [Product] = [
            .init(
                id: 1,
                title: "ProductDemo",
                price: 10.5,
                description: "Hi mom!",
                category: "Category1",
                imageString: "image"
            ),
            .init(
                id: 2,
                title: "AnotherProduct",
                price: 99.99,
                description: "Hi Dad!",
                category: "Category2",
                imageString: "image2"
            ),
        ]
        
        let id1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let id2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        
        let identifiedProducts = IdentifiedArrayOf(
            uniqueElements: [
                ProductDomain.State(
                    id: id1,
                    product: products[0]
                ),
                ProductDomain.State(
                    id: id2,
                    product: products[1]
                ),
            ]
        )
        
        let store = TestStore(
            initialState: ProductListDomain.State(
                productList: identifiedProducts
            ),
            reducer: { ProductListDomain() }
        ) {
            $0.uuid = .incrementing
        }
        
		await store.send(\.product[id: id1].addToCart.didTapPlusButton) {
            $0.productList[id: id1]?.addToCartState.count = 1
        }
        
		await store.send(\.product[id: id1].addToCart.didTapPlusButton) {
            $0.productList[id: id1]?.addToCartState.count = 2
        }
        
        let expectedCartState = CartListDomain.State(
            cartItems: IdentifiedArrayOf(
                uniqueElements: [
                    CartItemDomain.State(
                        id: id1,
                        cartItem: CartItem(
                            product: products.first!,
                            quantity: 2
                        )
                    )
                ]
            )
        )
        
        await store.send(\.setCartView, true) {
            $0.cartState = expectedCartState
        }
		
		await store.send(\.cart.didPressPayButton) {
			$0.cartState?.alert = .confirmationAlert(totalPriceString: "$0.0")
		}
        
		await store.send(\.cart.alert.dismissSuccessAlert) {
            $0.productList[id: id1]?.addToCartState.count = 0
			$0.cartState?.alert = nil
        }
        
        await store.receive(\.closeCart) {
            $0.cartState = nil
        }
    }
    
    func testItemRemovedFromCart() async {
        let products: [Product] = [
            .init(
                id: 1,
                title: "ProductDemo",
                price: 10.5,
                description: "Hi mom!",
                category: "Category1",
                imageString: "image"
            ),
            .init(
                id: 2,
                title: "AnotherProduct",
                price: 99.99,
                description: "Hi Dad!",
                category: "Category2",
                imageString: "image2"
            ),
        ]
        
        let id1 = UUID(uuidString: "00000000-0000-0000-0000-000000000000")!
        let id2 = UUID(uuidString: "00000000-0000-0000-0000-000000000001")!
        let numberOfItems = 2
        
        let identifiedProducts = IdentifiedArrayOf(
            uniqueElements: [
                ProductDomain.State(
                    id: id1,
                    product: products[0],
                    addToCartState: AddToCartDomain.State(count: numberOfItems)
                ),
                // This item should not be added:
                ProductDomain.State(
                    id: id2,
                    product: products[1],
                    addToCartState: AddToCartDomain.State(count: 0)
                ),
            ]
        )
        
        let store = TestStore(
            initialState: ProductListDomain.State(
                productList: identifiedProducts
            ),
            reducer: { ProductListDomain() }
        ) {
            $0.uuid = .incrementing
        }
        
        let expectedCartState = CartListDomain.State(
            cartItems: IdentifiedArrayOf(
                uniqueElements: [
                    CartItemDomain.State(
                        id: id1,
                        cartItem: CartItem(
                            product: products.first!,
                            quantity: numberOfItems
                        )
                    )
                ]
            )
        )
        
        await store.send(\.setCartView, true) {
            $0.cartState = expectedCartState
        }
		await store.send(
			.cart(
				.presented(
					.cartItem(
						.element(id: id1, action: .deleteCartItem(product: products[0]))
					)
				)
			)
		) {
			$0.cartState?.cartItems = []
		}
		
		await store.receive(\.cart.getTotalPrice) {
            $0.cartState?.totalPrice = 0
            $0.cartState?.isPayButtonDisable = true
        }
        await store.receive(\.resetProduct, products[0]) {
            $0.productList = identifiedProducts
            $0.productList[id: id1]?.count = 0
        }
    }
}


extension UUID {
    // uuIdTestCounter needs to be set to 0 on setUp() method
    static var uuIdTestCounter: UInt = 0
    
    static var newUUIDForTest: UUID {
        defer {
            uuIdTestCounter += 1
        }
        return UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", uuIdTestCounter))")!
    }
}


================================================
FILE: OnlineStoreTCA/Profile/ProfileDomain.swift
================================================
//
//  ProfileDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 25/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct ProfileDomain {
    @ObservableState
    struct State: Equatable {
        var profile: UserProfile = .default
        fileprivate var dataState = DataState.notStarted
        var isLoading: Bool {
            dataState == .loading
        }
    }
    
    fileprivate enum DataState {
        case notStarted
        case loading
        case complete
    }
    
    enum Action: Equatable {
        case fetchUserProfile
        case fetchUserProfileResponse(TaskResult<UserProfile>)
    }
    
    @Dependency(\.apiClient.fetchUserProfile) var fetchUserProfile
    
    func reduce(into state: inout State, action: Action) -> Effect<Action> {
        switch action {
            case .fetchUserProfile:
                if state.dataState == .complete || state.dataState == .loading {
                    return .none
                }
                
                state.dataState = .loading
                return .run { send in
                    await send(
                        .fetchUserProfileResponse(
                            TaskResult { try await self.fetchUserProfile() }
                        )
                    )
                }
            case .fetchUserProfileResponse(.success(let profile)):
                state.dataState = .complete
                state.profile = profile
                return .none
            case .fetchUserProfileResponse(.failure(let error)):
                state.dataState = .complete
                print("Error: \(error)")
                return .none
        }
    }
}


================================================
FILE: OnlineStoreTCA/Profile/ProfileView.swift
================================================
//
//  ProfileView.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 25/08/22.
//

import SwiftUI
import ComposableArchitecture

struct ProfileView: View {
    let store: StoreOf<ProfileDomain>
    
    var body: some View {
        WithPerceptionTracking {
            NavigationView {
                ZStack {
                    Form {
                        Section {
                            Text(store.profile.firstName.capitalized)
                            +
                            Text(" \(store.profile.lastName.capitalized)")
                        } header: {
                            Text("Full name")
                        }
                        
                        Section {
                            Text(store.profile.email)
                        } header: {
                            Text("Email")
                        }
                    }
                    
                    if store.isLoading {
                        ProgressView()
                    }
                }
                .task {
                    store.send(.fetchUserProfile)
                }
                .navigationTitle("Profile")
            }
        }
    }
}

struct ProfileView_Previews: PreviewProvider {
    static var previews: some View {
        ProfileView(
            store: Store(initialState: ProfileDomain.State()) {
                ProfileDomain()
            } withDependencies: {
                $0.apiClient.fetchUserProfile = { .sample }
            }
        )
    }
}


================================================
FILE: OnlineStoreTCA/Profile/UserProfile.swift
================================================
//
//  UserProfile.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 25/08/22.
//

import Foundation

struct UserProfile: Equatable {
    let id: Int
    let email: String
    let firstName: String
    let lastName: String
}

extension UserProfile: Decodable {
    private enum ProfileKeys: String, CodingKey {
        case id
        case email
        case name
        case firstname
        case lastname
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: ProfileKeys.self)
        self.id = try container.decode(Int.self, forKey: .id)
        self.email = try container.decode(String.self, forKey: .email)
        
        let nameContainer = try container.nestedContainer(keyedBy: ProfileKeys.self, forKey: .name)
        self.firstName = try nameContainer.decode(String.self, forKey: .firstname)
        self.lastName = try nameContainer.decode(String.self, forKey: .lastname)
    }
}

extension UserProfile {
    static var sample: UserProfile {
        .init(
            id: 1,
            email: "hello@demo.com",
            firstName: "Pedro",
            lastName: "Rojas"
        )
    }
    
    static var `default`: UserProfile {
        .init(
            id: 0,
            email: "",
            firstName: "",
            lastName: ""
        )
    }
}


================================================
FILE: OnlineStoreTCA/Root/OnlineStoreTCAApp.swift
================================================
//
//  OnlineStoreTCAApp.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 04/08/22.
//

import SwiftUI
import ComposableArchitecture

@main
struct OnlineStoreTCAApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(
                store: Store(
                    initialState: RootDomain.State(),
					reducer: { RootDomain() }
                )
            )
        }
    }
}


================================================
FILE: OnlineStoreTCA/Root/RootDomain.swift
================================================
//
//  RootDomain.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 24/08/22.
//

import Foundation
import ComposableArchitecture

@Reducer
struct RootDomain {
    @ObservableState
    struct State: Equatable {
        var selectedTab = Tab.products
        var productListState = ProductListDomain.State()
        var profileState = ProfileDomain.State()
    }
    
    enum Tab {
        case products
        case profile
    }
    
    enum Action: Equatable {
        case tabSelected(Tab)
        case productList(ProductListDomain.Action)
        case profile(ProfileDomain.Action)
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                case .productList:
                    return .none
                case .tabSelected(let tab):
                    state.selectedTab = tab
                    return .none
                case .profile:
                    return .none
            }
        }
        Scope(state: \.productListState, action: \.productList) {
            ProductListDomain()
        }
        Scope(state:  \.profileState, action: \.profile) {
            ProfileDomain()
        }
    }
}


================================================
FILE: OnlineStoreTCA/Root/RootView.swift
================================================
//
//  RootView.swift
//  OnlineStoreTCA
//
//  Created by Pedro Rojas on 24/08/22.
//

import SwiftUI
import ComposableArchitecture

struct RootView: View {
    @Perception.Bindable var store: StoreOf<RootDomain>
    
    var body: some View {
        WithPerceptionTracking {
            TabView(
                selection: $store.selectedTab.sending(\.tabSelected)
            ) {
                ProductListView(
                    store: self.store.scope(
                        state: \.productListState,
                        action: \.productList
                    )
                )
                .tabItem {
                    Image(systemName: "list.bullet")
                    Text("Products")
                }
                .tag(RootDomain.Tab.products)
                ProfileView(
                    store: self.store.scope(
                        state: \.profileState,
                        action: \.profile
                    )
                )
                .tabItem {
                    Image(systemName: "person.fill")
                    Text("Profile")
                }
                .tag(RootDomain.Tab.profile)
            }
        }
    }
}

struct RootView_Previews: PreviewProvider {
    static var previews: some View {
        RootView(
            store: Store(
                initialState: RootDomain.State()
            ) {
                RootDomain()
            } withDependencies: {
                $0.apiClient.fetchProducts = { Product.sample }
                $0.apiClient.sendOrder = { _ in "OK" }
                $0.apiClient.fetchUserProfile = { UserProfile.sample }
                $0.uuid = .incrementing
            }
        )
    }
}


================================================
FILE: OnlineStoreTCA.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 55;
	objects = {

/* Begin PBXBuildFile section */
		B50F8EFF28B18F020034B039 /* ProductCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8EFE28B18F020034B039 /* ProductCell.swift */; };
		B50F8F0128B19B880034B039 /* AddToCartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F0028B19B880034B039 /* AddToCartButton.swift */; };
		B50F8F0428B19CD30034B039 /* PlusMinusButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F0328B19CD30034B039 /* PlusMinusButton.swift */; };
		B50F8F0628B19E660034B039 /* AddToCartDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F0528B19E660034B039 /* AddToCartDomain.swift */; };
		B50F8F0C28B2C1A70034B039 /* ProductDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F0B28B2C1A70034B039 /* ProductDomain.swift */; };
		B50F8F1028B3D7FB0034B039 /* CartCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F0F28B3D7FB0034B039 /* CartCell.swift */; };
		B50F8F1328B482E80034B039 /* CartItemDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50F8F1228B482E80034B039 /* CartItemDomain.swift */; };
		B51F8DF728BF2FF20047E7D9 /* CartListDomainTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51F8DF628BF2FF20047E7D9 /* CartListDomainTest.swift */; };
		B525334728AD3F6000B29A17 /* ProductListDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525334628AD3F6000B29A17 /* ProductListDomain.swift */; };
		B525334C28ADCBCE00B29A17 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525334B28ADCBCE00B29A17 /* Product.swift */; };
		B525334E28ADD6DE00B29A17 /* ProductListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525334D28ADD6DE00B29A17 /* ProductListView.swift */; };
		B525335128AE87C700B29A17 /* CartListDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525335028AE87C700B29A17 /* CartListDomain.swift */; };
		B525335328AE883B00B29A17 /* CartItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525335228AE883B00B29A17 /* CartItem.swift */; };
		B525335528AE8C5600B29A17 /* CartListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B525335428AE8C5600B29A17 /* CartListView.swift */; };
		B5389D1528B9AAA4002BF611 /* OnlineStoreTCATests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5389D1428B9AAA4002BF611 /* OnlineStoreTCATests.swift */; };
		B5389D1B28B9AAC6002BF611 /* AddToCartDomainTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E18428B919FB00CB51F4 /* AddToCartDomainTest.swift */; };
		B556E16428B5C44000CB51F4 /* APIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E16328B5C44000CB51F4 /* APIClient.swift */; };
		B556E16B28B5CEA900CB51F4 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = B556E16A28B5CEA900CB51F4 /* ComposableArchitecture */; };
		B556E17928B671AC00CB51F4 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E17828B671AC00CB51F4 /* RootView.swift */; };
		B556E17B28B6740A00CB51F4 /* RootDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E17A28B6740A00CB51F4 /* RootDomain.swift */; };
		B556E17D28B7BE1600CB51F4 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E17C28B7BE1600CB51F4 /* ProfileView.swift */; };
		B556E17F28B7BE2D00CB51F4 /* ProfileDomain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E17E28B7BE2D00CB51F4 /* ProfileDomain.swift */; };
		B556E18128B7BEC200CB51F4 /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E18028B7BEC200CB51F4 /* UserProfile.swift */; };
		B556E18328B84F1500CB51F4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B556E18228B84F1500CB51F4 /* ErrorView.swift */; };
		B5A5CFB5289C257900AC3E5E /* OnlineStoreTCAApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A5CFB4289C257900AC3E5E /* OnlineStoreTCAApp.swift */; };
		B5A5CFB9289C257B00AC3E5E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A5CFB8289C257B00AC3E5E /* Assets.xcassets */; };
		B5A5CFBC289C257B00AC3E5E /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B5A5CFBB289C257B00AC3E5E /* Preview Assets.xcassets */; };
		B5BE68DB28BA6EA8008FC353 /* ProductDomainTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BE68D928BA6EA4008FC353 /* ProductDomainTest.swift */; };
		B5BE68DF28BB23B1008FC353 /* ProductListDomainTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BE68DD28BB23AC008FC353 /* ProductListDomainTest.swift */; };
		B5BE68E128BBAE93008FC353 /* DataLoadingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BE68E028BBAE93008FC353 /* DataLoadingStatus.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
		B5389D1628B9AAA4002BF611 /* PBXContainerItemProxy */ = {
			isa = PBXContainerItemProxy;
			containerPortal = B5A5CFA9289C257900AC3E5E /* Project object */;
			proxyType = 1;
			remoteGlobalIDString = B5A5CFB0289C257900AC3E5E;
			remoteInfo = OnlineStoreTCA;
		};
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
		B50F8EFE28B18F020034B039 /* ProductCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCell.swift; sourceTree = "<group>"; };
		B50F8F0028B19B880034B039 /* AddToCartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToCartButton.swift; sourceTree = "<group>"; };
		B50F8F0328B19CD30034B039 /* PlusMinusButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlusMinusButton.swift; sourceTree = "<group>"; };
		B50F8F0528B19E660034B039 /* AddToCartDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToCartDomain.swift; sourceTree = "<group>"; };
		B50F8F0B28B2C1A70034B039 /* ProductDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDomain.swift; sourceTree = "<group>"; };
		B50F8F0F28B3D7FB0034B039 /* CartCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartCell.swift; sourceTree = "<group>"; };
		B50F8F1228B482E80034B039 /* CartItemDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItemDomain.swift; sourceTree = "<group>"; };
		B51F8DF628BF2FF20047E7D9 /* CartListDomainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartListDomainTest.swift; sourceTree = "<group>"; };
		B525334628AD3F6000B29A17 /* ProductListDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListDomain.swift; sourceTree = "<group>"; };
		B525334B28ADCBCE00B29A17 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = "<group>"; };
		B525334D28ADD6DE00B29A17 /* ProductListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListView.swift; sourceTree = "<group>"; };
		B525335028AE87C700B29A17 /* CartListDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartListDomain.swift; sourceTree = "<group>"; };
		B525335228AE883B00B29A17 /* CartItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItem.swift; sourceTree = "<group>"; };
		B525335428AE8C5600B29A17 /* CartListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartListView.swift; sourceTree = "<group>"; };
		B5389D1228B9AAA3002BF611 /* OnlineStoreTCATests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OnlineStoreTCATests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
		B5389D1428B9AAA4002BF611 /* OnlineStoreTCATests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineStoreTCATests.swift; sourceTree = "<group>"; };
		B556E16328B5C44000CB51F4 /* APIClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIClient.swift; sourceTree = "<group>"; };
		B556E17828B671AC00CB51F4 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
		B556E17A28B6740A00CB51F4 /* RootDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootDomain.swift; sourceTree = "<group>"; };
		B556E17C28B7BE1600CB51F4 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
		B556E17E28B7BE2D00CB51F4 /* ProfileDomain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDomain.swift; sourceTree = "<group>"; };
		B556E18028B7BEC200CB51F4 /* UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfile.swift; sourceTree = "<group>"; };
		B556E18228B84F1500CB51F4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
		B556E18428B919FB00CB51F4 /* AddToCartDomainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToCartDomainTest.swift; sourceTree = "<group>"; };
		B5A5CFB1289C257900AC3E5E /* OnlineStoreTCA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OnlineStoreTCA.app; sourceTree = BUILT_PRODUCTS_DIR; };
		B5A5CFB4289C257900AC3E5E /* OnlineStoreTCAApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlineStoreTCAApp.swift; sourceTree = "<group>"; };
		B5A5CFB8289C257B00AC3E5E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		B5A5CFBB289C257B00AC3E5E /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
		B5BE68D928BA6EA4008FC353 /* ProductDomainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDomainTest.swift; sourceTree = "<group>"; };
		B5BE68DD28BB23AC008FC353 /* ProductListDomainTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductListDomainTest.swift; sourceTree = "<group>"; };
		B5BE68E028BBAE93008FC353 /* DataLoadingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataLoadingStatus.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
		B5389D0F28B9AAA3002BF611 /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
		B5A5CFAE289C257900AC3E5E /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				B556E16B28B5CEA900CB51F4 /* ComposableArchitecture in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		B50F8F0228B19CBD0034B039 /* AddToCart */ = {
			isa = PBXGroup;
			children = (
				B5BE68D728BA6E63008FC353 /* Test */,
				B50F8F0028B19B880034B039 /* AddToCartButton.swift */,
				B50F8F0328B19CD30034B039 /* PlusMinusButton.swift */,
				B50F8F0528B19E660034B039 /* AddToCartDomain.swift */,
			);
			path = AddToCart;
			sourceTree = "<group>";
		};
		B50F8F0D28B2C1C90034B039 /* ProductList */ = {
			isa = PBXGroup;
			children = (
				B5BE68DC28BB2399008FC353 /* Test */,
				B525334628AD3F6000B29A17 /* ProductListDomain.swift */,
				B525334D28ADD6DE00B29A17 /* ProductListView.swift */,
				B556E18228B84F1500CB51F4 /* ErrorView.swift */,
			);
			path = ProductList;
			sourceTree = "<group>";
		};
		B50F8F0E28B2C1D40034B039 /* Product */ = {
			isa = PBXGroup;
			children = (
				B5BE68D828BA6E94008FC353 /* Test */,
				B525334B28ADCBCE00B29A17 /* Product.swift */,
				B50F8EFE28B18F020034B039 /* ProductCell.swift */,
				B50F8F0B28B2C1A70034B039 /* ProductDomain.swift */,
			);
			path = Product;
			sourceTree = "<group>";
		};
		B50F8F1128B4814E0034B039 /* Cart */ = {
			isa = PBXGroup;
			children = (
				B525335228AE883B00B29A17 /* CartItem.swift */,
				B50F8F0F28B3D7FB0034B039 /* CartCell.swift */,
				B50F8F1228B482E80034B039 /* CartItemDomain.swift */,
			);
			path = Cart;
			sourceTree = "<group>";
		};
		B51F8DF528BF2F7C0047E7D9 /* Test */ = {
			isa = PBXGroup;
			children = (
				B51F8DF628BF2FF20047E7D9 /* CartListDomainTest.swift */,
			);
			path = Test;
			sourceTree = "<group>";
		};
		B525334328AD3DCE00B29A17 /* Products */ = {
			isa = PBXGroup;
			children = (
				B50F8F0E28B2C1D40034B039 /* Product */,
				B50F8F0D28B2C1C90034B039 /* ProductList */,
				B50F8F0228B19CBD0034B039 /* AddToCart */,
			);
			path = Products;
			sourceTree = "<group>";
		};
		B525334428AD3DD500B29A17 /* Profile */ = {
			isa = PBXGroup;
			children = (
				B556E17C28B7BE1600CB51F4 /* ProfileView.swift */,
				B556E17E28B7BE2D00CB51F4 /* ProfileDomain.swift */,
				B556E18028B7BEC200CB51F4 /* UserProfile.swift */,
			);
			path = Profile;
			sourceTree = "<group>";
		};
		B525334528AD3DDF00B29A17 /* Root */ = {
			isa = PBXGroup;
			children = (
				B5A5CFB4289C257900AC3E5E /* OnlineStoreTCAApp.swift */,
				B556E17828B671AC00CB51F4 /* RootView.swift */,
				B556E17A28B6740A00CB51F4 /* RootDomain.swift */,
			);
			path = Root;
			sourceTree = "<group>";
		};
		B525334F28AE87B500B29A17 /* CartList */ = {
			isa = PBXGroup;
			children = (
				B51F8DF528BF2F7C0047E7D9 /* Test */,
				B50F8F1128B4814E0034B039 /* Cart */,
				B525335028AE87C700B29A17 /* CartListDomain.swift */,
				B525335428AE8C5600B29A17 /* CartListView.swift */,
			);
			path = CartList;
			sourceTree = "<group>";
		};
		B5389D1328B9AAA4002BF611 /* OnlineStoreTCATests */ = {
			isa = PBXGroup;
			children = (
				B5389D1428B9AAA4002BF611 /* OnlineStoreTCATests.swift */,
			);
			path = OnlineStoreTCATests;
			sourceTree = "<group>";
		};
		B556E16228B5BD6000CB51F4 /* Network */ = {
			isa = PBXGroup;
			children = (
				B556E16328B5C44000CB51F4 /* APIClient.swift */,
				B5BE68E028BBAE93008FC353 /* DataLoadingStatus.swift */,
			);
			path = Network;
			sourceTree = "<group>";
		};
		B5A5CFA8289C257900AC3E5E = {
			isa = PBXGroup;
			children = (
				B5A5CFB3289C257900AC3E5E /* OnlineStoreTCA */,
				B5389D1328B9AAA4002BF611 /* OnlineStoreTCATests */,
				B5A5CFB2289C257900AC3E5E /* Products */,
			);
			sourceTree = "<group>";
		};
		B5A5CFB2289C257900AC3E5E /* Products */ = {
			isa = PBXGroup;
			children = (
				B5A5CFB1289C257900AC3E5E /* OnlineStoreTCA.app */,
				B5389D1228B9AAA3002BF611 /* OnlineStoreTCATests.xctest */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		B5A5CFB3289C257900AC3E5E /* OnlineStoreTCA */ = {
			isa = PBXGroup;
			children = (
				B556E16228B5BD6000CB51F4 /* Network */,
				B525334F28AE87B500B29A17 /* CartList */,
				B525334528AD3DDF00B29A17 /* Root */,
				B525334428AD3DD500B29A17 /* Profile */,
				B525334328AD3DCE00B29A17 /* Products */,
				B5A5CFB8289C257B00AC3E5E /* Assets.xcassets */,
				B5A5CFBA289C257B00AC3E5E /* Preview Content */,
			);
			path = OnlineStoreTCA;
			sourceTree = "<group>";
		};
		B5A5CFBA289C257B00AC3E5E /* Preview Content */ = {
			isa = PBXGroup;
			children = (
				B5A5CFBB289C257B00AC3E5E /* Preview Assets.xcassets */,
			);
			path = "Preview Content";
			sourceTree = "<group>";
		};
		B5BE68D728BA6E63008FC353 /* Test */ = {
			isa = PBXGroup;
			children = (
				B556E18428B919FB00CB51F4 /* AddToCartDomainTest.swift */,
			);
			path = Test;
			sourceTree = "<group>";
		};
		B5BE68D828BA6E94008FC353 /* Test */ = {
			isa = PBXGroup;
			children = (
				B5BE68D928BA6EA4008FC353 /* ProductDomainTest.swift */,
			);
			path = Test;
			sourceTree = "<group>";
		};
		B5BE68DC28BB2399008FC353 /* Test */ = {
			isa = PBXGroup;
			children = (
				B5BE68DD28BB23AC008FC353 /* ProductListDomainTest.swift */,
			);
			path = Test;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		B5389D1128B9AAA3002BF611 /* OnlineStoreTCATests */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = B5389D1A28B9AAA4002BF611 /* Build configuration list for PBXNativeTarget "OnlineStoreTCATests" */;
			buildPhases = (
				B5389D0E28B9AAA3002BF611 /* Sources */,
				B5389D0F28B9AAA3002BF611 /* Frameworks */,
				B5389D1028B9AAA3002BF611 /* Resources */,
			);
			buildRules = (
			);
			dependencies = (
				B5389D1728B9AAA4002BF611 /* PBXTargetDependency */,
			);
			name = OnlineStoreTCATests;
			productName = OnlineStoreTCATests;
			productReference = B5389D1228B9AAA3002BF611 /* OnlineStoreTCATests.xctest */;
			productType = "com.apple.product-type.bundle.unit-test";
		};
		B5A5CFB0289C257900AC3E5E /* OnlineStoreTCA */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = B5A5CFBF289C257B00AC3E5E /* Build configuration list for PBXNativeTarget "OnlineStoreTCA" */;
			buildPhases = (
				B5A5CFAD289C257900AC3E5E /* Sources */,
				B5A5CFAE289C257900AC3E5E /* Frameworks */,
				B5A5CFAF289C257900AC3E5E /* Resources */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = OnlineStoreTCA;
			packageProductDependencies = (
				B556E16A28B5CEA900CB51F4 /* ComposableArchitecture */,
			);
			productName = OnlineStoreTCA;
			productReference = B5A5CFB1289C257900AC3E5E /* OnlineStoreTCA.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		B5A5CFA9289C257900AC3E5E /* Project object */ = {
			isa = PBXProject;
			attributes = {
				BuildIndependentTargetsInParallel = 1;
				LastSwiftUpdateCheck = 1400;
				LastUpgradeCheck = 1330;
				TargetAttributes = {
					B5389D1128B9AAA3002BF611 = {
						CreatedOnToolsVersion = 14.0;
						TestTargetID = B5A5CFB0289C257900AC3E5E;
					};
					B5A5CFB0289C257900AC3E5E = {
						CreatedOnToolsVersion = 13.3;
					};
				};
			};
			buildConfigurationList = B5A5CFAC289C257900AC3E5E /* Build configuration list for PBXProject "OnlineStoreTCA" */;
			compatibilityVersion = "Xcode 13.0";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
			);
			mainGroup = B5A5CFA8289C257900AC3E5E;
			packageReferences = (
				B556E16928B5CEA900CB51F4 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */,
			);
			productRefGroup = B5A5CFB2289C257900AC3E5E /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				B5A5CFB0289C257900AC3E5E /* OnlineStoreTCA */,
				B5389D1128B9AAA3002BF611 /* OnlineStoreTCATests */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		B5389D1028B9AAA3002BF611 /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
		B5A5CFAF289C257900AC3E5E /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				B5A5CFBC289C257B00AC3E5E /* Preview Assets.xcassets in Resources */,
				B5A5CFB9289C257B00AC3E5E /* Assets.xcassets in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		B5389D0E28B9AAA3002BF611 /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				B5389D1B28B9AAC6002BF611 /* AddToCartDomainTest.swift in Sources */,
				B5389D1528B9AAA4002BF611 /* OnlineStoreTCATests.swift in Sources */,
				B5BE68DB28BA6EA8008FC353 /* ProductDomainTest.swift in Sources */,
				B5BE68DF28BB23B1008FC353 /* ProductListDomainTest.swift in Sources */,
				B51F8DF728BF2FF20047E7D9 /* CartListDomainTest.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
		B5A5CFAD289C257900AC3E5E /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				B525335128AE87C700B29A17 /* CartListDomain.swift in Sources */,
				B50F8F0128B19B880034B039 /* AddToCartButton.swift in Sources */,
				B50F8F1328B482E80034B039 /* CartItemDomain.swift in Sources */,
				B525335528AE8C5600B29A17 /* CartListView.swift in Sources */,
				B556E18328B84F1500CB51F4 /* ErrorView.swift in Sources */,
				B50F8F0628B19E660034B039 /* AddToCartDomain.swift in Sources */,
				B5BE68E128BBAE93008FC353 /* DataLoadingStatus.swift in Sources */,
				B556E17B28B6740A00CB51F4 /* RootDomain.swift in Sources */,
				B525334E28ADD6DE00B29A17 /* ProductListView.swift in Sources */,
				B5A5CFB5289C257900AC3E5E /* OnlineStoreTCAApp.swift in Sources */,
				B50F8F1028B3D7FB0034B039 /* CartCell.swift in Sources */,
				B556E17F28B7BE2D00CB51F4 /* ProfileDomain.swift in Sources */,
				B556E17928B671AC00CB51F4 /* RootView.swift in Sources */,
				B556E17D28B7BE1600CB51F4 /* ProfileView.swift in Sources */,
				B556E18128B7BEC200CB51F4 /* UserProfile.swift in Sources */,
				B556E16428B5C44000CB51F4 /* APIClient.swift in Sources */,
				B525334728AD3F6000B29A17 /* ProductListDomain.swift in Sources */,
				B50F8F0C28B2C1A70034B039 /* ProductDomain.swift in Sources */,
				B50F8EFF28B18F020034B039 /* ProductCell.swift in Sources */,
				B525335328AE883B00B29A17 /* CartItem.swift in Sources */,
				B50F8F0428B19CD30034B039 /* PlusMinusButton.swift in Sources */,
				B525334C28ADCBCE00B29A17 /* Product.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin PBXTargetDependency section */
		B5389D1728B9AAA4002BF611 /* PBXTargetDependency */ = {
			isa = PBXTargetDependency;
			target = B5A5CFB0289C257900AC3E5E /* OnlineStoreTCA */;
			targetProxy = B5389D1628B9AAA4002BF611 /* PBXContainerItemProxy */;
		};
/* End PBXTargetDependency section */

/* Begin XCBuildConfiguration section */
		B5389D1828B9AAA4002BF611 /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				BUNDLE_LOADER = "$(TEST_HOST)";
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = LHJA6J73E9;
				GENERATE_INFOPLIST_FILE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = com.swiftandtips.OnlineStoreTCATests;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = NO;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OnlineStoreTCA.app/OnlineStoreTCA";
			};
			name = Debug;
		};
		B5389D1928B9AAA4002BF611 /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				BUNDLE_LOADER = "$(TEST_HOST)";
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = LHJA6J73E9;
				GENERATE_INFOPLIST_FILE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = com.swiftandtips.OnlineStoreTCATests;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = NO;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
				TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OnlineStoreTCA.app/OnlineStoreTCA";
			};
			name = Release;
		};
		B5A5CFBD289C257B00AC3E5E /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = dwarf;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
				MTL_FAST_MATH = YES;
				ONLY_ACTIVE_ARCH = YES;
				SDKROOT = iphoneos;
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
			};
			name = Debug;
		};
		B5A5CFBE289C257B00AC3E5E /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_ENABLE_OBJC_WEAK = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 16.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				MTL_FAST_MATH = YES;
				SDKROOT = iphoneos;
				SWIFT_COMPILATION_MODE = wholemodule;
				SWIFT_OPTIMIZATION_LEVEL = "-O";
				VALIDATE_PRODUCT = YES;
			};
			name = Release;
		};
		B5A5CFC0289C257B00AC3E5E /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_ASSET_PATHS = "\"OnlineStoreTCA/Preview Content\"";
				DEVELOPMENT_TEAM = LHJA6J73E9;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/Frameworks",
				);
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = com.swiftandtips.OnlineStoreTCA;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
			};
			name = Debug;
		};
		B5A5CFC1289C257B00AC3E5E /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_ASSET_PATHS = "\"OnlineStoreTCA/Preview Content\"";
				DEVELOPMENT_TEAM = LHJA6J73E9;
				ENABLE_PREVIEWS = YES;
				GENERATE_INFOPLIST_FILE = YES;
				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
				LD_RUNPATH_SEARCH_PATHS = (
					"$(inherited)",
					"@executable_path/Frameworks",
				);
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = com.swiftandtips.OnlineStoreTCA;
				PRODUCT_NAME = "$(TARGET_NAME)";
				SWIFT_EMIT_LOC_STRINGS = YES;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		B5389D1A28B9AAA4002BF611 /* Build configuration list for PBXNativeTarget "OnlineStoreTCATests" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				B5389D1828B9AAA4002BF611 /* Debug */,
				B5389D1928B9AAA4002BF611 /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		B5A5CFAC289C257900AC3E5E /* Build configuration list for PBXProject "OnlineStoreTCA" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				B5A5CFBD289C257B00AC3E5E /* Debug */,
				B5A5CFBE289C257B00AC3E5E /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		B5A5CFBF289C257B00AC3E5E /* Build configuration list for PBXNativeTarget "OnlineStoreTCA" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				B5A5CFC0289C257B00AC3E5E /* Debug */,
				B5A5CFC1289C257B00AC3E5E /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */

/* Begin XCRemoteSwiftPackageReference section */
		B556E16928B5CEA900CB51F4 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = {
			isa = XCRemoteSwiftPackageReference;
			repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git";
			requirement = {
				kind = exactVersion;
				version = 1.14.0;
			};
		};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
		B556E16A28B5CEA900CB51F4 /* ComposableArchitecture */ = {
			isa = XCSwiftPackageProductDependency;
			package = B556E16928B5CEA900CB51F4 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */;
			productName = ComposableArchitecture;
		};
/* End XCSwiftPackageProductDependency section */
	};
	rootObject = B5A5CFA9289C257900AC3E5E /* Project object */;
}


================================================
FILE: OnlineStoreTCATests/OnlineStoreTCATests.swift
================================================
//
//  OnlineStoreTCATests.swift
//  OnlineStoreTCATests
//
//  Created by Pedro Rojas on 26/08/22.
//

import XCTest

final class OnlineStoreTCATests: XCTestCase {

    override func setUpWithError() throws {
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDownWithError() throws {
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func testExample() throws {
        // This is an example of a functional test case.
        // Use XCTAssert and related functions to verify your tests produce the correct results.
        // Any test you write for XCTest can be annotated as throws and async.
        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
    }

    func testPerformanceExample() throws {
        // This is an example of a performance test case.
        measure {
            // Put the code you want to measure the time of here.
        }
    }

}


================================================
FILE: README.md
================================================
# Before starting
- This demo was implemented using version [1.15.2](https://pointfreeco.github.io/swift-composable-architecture/1.14.0/documentation/composablearchitecture/) of TCA.
- The demo runs on iOS 17.6 and above.
- All credits about TCA go to [Brandon Willams](https://twitter.com/mbrandonw), [Stephen Celis](https://twitter.com/stephencelis) and the incredible team at [pointfree.co](https://www.pointfree.co/) ❤️.

# Online Store made with Composable Architecture (TCA)
The purpose of this demo is to provide an introduction to the main concepts of TCA. If you are new to TCA, I **highly** recommend starting with the README from the [main repository](https://github.com/pointfreeco/swift-composable-architecture) and watching the informative [Tour of TCA](https://www.pointfree.co/collections/composable-architecture/a-tour-of-the-composable-architecture). These resources will provide you with a solid foundation and a comprehensive understanding of the TCA framework.

## Content
* [Motivation](#motivation)
* [Screenshots of the app](#screenshots)
* [The basics](#the-basics)
    * [Archiecture Diagram](#archiecture-diagram)
    * [Hello World Example](#hello-world-example)
* [Composition](#composition)
    * [Body to compose multiple Reducers](#body-to-compose-multiple-reducers)
    * [Single state operators](#single-state-operators)
      * [store.scope(state:action:)](#storescopestateaction)
      * [Scope in Reducers](#scope-in-reducers)
    * [Collection of states](#collection-of-states)
      * [forEach in Reducer](#foreach-in-reducer)
* [Dependencies](#dependencies)
* [Side Effects](#side-effects)
    * [Network Calls](#network-calls)
* [Navigation](#navigation)
    * [Alerts](#alerts)
    * [Sheets](#sheets)
* [Testing](#testing)
    * [Basics](#testing-basics)
    * [Side Effects](#testing-side-effects)
    * [CasePathable](#testing-CasePathable)
* [Other Topics](#other-topics)
    * [Optional States](#optional-states)
    * [Private Actions](#private-actions)
    * [Making a Root Domain with Tab View](#making-a-root-domain-with-tab-view)
* [Contact](#contact)


## Motivation
**TL;DR:** This project aims to build an app using TCA, striking a balance between simplicity and complexity. It focuses on exploring the most important use cases of TCA while providing concise and accessible documentation for new learners. The goal is to create a valuable learning resource that offers practical insights into using TCA effectively.

I aimed to showcase the power of the TCA architecture in building robust applications for the Apple ecosystem, including iOS, macOS, and more excitingly, its future expansion beyond the Apple world! 🚀

While there are many articles available that demonstrate simple one-screen applications to introduce TCA's core concepts, I noticed a gap between these basic demos and real-world applications like [isoword](https://github.com/pointfreeco/isowords), which can be complex and challenging to understand certain important use cases (like navigation and how reducers are glued).

In this demo, I have implemented a minimal online store that connects to a real network API (https://fakestoreapi.com). It features a product list, the ability to add items to the cart, and the functionality to place orders. While the requests are not processed in real-time (as it uses a fake API), the network status is simulated, allowing you to experience the interaction and mapping of network calls using TCA.

While this demo may not be a full-scale real-world application, it includes enough reducers to illustrate how data can be effectively connected and how domains can be isolated to handle specific components within the app (e.g., Tabs -> Product List -> Product Cell -> Add to Cart button).

Furthermore, I have created tests to demonstrate one of TCA's key features: ensuring that tests fail if the expected state mutations are not captured accurately. This showcases how TCA promotes testability and helps ensure the correctness of your application.

If you're looking to dive into TCA, this demo provides a valuable middle ground between simple examples and complex projects, offering concise documentation and practical insights into working with TCA in a more realistic application setting.

Any feedback is welcome! 🙌🏻

## Screenshots
### Tabs
<img src="./Images/demo1.png"  width="25%" height="25%">|<img src="./Images/demo2.png"  width="25%" height="25%">|<img src="./Images/demo6.png"  width="25%" height="25%">

### Cart
<img src="./Images/demo3.png"  width="25%" height="25%">|<img src="./Images/demo4.png"  width="25%" height="25%">|<img src="./Images/demo5.png"  width="25%" height="25%">

## The basics
### Archiecture Diagram
<img src="./Images/TCA_Architecture2.png">

### Hello World Example
Consider the following implementation of a simple app using TCA, where you will have two buttons: one to increment a counter displayed on the screen and the other to decrement it.

Here's an example of how this app would be coded with TCA:

1. A struct that will represent the domain of the feature. This struct must conform `ReducerProtocol` protocol and providing `State` struct, `Action` enum and `reduce` method.

```swift
struct CounterDomain: ReducerProtocol {
    struct State {
        // State of the feature
    }

    enum Action {
        // actions that use can do in the app
    }
    
    func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
        // Method that will mutate the state given an action.
    }
}
```

2. The view that is presented in the screen will display the current state of the app.
<img src="./Images/viewDemo1.png" width="30%" height="30%">

```swift
struct State: Equatable {
    var counter = 0
}
```

3. When the user presses a button (let's say increase button), it will internally send an action to the store.
<img src="./Images/actionDemo1.png" width="30%" height="30%">

```swift
enum Action: Equatable {
    case increaseCounter
    case decreaseCounter
}
```

4. The action will be received by the reducer and proceed to mutate the state. Reducer MUST also return an effect, that represent logic from the "outside world" (network calls, notifications, database, etc). If no effect is needed, just return `EffectTask.none` .

```swift
func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
    switch action {
    case .increaseCounter:
        state.counter += 1
        return .none
    case .decreaseCounter:
        state.counter -= 1
        return .none
    }
}
```

5. Once the mutation is done and the reducer returned the effect, the view will render the update in the screen. 
<img src="./Images/viewUpdateDemo1.png" width="30%" height="30%">

6. To observe state changes in TCA, we need an object called `viewStore`, that in this example is wrapped within WithViewStore view. We can send an action from the view to the store using `viewStore.send()` and an `Action` value.

```swift
struct ContentView: View {
    let store: Store<State, Action>

    var body: some View {
        WithViewStore(self.store) { viewStore in
            HStack {
                Button {
                    viewStore.send(.decreaseCounter)
                } label: {
                    Text("-")
                        .padding(10)
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .buttonStyle(.plain)

                Text(viewStore.counter.description)
                    .padding(5)

                Button {
                    viewStore.send(.increaseCounter)
                } label: {
                    Text("+")
                        .padding(10)
                        .background(.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
                .buttonStyle(.plain)
            }
        }
    }
}
```

7. View is initialized by a `Store` object.

```swift
ContentView(
    store: Store(
        initialState: CounterDomain.State(),
        reducer: CounterDomain()
    )
)
```

If you want to learn more about the basics, check out the following [video](https://youtu.be/SfFDj6qT-xg)

> Note: The videos shared here were made using the legacy version of TCA with Environment and without `ReducerProtocol`. If you want to see the legacy version of TCA, check out this [branch](https://github.com/pitt500/OnlineStoreTCA/tree/legacy-tca-with-environment).

## Composition

Composition refers to the process of building complex software systems by combining smaller, reusable software components. Take a look to this image:

<img src="./Images/composition2.png" width="80%" height="80%">

We started with a simple button counter, then we add an extra state to display text, next we put the whole button in a Product cell, and finally, each product cell will be part of a Product list. That is composition!

### Body to compose multiple Reducers
In the previous example, we demonstrated the usage of `reduce(into:action:)` to create our reducer function and define how state will be modified for each action. However, it's important to note that this method is suitable only for leaf components, which refer to the smallest components in your application.

For larger components, we can leverage the `body` property provided by the `ReducerProtocol`. This property enables you to combine multiple reducers, facilitating the creation of more comprehensive components. By utilizing the `body` property, you can effectively compose and manage the state mutations of these larger components.
```swift
var body: some ReducerProtocol<State, Action> {
    ChildReducer1()
    Reduce { state, action in
        switch action {
        case .increaseCounter:
            state.counter += 1
            return .none
        case .decreaseCounter:
            state.counter -= 1
            return .none
        }
    }
    ChildReducer2()
}
```

The `Reduce` closure will always encapsulate the logic from the parent domain. To understand how to combine additional components, please continue reading below.

> Compared to the previous version of TCA without `ReducerProtocol`, the order of child reducers will not affect the result. Parent Reducer (`Reduce`) will be always executed at the end.

### Single state operators

For single states (all except collections/lists), TCA provides operators to glue the components and make bigger ones.

#### store.scope(state:action:) 
`store.scope` is an operator used in views to get the child domain's (`AddToCartDomain`) state and action from parent domain (`ProductDomain`) to initialize subviews. 
For example, the `ProductDomain` below contains two properties as part of its state: `product` and `addToCartState`.

```swift
struct ProductDomain: ReducerProtocol {
    struct State: Equatable, Identifiable {
        let product: Product
        var addToCartState = AddToCartDomain.State()
    }
    // ...
```

Furthermore, we utilize an action with an associated value that encapsulates all actions from the child domain, providing a comprehensive and cohesive approach.
```swift
struct ProductDomain: ReducerProtocol {
    // State ...

    enum Action {
        case addToCart(AddToCartDomain.Action)
    }
    // ...
```

Let's consider the scenario where we need to configure the `ProductCell` view below. The `ProductCell` is designed to handle the `ProductDomain`, while we need to provide some information to initialize the `AddToCartButton`. However, the `AddToCartButton` is only aware of its own domain, `AddToCartDomain`, and not the `ProductDomain`. To address this, we can use the `scope` method from `store` to get the child's state and action from parent domain. This enables us to narrow down the scope of the button to focus solely on its own functionality.

```swift
struct ProductCell: View {
    let store: Store<ProductDomain.State, ProductDomain.Action>
    
    var body: some View {
        WithViewStore(self.store) { viewStore in
            // More views here ...
            AddToCartButton(
                store: self.store.scope(
                    state: \.addToCartState,
                    action: ProductDomain.Action.addToCart
                )
            )
        }
    }
```
By employing this approach, the `AddToCartDomain` will solely possess knowledge of its own state and remain unaware of any product-related information.

#### Scope in Reducers
`Scope` is utilized within the `body` to seamlessly transform the child reducer (`AddToCart`) into a compatible form that aligns with the parent reducer (`Product`). This allows for smooth integration and interaction between the two.
```swift
var body: some ReducerProtocol<State, Action> {
    Scope(state: \.addToCartState, action: /ProductDomain.Action.addToCart) {
        AddToCartDomain()
    }
    Reduce { state, action in
        // Parent Reducer logic ...
    }
}
```
This transformation becomes highly valuable when combining multiple reducers to construct a more complex component.

> In earlier versions, the `pullback` and `combine` operators were employed to carry out the same operation. You can watch this [video](https://youtu.be/Zf2pFEa3uew).

### Collection of states

Are you looking to manage a collection of states? TCA offers excellent support for that as well!

In this particular example, instead of using a regular array, TCA requires a list of (`Product`) states, which can be achieved by utilizing `IdentifiedArray`:
```swift
struct ProductListDomain: ReducerProtocol {
    struct State: Equatable {
        var productList: IdentifiedArrayOf<ProductDomain.State> = []
        // ...    
    }
    // ...
}
```

#### forEach in Reducer

The `forEach` operator functions similarly to the [`Scope`](#scope-in-reducers) operator, with the distinction that it operates on a collection of states. It effectively transforms the child reducers into compatible forms that align with the parent reducer.

```swift
struct ProductListDomain: ReducerProtocol {
    // State and Actions ...
    
    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            // Parent Reducer...
        }
        .forEach(
            \.productList, 
            action: /ProductListDomain.Action.product(id:action:)
        ) {
            ProductDomain()
        }
    }
}
```

Subsequently, in the user interface, we employ `ForEachStore` and `store.scope` to iterate through all the (`Product`) states and actions. This enables us to send actions to the corresponding cell and modify its state accordingly.
```swift
List {
    ForEachStore(
        self.store.scope(
            state: \.productListState,
            action: ProductListDomain.Action
                .product(id: action:)
        )
    ) {
        ProductCell(store: $0)
    }
}
```

> There's a legacy `forEach` operator, If you want to learn more, check out this [video](https://youtu.be/sid-zfggYhQ)

## Dependencies
In previous iterations of TCA, `Environment` played a crucial role in consolidating all the dependencies utilized by a domain.

With the introduction of the [`ReducerProtocol`](https://www.pointfree.co/blog/posts/81-announcing-the-reducer-protocol), we have eliminated the concept of `Environment`. As a result, dependencies now reside directly within the domain.

```swift
struct ProductListDomain: ReducerProtocol {
    // State ...

    // Actions...

    var fetchProducts:  () async throws -> [Product]
    var sendOrder: ([CartItem]) async throws -> String
    var uuid: () -> UUID

    // Reducer ...
}
```

Nevertheless, we have the option to leverage the [Dependencies Framework](https://github.com/pointfreeco/swift-dependencies) to achieve a more enhanced approach in managing our dependencies:

```swift
struct ProductListDomain: ReducerProtocol {
    // State ...

    // Actions...

    @Dependency(\.apiClient.fetchProducts) var fetchProducts
    @Dependency(\.apiClient.sendOrder) var sendOrder
    @Dependency(\.uuid) var uuid

    // Reducer ...
}
```

> If you want to learn more about how Environment object works on TCA, take a look to this [video](https://youtu.be/sid-zfggYhQ?list=PLHWvYoDHvsOVo4tklgLW1g7gy4Kmk4kjw&t=103)

## Side Effects
A side effect refers to an observable change that arises when executing a function or method. This encompasses actions such as modifying state outside the function, performing I/O operations to a file or making network requests. TCA facilitates the encapsulation of such side effects through the use of `EffectTask` objects.

<img src="./Images/sideEffects1.png" width="80%" height="80%">

> If you want to learn more about side effects, check out this [video](https://youtu.be/t3HHam3GYkU)

### Network calls
Network calls are a fundamental aspect of mobile development, and TCA offers robust tools to handle them efficiently. As network calls are considered external interactions or [side effects](#side-effects), TCA utilizes the `EffectTask` object to encapsulate these calls. Specifically, network calls are encapsulated within the `EffectTask.task` construct, allowing for streamlined management of asynchronous operations within the TCA framework.

However, it's important to note that the task operator alone is responsible for making the web API call. To obtain the actual response, an additional action needs to be implemented, which will capture and store the result within a `TaskResult` object.

```swift
struct ProductListDomain: ReducerProtocol {
    // State and more ...
    
    enum Action: Equatable {
        case fetchProducts
        case fetchProductsResponse(TaskResult<[Product]>)
    }
   
    var fetchProducts: () async throws -> [Product]
    var uuid: () -> UUID
    
    var body: some ReducerProtocol<State, Action> {
        // Other child reducers...
        Reduce { state, action in
            switch action {
            case .fetchProducts:
                return .task {
                    // Just making the call 
                    await .fetchProductsResponse(
                        TaskResult { try await fetchProducts() }
                    )
                }
            case .fetchProductsResponse(.success(let products)):
                // Getting the success response
                state.productListState = IdentifiedArrayOf(
                    uniqueElements: products.map {
                        ProductDomain.State(
                            id: uuid(),
                            product: $0
                        )
                    }
                )
                return .none
            case .fetchProductsResponse(.failure(let error)):
                // Getting an error from the web API
                print("Error getting products, try again later.", error)
                return .none
            }
        }
    }
}
```

> To learn more about network requests in TCA, I recommend watching this insightful [video](https://youtu.be/sid-zfggYhQ?list=PLHWvYoDHvsOVo4tklgLW1g7gy4Kmk4kjw&t=144) that explains asynchronous requests. Additionally, you can refer to this informative [video](https://youtu.be/j2qymM6i9n4) that demonstrates the configuration of a real web API call, providing practical insights into the process.

## Navigation

 Navigation is a huge and complex topic. Navigation are alerts, confirmation dialogs, sheets, popovers and links. Also, you can add a custom navigations if you want. In this project you will see alerts and sheets.

### Alerts

The TCA library offers support for `AlertView`, enabling the addition of custom state and a consistent UI building approach without deviating from the TCA architecture. To create your own alert using TCA, follow these steps:

1. Create the alert actions inside of the Action enum of the reducer. The recommended way is create a nested enum inside the action.

```swift
enum Action: Equatable {
    enum Alert {
        case alertAction1
        case alertAction2
        ....
    }
}
```

2. Next, create a case alert and use `PresentationAction`.

```swift
enum Action: Equatable {
    case alert(PresentationAction<Alert>)
    case alertButtonTapped

    enum Alert {
        case alertAction1
        case alertAction2
        ....
    }
}
```

`PresentationAction` is a generic that represents the presented actions and an special action named dismiss. This is very useful case because with the dismiss action, the reducer can manage if a side effect is running and remove to the system. More information about effect cancelling in navigations [here](https://www.pointfree.co/collections/composable-architecture/navigation/ep225-composable-navigation-behavior).

```swift
public enum PresentationAction<Action> {
  /// An action sent to `nil` out the associated presentation state.
  case dismiss

  /// An action sent to the associated, non-`nil` presentation state.
  indirect case presented(Action)
}
```

3. Create an alert state inside of the reducer.
```swift
@Presents var alert: AlertState<Action.Alert>?
```
`@Presents` is a property wrapper that you need to use when creates a navigation state in the reducer. The reason to use `@Presents` is when composing a lots of features together, the root state could overflow the stack. More information [here](https://www.pointfree.co/collections/composable-architecture/navigation/ep230-composable-navigation-stack-vs-heap).


4. Extent `AlertState` and create as many alerts as you want. You can create a property wrapper or a function if you need some dynamic information.

```swift
extension AlertState where Action == CartListDomain.Action.Alert {
    static var successAlert: AlertState {
        AlertState {
            TextState("Thank you!")
        } actions: {
            ButtonState(action: .dismissSuccessAlert, label: { TextState("Done") })
            ButtonState(role: .cancel, action: .didCancelConfirmation, label: { TextState("Cancel") })
        } message: {
            TextState("Your order is in process.")
        }
    }

    static func confirmationAlert(totalPriceString: String) -> AlertState {
        AlertState {
            TextState("Confirm your purchase")
        } actions: {
            ButtonState(action: .didConfirmPurchase, label: { TextState("Pay \(totalPriceString)") })
            ButtonState(role: .cancel, action: .didCancelConfirmation, label: { TextState("Cancel") })
        } message: {
            TextState("Do you want to proceed with your purchase of \(totalPriceString)?")
        }
    }
}
```

5. Inside of the body of the reducer you can set the alert. As the state is an optional value, you need to implement `ifLet` in the reducer. This is a particular modifier that not need a reducer like a tipical `ifLet` reducer. 

Another question is when you use a reducer for navigation, you will use the binding operator `$` in the state. This is because navigation modifiers in SwiftUI use a binding for presenting, usually the `isPresented` boolean. In this case, in order to manage when the alert is presented or no, you use a binding state in the reducer. Now, the reducer is fully synchronized with the view.

```swift
var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
                case .alert:
                    return .none
                case .alertButtonTapped:
                    state.alert = .successAlert
                    return .none
            }
        }
        .ifLet(
            \.$alert, 
            action: \.alert
        )
}
```

6. Finally, implements `AlertView` modifier in the view. Remember, you use the binding `$` operator.

```swift
.alert(
	store: store.scope(
		state: \.$alert,
		action: \.alert
	)
)
```


<details>
<summary>See Alerts in previous versions of TCA</summary>

The TCA library also offers support for `AlertView`, enabling the addition of custom state and a consistent UI building approach without deviating from the TCA architecture. To create your own alert using TCA, follow these steps:

1. Create an `AlertState` with actions of your own domain.
2. Create the actions that will trigger events for the alert:
    - Initialize AlertState (`didPressPayButton`)
    - Dismiss the alert (`didCancelConfirmation`)
    - Execute the alert's handler (`didConfirmPurchase`)

```swift
struct CartListDomain: ReducerProtocol {
    struct State: Equatable {
        var confirmationAlert: AlertState<CartListDomain.Action>?
        
        // More properties ...
    }
    
    enum Action: Equatable {
        case didPressPayButton
        case didCancelConfirmation
        case didConfirmPurchase
        
        // More actions ...
    }
    
    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            case .didCancelConfirmation:
                state.confirmationAlert = nil
                return .none
            case .didConfirmPurchase:
                // Sent order and Pay ...
            case .didPressPayButton:
                state.confirmationAlert = AlertState(
                    title: TextState("Confirm your purchase"),
                    message: TextState("Do you want to proceed with your purchase of \(state.totalPriceString)?"),
                    buttons: [
                        .default(
                            TextState("Pay \(state.totalPriceString)"),
                            action: .send(.didConfirmPurchase)),
                        .cancel(TextState("Cancel"), action: .send(.didCancelConfirmation))
                    ]
                )
                return .none
            // More actions ...
            }
        }
        .forEach(\.cartItems, action: /Action.cartItem(id:action:)) {
            CartItemDomain()
        }
    }
}              
```
</details>

### Sheets

Other type of navigation are sheets. To create your own alert using TCA, follow these steps:

1. As the alerts, create the state. You use `@Presents` to avoid accidentally overflow the stack.

```swift
@Presents var cartState: CartListDomain.State?
```

2. Next, create the action. Remember to use PresentationAction inside the case of the sheet.

```swift
case cart(PresentationAction<CartListDomain.Action>)
```

3. Create the `ifLet` in the reducer. Here, you need to define the reducer of the destination.

```swift
.ifLet(\.$cartState, action: \.cart) {
    CartListDomain()
}
```

4. Finally, in the view, you can define the sheet operator like this.

```swift
.sheet(
	item: $store.scope(
		state: \.cartState,
		action: \.cart
	)
) { store in
	CartListView(store: store)
}
```

<details>
<summary>See Sheets in previous versions of TCA</summary>
### Opening Modal Views

If you require to open a view modally in SwiftUI, you will need to use sheet modifier and provide a binding parameter:
```swift
func sheet<Content>(
    isPresented: Binding<Bool>,
    onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content
) -> some View where Content : View
```

To utilize this modifier (or any modifier with binding parameters) in TCA, it is necessary to employ the `binding` operator from `viewStore` and supply two parameters:

1. The state property that will undergo mutation.
2. The action that will trigger the mutation.

```swift
// Domain:
struct Domain: ReducerProtocol {
    struct State {
        var shouldOpenModal = false
    }
    enum Action {
        case setCartView(isPresented: Bool)
    }

    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
                case .setCartView(let isPresented):
                    state.shouldOpenModal = isPresented
            }
        }
    }
}

// UI:
Text("Parent View")
.sheet(
    isPresented: viewStore.binding(
        get: \.shouldOpenModal,
        send: Action.setModalView(isPresented:)
    )
) {
    Text("I'm a Modal View!")
}
```

> If you want to lean more about Binding with TCA and SwiftUI, take a look to this [video](https://youtu.be/Ilr8AsoggIY).
</details>

## Testing

### Testing Basics

Testing is a crucial part of software development. TCA has its own tools to test reducers in a very simple way.

When you test a reducer, you will use a TestStore class passing an initial state and a reducer like the store that you are using in the production code.

Next, you can send an action but, in this case, send receive a closure that you need to expect the result of this action. For example, when you send increseCounter action, you expect that count is equal to 1 if previously, your state counter is 0.

Finally, you send a decreaseCounter and the expectation of this action is count state equal to 0 because previously count was setted to 1.

```swift
@MainActor
class CounterDomainTest: XCTestCase {
    func testHappyPath() {
        let store = TestStore(
            initialState: CounterDomain.State(),
            reducer: { CounterDomain() }
        )

        await store.send(.increaseCounter) {
            $0.count = 1
        }

        await store.send(.decreaseCounter) {
            $0.count = 0
        }
    }
}
```

### Testing Side effects

The first thing is the ability to mock every side effect of the system. To do that TestStore has a closure for this purpose.

Notice that `fetchProducts` action has a side effect. When it finishes, send an action `fetchProductsResponse` back to the system. When you test this, you will use `store.receive` for response actions.

```swift
@MainActor
class ProductListDomainTest: XCTestCase {
    func testSideEffects() {
        let products: [Product] = ...
        let store = TestStore(
            initialState: ProductListDomain.State(),
            reducer: { ProductListDomain() }
        ) {
            $0.apiClient.fetchProducts = { products }
        }

         await store.send(.fetchProducts) {
            $0.dataLoadingStatus = .loading
        }
        
        await store.receive(.fetchProductsResponse(.success(products))) {
            $0.products = products
            $0.dataLoadingStatus = .success
        }
    }
}
```

### Testing CasePathable

CasePathable is a nice macro that it has a lot of useful tips. One of those is using keypaths for testing actions. For example, if you have this test.

```swift
await store.send(
            .cartItem(
                .element(
                    id: cartItemId1,
                    action: .deleteCartItem(product: Product.sample[0]))
            )
        ) {
            ...
        }
```

We can update this with:

```swift
await store.send(\.cartItem[id: cartItemId1].deleteCartItem, Product.sample[0]) {
    ...
}
```

Another example:

```swift
await store.send(.alert(.presented(.didConfirmPurchase)))
```

```swift
await store.send(\.alert.didConfirmPurchase)
```

## Other topics

### Optional States

By default, TCA keeps a state in memory throughout the entire lifecycle of an app. However, in certain scenarios, maintaining a state can be resource-intensive and unnecessary. One such case is when dealing with modal views that are displayed for a short duration. In these situations, it is more efficient to use optional states.

Creating an optional state in TCA follows the same approach as declaring any optional value in Swift. Simply define the property within the parent state, but instead of assigning a default value, declare it as optional. For instance, in the provided example, the `cartState` property holds an optional state for a Cart List.

```swift
struct ProductListDomain: ReducerProtocol {
    struct State: Equatable {
        var productListState: IdentifiedArrayOf<ProductDomain.State> = []
        var shouldOpenCart = false
        var cartState: CartListDomain.State?
        
        // More properties...
    }
}
```

Now, in the `Reduce` function, we can utilize the `ifLet` operator to transform the child reducer (`CartListDomain`) into one that is compatible with the parent reducer (`ProductList`). 

In the provided example, the `CartListDomain` will be evaluated only if the `cartState` is non-nil. To assign a new non-optional state, the parent reducer will need to initialize the property (`cartState`) when a specific action (`setCartView`) is triggered. 

This approach ensures that the optional state is properly handled within the TCA framework and allows for seamless state management between the parent and the optional child reducers.

```swift
struct ProductListDomain: ReducerProtocol {
    // State and Actions ...
    
    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            //  More cases ...
            case .setCartView(let isPresented):
                state.shouldOpenCart = isPresented
                state.cartState = isPresented
                ? CartListDomain.State(...)
                : nil
                return .none
            }
        }
        .ifLet(\.cartState, action: /ProductListDomain.Action.cart) {
            CartListDomain()
        }
    }
}
```

Lastly, in the view, you can employ `IfLetStore` to unwrap a store with optional state. This allows you to conditionally display the corresponding view that operates with that particular state.


```swift
List {
    ForEachStore(
        self.store.scope(
            state: \.productListState,
            action: ProductListDomain.Action
                .product(id: action:)
        )
    ) {
        ProductCell(store: $0)
    }
}
.sheet(
    isPresented: viewStore.binding(
        get: \.shouldOpenCart,
        send: ProductListDomain.Action.setCartView(isPresented:)
    )
) {
    IfLetStore(
        self.store.scope(
            state: \.cartState,
            action: ProductListDomain.Action.cart
        )
    ) {
        CartListView(store: $0)
    }
}
```

> If you want to learn more about optional states, check out this [video](https://youtu.be/AV0laQw2OjM).

### Private Actions

By default, when you declare an action in a TCA domain, it is accessible to other reducers as well. However, there are situations where an action is intended to be specific to a particular reducer and does not need to be exposed outside of it. 

In such cases, you can simply declare private functions to encapsulate those actions within the domain's scope. This approach ensures that the actions remain private and only accessible within the intended context, enhancing the encapsulation and modularity of your TCA implementation:

```swift
var body: some ReducerProtocol<State, Action>
    // More reducers ...
    Reduce { state, action in
        switch action {
        // More actions ...
        case .cart(let action):
            switch action {
            case .didPressCloseButton:
                return closeCart(state: &state)
            case .dismissSuccessAlert:
                resetProductsToZero(state: &state)

                return .task {
                    .closeCart
                }
            }
        case .closeCart:
            return closeCart(state: &state)
        }
    }
}

private func closeCart(
        state: inout State
) -> Effect<Action, Never> {
    state.shouldOpenCart = false
    state.cartState = nil

    return .none
}

private func resetProductsToZero(
    state: inout State
) {
    for id in state.productListState.map(\.id)
    where state.productListState[id: id]?.count != 0  {
        state.productListState[id: id]?.addToCartState.count = 0
    }
}
```

> For more about private actions, check out this [video](https://youtu.be/7BkZX_7z-jw).

3. Invoke the UI

<img src="./Images/alertView1.png" width="50%" height="50%">

```swift
let store: Store<CartListDomain.State, CartListDomain.Action>

Text("Parent View")
.alert(
    self.store.scope(state: \.confirmationAlert, action: { $0 }),
    dismiss: .didCancelConfirmation
)
```

> Explicit action is always needed for `store.scope`. Check out this commit to learn more: https://github.com/pointfreeco/swift-composable-architecture/commit/da205c71ae72081647dfa1442c811a57181fb990

This [video](https://youtu.be/U3EMduy-DhE) explains more about AlertView in SwiftUI and TCA.

### Making a Root Domain with Tab View

Creating a Root Domain in TCA is similar to creating any other domain. In this case, each property within the state will correspond to a complex substate. To handle tab logic, we can include an enum that represents each tab item, providing a structured approach to managing the different tabs:

```swift
struct RootDomain: ReducerProtocol {
    struct State: Equatable {
        var selectedTab = Tab.products
        var productListState = ProductListDomain.State()
        var profileState = ProfileDomain.State()
    }
    
    enum Tab {
        case products
        case profile
    }
    
    enum Action: Equatable {
        case tabSelected(Tab)
        case productList(ProductListDomain.Action)
        case profile(ProfileDomain.Action)
    }
    
    // Dependencies
    var fetchProducts: @Sendable () async throws -> [Product]
    var sendOrder:  @Sendable ([CartItem]) async throws -> String
    var fetchUserProfile:  @Sendable () async throws -> UserProfile
    var uuid: @Sendable () -> UUID
    
    static let live = Self(
        fetchProducts: APIClient.live.fetchProducts,
        sendOrder: APIClient.live.sendOrder,
        fetchUserProfile: APIClient.live.fetchUserProfile,
        uuid: { UUID() }
    )
    
    var body: some ReducerProtocol<State, Action> {
        Reduce { state, action in
            switch action {
            case .productList:
                return .none
            case .tabSelected(let tab):
                state.selectedTab = tab
                return .none
            case .profile:
                return .none
            }
        }
        Scope(state: \.productListState, action: /RootDomain.Action.productList) {
            ProductListDomain(
                fetchProducts: fetchProducts,
                sendOrder: sendOrder,
                uuid: uuid
            )
        }
        Scope(state:  \.profileState, action: /RootDomain.Action.profile) {
            ProfileDomain(fetchUserProfile: fetchUserProfile)
        }
    }
}
```

When it comes to the UI implementation, it closely resembles the standard SwiftUI approach, with a small difference. Instead of using a regular property, we hold the `store` property to manage the currently selected tab:

```swift
struct RootView: View {
    let store: Store<RootDomain.State, RootDomain.Action>
    
    var body: some View {
        WithViewStore(self.store) { viewStore in
            TabView(
                selection: viewStore.binding(
                    get: \.selectedTab,
                    send: RootDomain.Action.tabSelected
                )
            ) {
                ProductListView(
                    store: self.store.scope(
                        state: \.productListState,
                        action: RootDomain.Action
                            .productList
                    )
                )
                .tabItem {
                    Image(systemName: "list.bullet")
                    Text("Products")
                }
                .tag(RootDomain.Tab.products)
                ProfileView(
                    store: self.store.scope(
                        state: \.profileState,
                        action: RootDomain.Action.profile
                    )
                )
                .tabItem {
                    Image(systemName: "person.fill")
                    Text("Profile")
                }
                .tag(RootDomain.Tab.profile)
            }
        }
    }
}
```

To call RootView, we provide the initial domain state and the reducer:
To instantiate the `RootView`, you need to provide two parameters: the initial domain state and the reducer:

```swift
@main
struct OnlineStoreTCAApp: App {
    var body: some Scene {
        WindowGroup {
            RootView(
                store: Store(
                    initialState: RootDomain.State(),
                    reducer: RootDomain.live
                )
            )
        }
    }
}
```

These elements enable the proper initialization and functioning of the `RootView` within the TCA architecture.

> For a comprehensive understanding of this implementation, I recommend checking out this [video](https://youtu.be/a_FwMVIhCHY).

## Contact
If you have any feedback, I would love to hear from you. Please feel free to reach out to me through any of my social media channels:

* [Youtube](https://youtube.com/@swiftandtips)
* [Twitter](https://twitter.com/swiftandtips)
* [LinkedIn](https://www.linkedin.com/in/pedrorojaslo/)
* [Mastodon](https://iosdev.space/@swiftandtips)

Thanks for reading, and have a great day! 😄
Download .txt
gitextract_gp_koxcg/

├── .gitignore
├── OnlineStoreTCA/
│   ├── Assets.xcassets/
│   │   ├── AccentColor.colorset/
│   │   │   └── Contents.json
│   │   ├── AppIcon.appiconset/
│   │   │   └── Contents.json
│   │   ├── Contents.json
│   │   ├── bag.imageset/
│   │   │   └── Contents.json
│   │   ├── jacket.imageset/
│   │   │   └── Contents.json
│   │   └── tshirt.imageset/
│   │       └── Contents.json
│   ├── CartList/
│   │   ├── Cart/
│   │   │   ├── CartCell.swift
│   │   │   ├── CartItem.swift
│   │   │   └── CartItemDomain.swift
│   │   ├── CartListDomain.swift
│   │   ├── CartListView.swift
│   │   └── Test/
│   │       └── CartListDomainTest.swift
│   ├── Network/
│   │   ├── APIClient.swift
│   │   └── DataLoadingStatus.swift
│   ├── Preview Content/
│   │   └── Preview Assets.xcassets/
│   │       └── Contents.json
│   ├── Products/
│   │   ├── AddToCart/
│   │   │   ├── AddToCartButton.swift
│   │   │   ├── AddToCartDomain.swift
│   │   │   ├── PlusMinusButton.swift
│   │   │   └── Test/
│   │   │       └── AddToCartDomainTest.swift
│   │   ├── Product/
│   │   │   ├── Product.swift
│   │   │   ├── ProductCell.swift
│   │   │   ├── ProductDomain.swift
│   │   │   └── Test/
│   │   │       └── ProductDomainTest.swift
│   │   └── ProductList/
│   │       ├── ErrorView.swift
│   │       ├── ProductListDomain.swift
│   │       ├── ProductListView.swift
│   │       └── Test/
│   │           └── ProductListDomainTest.swift
│   ├── Profile/
│   │   ├── ProfileDomain.swift
│   │   ├── ProfileView.swift
│   │   └── UserProfile.swift
│   └── Root/
│       ├── OnlineStoreTCAApp.swift
│       ├── RootDomain.swift
│       └── RootView.swift
├── OnlineStoreTCA.xcodeproj/
│   └── project.pbxproj
├── OnlineStoreTCATests/
│   └── OnlineStoreTCATests.swift
└── README.md
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (153K chars).
[
  {
    "path": ".gitignore",
    "chars": 3101,
    "preview": "# Created by https://www.toptal.com/developers/gitignore/api/xcode,swift,macos\n# Edit at https://www.toptal.com/develope"
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/AccentColor.colorset/Contents.json",
    "chars": 123,
    "preview": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }"
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 1591,
    "preview": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"iphone\",\n      \"scale\" : \"2x\",\n      \"size\" : \"20x20\"\n    },\n    {\n      \"idiom\""
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/bag.imageset/Contents.json",
    "chars": 301,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"bag.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n    "
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/jacket.imageset/Contents.json",
    "chars": 304,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"jacket.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n "
  },
  {
    "path": "OnlineStoreTCA/Assets.xcassets/tshirt.imageset/Contents.json",
    "chars": 304,
    "preview": "{\n  \"images\" : [\n    {\n      \"filename\" : \"tshirt.jpg\",\n      \"idiom\" : \"universal\",\n      \"scale\" : \"1x\"\n    },\n    {\n "
  },
  {
    "path": "OnlineStoreTCA/CartList/Cart/CartCell.swift",
    "chars": 2816,
    "preview": "//\n//  CartCell.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 22/08/22.\n//\n\nimport SwiftUI\nimport Composable"
  },
  {
    "path": "OnlineStoreTCA/CartList/Cart/CartItem.swift",
    "chars": 972,
    "preview": "//\n//  CartItem.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 18/08/22.\n//\n\nimport Foundation\n\nstruct CartIt"
  },
  {
    "path": "OnlineStoreTCA/CartList/Cart/CartItemDomain.swift",
    "chars": 572,
    "preview": "//\n//  CartItemDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 22/08/22.\n//\n\nimport Foundation\nimport C"
  },
  {
    "path": "OnlineStoreTCA/CartList/CartListDomain.swift",
    "chars": 5524,
    "preview": "//\n//  CartListDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 18/08/22.\n//\n\nimport Foundation\nimport C"
  },
  {
    "path": "OnlineStoreTCA/CartList/CartListView.swift",
    "chars": 3907,
    "preview": "//\n//  CartListView.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 18/08/22.\n//\n\nimport SwiftUI\nimport Compos"
  },
  {
    "path": "OnlineStoreTCA/CartList/Test/CartListDomainTest.swift",
    "chars": 6408,
    "preview": "//\n//  CartListDomainTest.swift\n//  OnlineStoreTCATests\n//\n//  Created by Pedro Rojas on 30/08/22.\n//\n\nimport Composable"
  },
  {
    "path": "OnlineStoreTCA/Network/APIClient.swift",
    "chars": 2212,
    "preview": "//\n//  APIClient.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 23/08/22.\n//\n\nimport Foundation\nimport Compos"
  },
  {
    "path": "OnlineStoreTCA/Network/DataLoadingStatus.swift",
    "chars": 212,
    "preview": "//\n//  DataLoadingStatus.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 28/08/22.\n//\n\nimport Foundation\n\nenum"
  },
  {
    "path": "OnlineStoreTCA/Preview Content/Preview Assets.xcassets/Contents.json",
    "chars": 63,
    "preview": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "OnlineStoreTCA/Products/AddToCart/AddToCartButton.swift",
    "chars": 1055,
    "preview": "//\n//  AddToCartButton.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 20/08/22.\n//\n\nimport SwiftUI\nimport Com"
  },
  {
    "path": "OnlineStoreTCA/Products/AddToCart/AddToCartDomain.swift",
    "chars": 679,
    "preview": "//\n//  PlusMinusDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 20/08/22.\n//\n\nimport Foundation\nimport "
  },
  {
    "path": "OnlineStoreTCA/Products/AddToCart/PlusMinusButton.swift",
    "chars": 1432,
    "preview": "//\n//  PlusMinusButton.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 20/08/22.\n//\n\nimport SwiftUI\nimport Com"
  },
  {
    "path": "OnlineStoreTCA/Products/AddToCart/Test/AddToCartDomainTest.swift",
    "chars": 2074,
    "preview": "//\n//  AddToCartDomainTest.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 26/08/22.\n//\n\nimport ComposableArch"
  },
  {
    "path": "OnlineStoreTCA/Products/Product/Product.swift",
    "chars": 2948,
    "preview": "//\n//  Product.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 17/08/22.\n//\n\nimport Foundation\n\nstruct Product"
  },
  {
    "path": "OnlineStoreTCA/Products/Product/ProductCell.swift",
    "chars": 1895,
    "preview": "//\n//  ProductCell.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 20/08/22.\n//\n\nimport SwiftUI\nimport Composa"
  },
  {
    "path": "OnlineStoreTCA/Products/Product/ProductDomain.swift",
    "chars": 1065,
    "preview": "//\n//  ProductDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 21/08/22.\n//\n\nimport Foundation\nimport Co"
  },
  {
    "path": "OnlineStoreTCA/Products/Product/Test/ProductDomainTest.swift",
    "chars": 3940,
    "preview": "//\n//  ProductDomainTest.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 27/08/22.\n//\n\nimport ComposableArchit"
  },
  {
    "path": "OnlineStoreTCA/Products/ProductList/ErrorView.swift",
    "chars": 1036,
    "preview": "//\n//  ErrorView.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 25/08/22.\n//\n\nimport SwiftUI\n\nstruct ErrorVie"
  },
  {
    "path": "OnlineStoreTCA/Products/ProductList/ProductListDomain.swift",
    "chars": 5423,
    "preview": "//\n//  ProductListDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 17/08/22.\n//\n\nimport Foundation\nimpor"
  },
  {
    "path": "OnlineStoreTCA/Products/ProductList/ProductListView.swift",
    "chars": 2486,
    "preview": "//\n//  ProductListView.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 17/08/22.\n//\n\nimport SwiftUI\nimport Com"
  },
  {
    "path": "OnlineStoreTCA/Products/ProductList/Test/ProductListDomainTest.swift",
    "chars": 8240,
    "preview": "//\n//  ProductListDomainTest.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 27/08/22.\n//\n\nimport ComposableAr"
  },
  {
    "path": "OnlineStoreTCA/Profile/ProfileDomain.swift",
    "chars": 1697,
    "preview": "//\n//  ProfileDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 25/08/22.\n//\n\nimport Foundation\nimport Co"
  },
  {
    "path": "OnlineStoreTCA/Profile/ProfileView.swift",
    "chars": 1540,
    "preview": "//\n//  ProfileView.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 25/08/22.\n//\n\nimport SwiftUI\nimport Composa"
  },
  {
    "path": "OnlineStoreTCA/Profile/UserProfile.swift",
    "chars": 1339,
    "preview": "//\n//  UserProfile.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 25/08/22.\n//\n\nimport Foundation\n\nstruct Use"
  },
  {
    "path": "OnlineStoreTCA/Root/OnlineStoreTCAApp.swift",
    "chars": 417,
    "preview": "//\n//  OnlineStoreTCAApp.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 04/08/22.\n//\n\nimport SwiftUI\nimport C"
  },
  {
    "path": "OnlineStoreTCA/Root/RootDomain.swift",
    "chars": 1197,
    "preview": "//\n//  RootDomain.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 24/08/22.\n//\n\nimport Foundation\nimport Compo"
  },
  {
    "path": "OnlineStoreTCA/Root/RootView.swift",
    "chars": 1710,
    "preview": "//\n//  RootView.swift\n//  OnlineStoreTCA\n//\n//  Created by Pedro Rojas on 24/08/22.\n//\n\nimport SwiftUI\nimport Composable"
  },
  {
    "path": "OnlineStoreTCA.xcodeproj/project.pbxproj",
    "chars": 32290,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 55;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "OnlineStoreTCATests/OnlineStoreTCATests.swift",
    "chars": 1211,
    "preview": "//\n//  OnlineStoreTCATests.swift\n//  OnlineStoreTCATests\n//\n//  Created by Pedro Rojas on 26/08/22.\n//\n\nimport XCTest\n\nf"
  },
  {
    "path": "README.md",
    "chars": 40779,
    "preview": "# Before starting\n- This demo was implemented using version [1.15.2](https://pointfreeco.github.io/swift-composable-arch"
  }
]

About this extraction

This page contains the full source code of the pitt500/OnlineStoreTCA GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (139.6 KB), approximately 35.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!