├── .gitattributes ├── .gitignore ├── README.md ├── hello-market-client ├── hello-market-client.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── azamsharp.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcuserdata │ │ └── azamsharp.xcuserdatad │ │ ├── xcdebugger │ │ └── Breakpoints_v2.xcbkptlist │ │ └── xcschemes │ │ └── xcschememanagement.plist └── hello-market-client │ ├── .gitignore │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Controllers │ ├── AuthenticationController.swift │ └── PaymentController.swift │ ├── Custom Errors │ └── Errors.swift │ ├── Extensions │ ├── EnvironmentValues+Extensions.swift │ └── String+Extensions.swift │ ├── HelloMarketClientApp.swift │ ├── Models │ └── DTOs.swift │ ├── Networking │ └── HTTPClient.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── Screens │ ├── AddProductScreen.swift │ ├── CartScreen.swift │ ├── CheckoutScreen.swift │ ├── HomeScreen.swift │ ├── LoginScreen.swift │ ├── MyProductDetailScreen.swift │ ├── MyProductListScreen.swift │ ├── OrderConfirmationScreen.swift │ ├── OrderHistoryScreen.swift │ ├── ProductDetailScreen.swift │ ├── ProductListScreen.swift │ ├── ProfileScreen.swift │ └── RegistrationScreen.swift │ ├── Services │ └── ImageUploaderDownloader.swift │ ├── Stores │ ├── CartStore.swift │ ├── OrderStore.swift │ ├── ProductStore.swift │ └── UserStore.swift │ ├── Utilities │ └── ImagePicker.swift │ ├── Utility │ ├── Constants.swift │ └── KeychainWrapper.swift │ ├── Validators │ └── TokenValidator.swift │ ├── View Modifiers │ ├── RequireAuthentication.swift │ └── WithMessageView.swift │ └── Views │ ├── CartItemListView.swift │ ├── CartItemQuantityView.swift │ ├── CartItemView.swift │ ├── MessageView.swift │ ├── OrderItemView.swift │ └── PasswordField.swift ├── hello-market-server ├── .gitignore ├── app.js ├── config │ └── config.json ├── controllers │ ├── authenticationController.js │ ├── cartController.js │ ├── orderController.js │ ├── paymentController.js │ ├── productController.js │ └── userController.js ├── middlewares │ ├── authMiddleware.js │ └── validationErrorsMiddleware.js ├── migrations │ ├── 20240905232846-create-user.js │ ├── 20240911011904-create-product.js │ ├── 20240914024544-add-user-id-to-product.js │ ├── 20241101154232-create-cart.js │ ├── 20241101155255-create-cart-item.js │ ├── 20241101160721-add-is-active-to-carts.js │ ├── 20241101172947-modify-cartitems-timestamps.js │ ├── 20241119213137-add-columns-to-users.js │ ├── 20241202002415-create-order.js │ └── 20241202003211-create-order-item.js ├── models │ ├── cart.js │ ├── cartitem.js │ ├── index.js │ ├── order.js │ ├── orderitem.js │ ├── product.js │ └── user.js ├── package-lock.json ├── package.json ├── public │ └── images │ │ ├── chair.jpg │ │ └── sofa.jpg ├── routes │ ├── auth.js │ ├── cart.js │ ├── order.js │ ├── payment.js │ ├── product.js │ └── user.js ├── uploads │ ├── image-1740321897379.png │ ├── image-1740323920514.png │ ├── image-1740323928212.png │ ├── image-1740323941520.png │ ├── image-1740324041116.png │ ├── image-1740324062515.png │ ├── image-1740324220919.png │ ├── image-1740324417512.png │ └── image-1740324776665.png └── utils │ ├── fileUtils.js │ └── validators │ └── validators.js ├── package-lock.json └── resources ├── e-commerce_logo.png ├── hellomarket.png └── logo.jpeg /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## HelloMarket 2 | 3 | ![HelloMarket Logo](/resources/logo.jpeg) 4 | 5 | **NOTE: This project is currently under development.** 6 | 7 | This repository is part of my course. If you are interested in learning how to build Smart Shop E-Commerce store from scratch then check out my course [Full Stack E-Commerce App Development with SwiftUI, Node.js and Postgres](https://azamsharp.teachable.com/p/full-stack-e-commerce-app-development-with-swiftui-node-js-and-postgres). 8 | 9 | 10 | 11 | HelloMarket, a full-stack e-commerce app using SwiftUI, Node.js, and Postgres. Develop key features such as user authentication, product management, order processing, and seamless Stripe integration for payments. 12 | 13 | Technologies and Frameworks 14 | - SwiftUI 15 | - NodeJS (ExpressJS) 16 | - Postgres (Database) 17 | - Sequelize (ORM) 18 | 19 | ### Setting Up the Backend: 20 | 21 | First, make sure that you have Node.js installed. You can install Node.js by following the instructions at [nodejs.org](https://nodejs.org/en). 22 | 23 | Once Node.js is installed, navigate to the `hello-market-server` folder and run: 24 | 25 | ``` 26 | npm install 27 | ``` 28 | 29 | This will install all the required packages and dependencies. 30 | 31 | Next, you need to set up your database. For HelloMarket, we use a PostgreSQL database. The easiest way to install PostgreSQL on your machine is by using the [Postgres App](https://postgresapp.com/). Once the Postgres App is installed, open it and initialize the database. You may see a few databases already created. Simply double-click on any of them, and it will open the PostgreSQL command line. 32 | 33 | From the command line, you can create a new database by running the following command: 34 | 35 | ``` 36 | CREATE DATABASE hellomarketdb; 37 | ``` 38 | 39 | Press Enter to execute the command. This will create the `hellomarketdb` database on your local machine. There are many tools available to visualize the database; I recommend using [BeeKeeper Community Edition](https://github.com/beekeeper-studio/beekeeper-studio). 40 | 41 | After creating the database, you can go inside the hello-market-server folder and run `npm install`. This will install all the required packages for your server. Once, all the packages are installed you need to configure the database using the following settings. This should be in ```config.json``` file, which resides inside the ```config``` folder. 42 | 43 | You can use the following configuration to connect BeeKeeper to the database: 44 | 45 | ```json 46 | "development": { 47 | "username": "postgres", 48 | "password": null, 49 | "database": "hellomarketdb", 50 | "host": "127.0.0.1", 51 | "dialect": "postgres" 52 | } 53 | ``` 54 | 55 | Next, to create all the required tables you can run sequelize db:migrate from the terminal from inside the hello-market-server folder. This will make sure you have all the tables required for the app. 56 | 57 | To run your server, make sure you are inside the `hello-market-server` folder, then run: 58 | 59 | ``` 60 | node app.js 61 | ``` 62 | 63 | Alternatively, if you have [Nodemon](https://www.npmjs.com/package/nodemon) installed, you can run: 64 | 65 | ``` 66 | nodemon app.js 67 | ``` 68 | 69 | Nodemon will restart the server automatically whenever changes are detected. 70 | 71 | 72 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8D0FC8B32C90E03100767285 /* (null) in Sources */ = {isa = PBXBuildFile; }; 11 | 8D1F69E22CFE3726007DD3D6 /* .env in Resources */ = {isa = PBXBuildFile; fileRef = 8D1F69E12CFE3722007DD3D6 /* .env */; }; 12 | 8D20503F2CACC09C0088CFAF /* JWTDecode in Frameworks */ = {isa = PBXBuildFile; productRef = 8D20503E2CACC09C0088CFAF /* JWTDecode */; }; 13 | 8D3EE22F2C92698B004E4B6D /* (null) in Sources */ = {isa = PBXBuildFile; }; 14 | 8D3EE2322C927112004E4B6D /* (null) in Sources */ = {isa = PBXBuildFile; }; 15 | 8D3EE2352C927136004E4B6D /* (null) in Sources */ = {isa = PBXBuildFile; }; 16 | 8D3EE2392C92819C004E4B6D /* HomeScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE2372C92819C004E4B6D /* HomeScreen.swift */; }; 17 | 8D3EE23A2C92819C004E4B6D /* ProductListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE2382C92819C004E4B6D /* ProductListScreen.swift */; }; 18 | 8D3EE23C2C9281AA004E4B6D /* ProductStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE23B2C9281AA004E4B6D /* ProductStore.swift */; }; 19 | 8D3EE23E2C951CD2004E4B6D /* ProductDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE23D2C951CD2004E4B6D /* ProductDetailScreen.swift */; }; 20 | 8D3EE2402C953384004E4B6D /* MyProductListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE23F2C953384004E4B6D /* MyProductListScreen.swift */; }; 21 | 8D3EE2422C968D59004E4B6D /* AddProductScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3EE2412C968D59004E4B6D /* AddProductScreen.swift */; }; 22 | 8D405AF32CD6B383004BCF0E /* CartScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D405AF22CD6B383004BCF0E /* CartScreen.swift */; }; 23 | 8D405AF52CD6C519004BCF0E /* CartItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D405AF42CD6C519004BCF0E /* CartItemView.swift */; }; 24 | 8D4536982CED23C200BD7BFF /* CheckoutScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D4536972CED23C200BD7BFF /* CheckoutScreen.swift */; }; 25 | 8D5B74462CAF70C200AA4BAC /* JWTDecode in Frameworks */ = {isa = PBXBuildFile; productRef = 8D5B74452CAF70C200AA4BAC /* JWTDecode */; }; 26 | 8D5B74482CAF737200AA4BAC /* ProfileScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5B74472CAF737200AA4BAC /* ProfileScreen.swift */; }; 27 | 8D5B744A2CAF769800AA4BAC /* RequireAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5B74492CAF769800AA4BAC /* RequireAuthentication.swift */; }; 28 | 8D5FE4852CB968A7000F47C8 /* ImageUploaderDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5FE4842CB968A7000F47C8 /* ImageUploaderDownloader.swift */; }; 29 | 8D6668562CD676C0001E1D52 /* CartStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D6668552CD676C0001E1D52 /* CartStore.swift */; }; 30 | 8D7B081D2C8BEFB0003D7E91 /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D7B081C2C8BEFB0003D7E91 /* MessageView.swift */; }; 31 | 8D80217D2CFCF28E006C709C /* Stripe in Frameworks */ = {isa = PBXBuildFile; productRef = 8D80217C2CFCF28E006C709C /* Stripe */; }; 32 | 8D80217F2CFCF28E006C709C /* StripeApplePay in Frameworks */ = {isa = PBXBuildFile; productRef = 8D80217E2CFCF28E006C709C /* StripeApplePay */; }; 33 | 8D8021812CFCF28E006C709C /* StripeCardScan in Frameworks */ = {isa = PBXBuildFile; productRef = 8D8021802CFCF28E006C709C /* StripeCardScan */; }; 34 | 8D8021832CFCF28E006C709C /* StripeFinancialConnections in Frameworks */ = {isa = PBXBuildFile; productRef = 8D8021822CFCF28E006C709C /* StripeFinancialConnections */; }; 35 | 8D8021852CFCF28E006C709C /* StripeIdentity in Frameworks */ = {isa = PBXBuildFile; productRef = 8D8021842CFCF28E006C709C /* StripeIdentity */; }; 36 | 8D8021872CFCF28E006C709C /* StripePaymentSheet in Frameworks */ = {isa = PBXBuildFile; productRef = 8D8021862CFCF28E006C709C /* StripePaymentSheet */; }; 37 | 8D8021892CFCF28E006C709C /* StripePayments in Frameworks */ = {isa = PBXBuildFile; productRef = 8D8021882CFCF28E006C709C /* StripePayments */; }; 38 | 8D80218B2CFCF28F006C709C /* StripePaymentsUI in Frameworks */ = {isa = PBXBuildFile; productRef = 8D80218A2CFCF28F006C709C /* StripePaymentsUI */; }; 39 | 8D8021922CFD27EE006C709C /* PaymentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8021912CFD27EB006C709C /* PaymentController.swift */; }; 40 | 8D8021982CFD337E006C709C /* OrderStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8021972CFD337C006C709C /* OrderStore.swift */; }; 41 | 8D80219A2CFD69C0006C709C /* OrderConfirmationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D8021992CFD69C0006C709C /* OrderConfirmationScreen.swift */; }; 42 | 8D906AD92CF22288003D382A /* UserStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D906AD82CF22288003D382A /* UserStore.swift */; }; 43 | 8D995CA12C8A9996003AFA2C /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D995CA02C8A9996003AFA2C /* String+Extensions.swift */; }; 44 | 8D995CA32C8AA1FA003AFA2C /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D995CA22C8AA1FA003AFA2C /* LoginScreen.swift */; }; 45 | 8D995CA72C8B369D003AFA2C /* KeychainWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D995CA62C8B369D003AFA2C /* KeychainWrapper.swift */; }; 46 | 8D995CAA2C8B374D003AFA2C /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D995CA92C8B374D003AFA2C /* Errors.swift */; }; 47 | 8D995CB02C8CE054003AFA2C /* WithMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D995CAF2C8CE054003AFA2C /* WithMessageView.swift */; }; 48 | 8D9FFECA2CFE08C300942B57 /* OrderHistoryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9FFEC92CFE08C300942B57 /* OrderHistoryScreen.swift */; }; 49 | 8D9FFECC2CFE100700942B57 /* OrderItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D9FFECB2CFE100700942B57 /* OrderItemView.swift */; }; 50 | 8DAD14C42CC3F8BB00417FE7 /* MyProductDetailScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DAD14C32CC3F8BB00417FE7 /* MyProductDetailScreen.swift */; }; 51 | 8DBB77FB2CD6CAD700B0C3E0 /* CartItemListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBB77FA2CD6CAD700B0C3E0 /* CartItemListView.swift */; }; 52 | 8DBEF9DE2CD8710F00B24511 /* CartItemQuantityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DBEF9DD2CD8710F00B24511 /* CartItemQuantityView.swift */; }; 53 | 8DDE5FB52C8A7D9300A0DBE0 /* HelloMarketClientApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FB42C8A7D9300A0DBE0 /* HelloMarketClientApp.swift */; }; 54 | 8DDE5FB92C8A7D9400A0DBE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DDE5FB82C8A7D9400A0DBE0 /* Assets.xcassets */; }; 55 | 8DDE5FBC2C8A7D9400A0DBE0 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8DDE5FBB2C8A7D9400A0DBE0 /* Preview Assets.xcassets */; }; 56 | 8DDE5FC42C8A7DB500A0DBE0 /* RegistrationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FC32C8A7DB500A0DBE0 /* RegistrationScreen.swift */; }; 57 | 8DDE5FC72C8A7DC700A0DBE0 /* AuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FC62C8A7DC700A0DBE0 /* AuthenticationController.swift */; }; 58 | 8DDE5FCA2C8A7E3700A0DBE0 /* HTTPClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FC92C8A7E3700A0DBE0 /* HTTPClient.swift */; }; 59 | 8DDE5FCD2C8A7FE900A0DBE0 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FCC2C8A7FE900A0DBE0 /* Constants.swift */; }; 60 | 8DDE5FD02C8A811A00A0DBE0 /* DTOs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDE5FCF2C8A811A00A0DBE0 /* DTOs.swift */; }; 61 | 8DDEA1A82CC9C8B000D24D3B /* PasswordField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DDEA1A72CC9C8B000D24D3B /* PasswordField.swift */; }; 62 | 8DE398FF2D0E141800AA3029 /* EnvironmentValues+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE398FE2D0E141800AA3029 /* EnvironmentValues+Extensions.swift */; }; 63 | 8DEC814F2CAED9DD006C4DA5 /* TokenValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DEC814E2CAED9DD006C4DA5 /* TokenValidator.swift */; }; 64 | 8DF13AF02C99262700898DFF /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF13AEF2C99262700898DFF /* ImagePicker.swift */; }; 65 | /* End PBXBuildFile section */ 66 | 67 | /* Begin PBXFileReference section */ 68 | 8D1F69E12CFE3722007DD3D6 /* .env */ = {isa = PBXFileReference; lastKnownFileType = text; path = .env; sourceTree = ""; }; 69 | 8D3EE2372C92819C004E4B6D /* HomeScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreen.swift; sourceTree = ""; }; 70 | 8D3EE2382C92819C004E4B6D /* ProductListScreen.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductListScreen.swift; sourceTree = ""; }; 71 | 8D3EE23B2C9281AA004E4B6D /* ProductStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductStore.swift; sourceTree = ""; }; 72 | 8D3EE23D2C951CD2004E4B6D /* ProductDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductDetailScreen.swift; sourceTree = ""; }; 73 | 8D3EE23F2C953384004E4B6D /* MyProductListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProductListScreen.swift; sourceTree = ""; }; 74 | 8D3EE2412C968D59004E4B6D /* AddProductScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddProductScreen.swift; sourceTree = ""; }; 75 | 8D405AF22CD6B383004BCF0E /* CartScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartScreen.swift; sourceTree = ""; }; 76 | 8D405AF42CD6C519004BCF0E /* CartItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItemView.swift; sourceTree = ""; }; 77 | 8D4536972CED23C200BD7BFF /* CheckoutScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutScreen.swift; sourceTree = ""; }; 78 | 8D5B74472CAF737200AA4BAC /* ProfileScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileScreen.swift; sourceTree = ""; }; 79 | 8D5B74492CAF769800AA4BAC /* RequireAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequireAuthentication.swift; sourceTree = ""; }; 80 | 8D5FE4842CB968A7000F47C8 /* ImageUploaderDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploaderDownloader.swift; sourceTree = ""; }; 81 | 8D6668552CD676C0001E1D52 /* CartStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartStore.swift; sourceTree = ""; }; 82 | 8D7B081C2C8BEFB0003D7E91 /* MessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageView.swift; sourceTree = ""; }; 83 | 8D8021912CFD27EB006C709C /* PaymentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentController.swift; sourceTree = ""; }; 84 | 8D8021972CFD337C006C709C /* OrderStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderStore.swift; sourceTree = ""; }; 85 | 8D8021992CFD69C0006C709C /* OrderConfirmationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderConfirmationScreen.swift; sourceTree = ""; }; 86 | 8D906AD82CF22288003D382A /* UserStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStore.swift; sourceTree = ""; }; 87 | 8D995CA02C8A9996003AFA2C /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 88 | 8D995CA22C8AA1FA003AFA2C /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; 89 | 8D995CA62C8B369D003AFA2C /* KeychainWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainWrapper.swift; sourceTree = ""; }; 90 | 8D995CA92C8B374D003AFA2C /* Errors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Errors.swift; sourceTree = ""; }; 91 | 8D995CAF2C8CE054003AFA2C /* WithMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithMessageView.swift; sourceTree = ""; }; 92 | 8D9FFEC92CFE08C300942B57 /* OrderHistoryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderHistoryScreen.swift; sourceTree = ""; }; 93 | 8D9FFECB2CFE100700942B57 /* OrderItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderItemView.swift; sourceTree = ""; }; 94 | 8DAD14C32CC3F8BB00417FE7 /* MyProductDetailScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProductDetailScreen.swift; sourceTree = ""; }; 95 | 8DBB77FA2CD6CAD700B0C3E0 /* CartItemListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItemListView.swift; sourceTree = ""; }; 96 | 8DBEF9DD2CD8710F00B24511 /* CartItemQuantityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CartItemQuantityView.swift; sourceTree = ""; }; 97 | 8DDE5FB12C8A7D9300A0DBE0 /* hello-market-client.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "hello-market-client.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 98 | 8DDE5FB42C8A7D9300A0DBE0 /* HelloMarketClientApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelloMarketClientApp.swift; sourceTree = ""; }; 99 | 8DDE5FB82C8A7D9400A0DBE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 100 | 8DDE5FBB2C8A7D9400A0DBE0 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 101 | 8DDE5FC32C8A7DB500A0DBE0 /* RegistrationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationScreen.swift; sourceTree = ""; }; 102 | 8DDE5FC62C8A7DC700A0DBE0 /* AuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationController.swift; sourceTree = ""; }; 103 | 8DDE5FC92C8A7E3700A0DBE0 /* HTTPClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPClient.swift; sourceTree = ""; }; 104 | 8DDE5FCC2C8A7FE900A0DBE0 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 105 | 8DDE5FCF2C8A811A00A0DBE0 /* DTOs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DTOs.swift; sourceTree = ""; }; 106 | 8DDEA1A72CC9C8B000D24D3B /* PasswordField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordField.swift; sourceTree = ""; }; 107 | 8DE398FE2D0E141800AA3029 /* EnvironmentValues+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EnvironmentValues+Extensions.swift"; sourceTree = ""; }; 108 | 8DEC814E2CAED9DD006C4DA5 /* TokenValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenValidator.swift; sourceTree = ""; }; 109 | 8DF13AEF2C99262700898DFF /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; 110 | /* End PBXFileReference section */ 111 | 112 | /* Begin PBXFrameworksBuildPhase section */ 113 | 8DDE5FAE2C8A7D9300A0DBE0 /* Frameworks */ = { 114 | isa = PBXFrameworksBuildPhase; 115 | buildActionMask = 2147483647; 116 | files = ( 117 | 8D8021892CFCF28E006C709C /* StripePayments in Frameworks */, 118 | 8D8021872CFCF28E006C709C /* StripePaymentSheet in Frameworks */, 119 | 8D5B74462CAF70C200AA4BAC /* JWTDecode in Frameworks */, 120 | 8D8021832CFCF28E006C709C /* StripeFinancialConnections in Frameworks */, 121 | 8D80217F2CFCF28E006C709C /* StripeApplePay in Frameworks */, 122 | 8D8021852CFCF28E006C709C /* StripeIdentity in Frameworks */, 123 | 8D20503F2CACC09C0088CFAF /* JWTDecode in Frameworks */, 124 | 8D8021812CFCF28E006C709C /* StripeCardScan in Frameworks */, 125 | 8D80218B2CFCF28F006C709C /* StripePaymentsUI in Frameworks */, 126 | 8D80217D2CFCF28E006C709C /* Stripe in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | /* End PBXFrameworksBuildPhase section */ 131 | 132 | /* Begin PBXGroup section */ 133 | 8D3EE2332C927129004E4B6D /* Stores */ = { 134 | isa = PBXGroup; 135 | children = ( 136 | 8D8021972CFD337C006C709C /* OrderStore.swift */, 137 | 8D3EE23B2C9281AA004E4B6D /* ProductStore.swift */, 138 | 8D6668552CD676C0001E1D52 /* CartStore.swift */, 139 | 8D906AD82CF22288003D382A /* UserStore.swift */, 140 | ); 141 | path = Stores; 142 | sourceTree = ""; 143 | }; 144 | 8D5FE4832CB96895000F47C8 /* Services */ = { 145 | isa = PBXGroup; 146 | children = ( 147 | 8D5FE4842CB968A7000F47C8 /* ImageUploaderDownloader.swift */, 148 | ); 149 | path = Services; 150 | sourceTree = ""; 151 | }; 152 | 8D7B08182C8BEBCF003D7E91 /* View Modifiers */ = { 153 | isa = PBXGroup; 154 | children = ( 155 | 8D995CAF2C8CE054003AFA2C /* WithMessageView.swift */, 156 | 8D5B74492CAF769800AA4BAC /* RequireAuthentication.swift */, 157 | ); 158 | path = "View Modifiers"; 159 | sourceTree = ""; 160 | }; 161 | 8D7B081B2C8BEFA7003D7E91 /* Views */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 8D7B081C2C8BEFB0003D7E91 /* MessageView.swift */, 165 | 8DDEA1A72CC9C8B000D24D3B /* PasswordField.swift */, 166 | 8D405AF42CD6C519004BCF0E /* CartItemView.swift */, 167 | 8DBB77FA2CD6CAD700B0C3E0 /* CartItemListView.swift */, 168 | 8DBEF9DD2CD8710F00B24511 /* CartItemQuantityView.swift */, 169 | 8D9FFECB2CFE100700942B57 /* OrderItemView.swift */, 170 | ); 171 | path = Views; 172 | sourceTree = ""; 173 | }; 174 | 8D995C9F2C8A998A003AFA2C /* Extensions */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 8D995CA02C8A9996003AFA2C /* String+Extensions.swift */, 178 | 8DE398FE2D0E141800AA3029 /* EnvironmentValues+Extensions.swift */, 179 | ); 180 | path = Extensions; 181 | sourceTree = ""; 182 | }; 183 | 8D995CA82C8B373B003AFA2C /* Custom Errors */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 8D995CA92C8B374D003AFA2C /* Errors.swift */, 187 | ); 188 | path = "Custom Errors"; 189 | sourceTree = ""; 190 | }; 191 | 8DDE5FA82C8A7D9300A0DBE0 = { 192 | isa = PBXGroup; 193 | children = ( 194 | 8DDE5FB32C8A7D9300A0DBE0 /* hello-market-client */, 195 | 8DDE5FB22C8A7D9300A0DBE0 /* Products */, 196 | ); 197 | sourceTree = ""; 198 | }; 199 | 8DDE5FB22C8A7D9300A0DBE0 /* Products */ = { 200 | isa = PBXGroup; 201 | children = ( 202 | 8DDE5FB12C8A7D9300A0DBE0 /* hello-market-client.app */, 203 | ); 204 | name = Products; 205 | sourceTree = ""; 206 | }; 207 | 8DDE5FB32C8A7D9300A0DBE0 /* hello-market-client */ = { 208 | isa = PBXGroup; 209 | children = ( 210 | 8D1F69E12CFE3722007DD3D6 /* .env */, 211 | 8D5FE4832CB96895000F47C8 /* Services */, 212 | 8DEC814D2CAED9D3006C4DA5 /* Validators */, 213 | 8DF13AEE2C99261D00898DFF /* Utilities */, 214 | 8D3EE2332C927129004E4B6D /* Stores */, 215 | 8D7B081B2C8BEFA7003D7E91 /* Views */, 216 | 8D7B08182C8BEBCF003D7E91 /* View Modifiers */, 217 | 8D995CA82C8B373B003AFA2C /* Custom Errors */, 218 | 8D995C9F2C8A998A003AFA2C /* Extensions */, 219 | 8DDE5FCE2C8A810E00A0DBE0 /* Models */, 220 | 8DDE5FCB2C8A7FDE00A0DBE0 /* Utility */, 221 | 8DDE5FC82C8A7E2B00A0DBE0 /* Networking */, 222 | 8DDE5FC52C8A7DB900A0DBE0 /* Controllers */, 223 | 8DDE5FC22C8A7DA400A0DBE0 /* Screens */, 224 | 8DDE5FB42C8A7D9300A0DBE0 /* HelloMarketClientApp.swift */, 225 | 8DDE5FB82C8A7D9400A0DBE0 /* Assets.xcassets */, 226 | 8DDE5FBA2C8A7D9400A0DBE0 /* Preview Content */, 227 | ); 228 | path = "hello-market-client"; 229 | sourceTree = ""; 230 | }; 231 | 8DDE5FBA2C8A7D9400A0DBE0 /* Preview Content */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 8DDE5FBB2C8A7D9400A0DBE0 /* Preview Assets.xcassets */, 235 | ); 236 | path = "Preview Content"; 237 | sourceTree = ""; 238 | }; 239 | 8DDE5FC22C8A7DA400A0DBE0 /* Screens */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | 8D3EE2372C92819C004E4B6D /* HomeScreen.swift */, 243 | 8D3EE2382C92819C004E4B6D /* ProductListScreen.swift */, 244 | 8DDE5FC32C8A7DB500A0DBE0 /* RegistrationScreen.swift */, 245 | 8D995CA22C8AA1FA003AFA2C /* LoginScreen.swift */, 246 | 8D3EE23D2C951CD2004E4B6D /* ProductDetailScreen.swift */, 247 | 8D3EE23F2C953384004E4B6D /* MyProductListScreen.swift */, 248 | 8D3EE2412C968D59004E4B6D /* AddProductScreen.swift */, 249 | 8D5B74472CAF737200AA4BAC /* ProfileScreen.swift */, 250 | 8DAD14C32CC3F8BB00417FE7 /* MyProductDetailScreen.swift */, 251 | 8D405AF22CD6B383004BCF0E /* CartScreen.swift */, 252 | 8D4536972CED23C200BD7BFF /* CheckoutScreen.swift */, 253 | 8D8021992CFD69C0006C709C /* OrderConfirmationScreen.swift */, 254 | 8D9FFEC92CFE08C300942B57 /* OrderHistoryScreen.swift */, 255 | ); 256 | path = Screens; 257 | sourceTree = ""; 258 | }; 259 | 8DDE5FC52C8A7DB900A0DBE0 /* Controllers */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | 8DDE5FC62C8A7DC700A0DBE0 /* AuthenticationController.swift */, 263 | 8D8021912CFD27EB006C709C /* PaymentController.swift */, 264 | ); 265 | path = Controllers; 266 | sourceTree = ""; 267 | }; 268 | 8DDE5FC82C8A7E2B00A0DBE0 /* Networking */ = { 269 | isa = PBXGroup; 270 | children = ( 271 | 8DDE5FC92C8A7E3700A0DBE0 /* HTTPClient.swift */, 272 | ); 273 | path = Networking; 274 | sourceTree = ""; 275 | }; 276 | 8DDE5FCB2C8A7FDE00A0DBE0 /* Utility */ = { 277 | isa = PBXGroup; 278 | children = ( 279 | 8DDE5FCC2C8A7FE900A0DBE0 /* Constants.swift */, 280 | 8D995CA62C8B369D003AFA2C /* KeychainWrapper.swift */, 281 | ); 282 | path = Utility; 283 | sourceTree = ""; 284 | }; 285 | 8DDE5FCE2C8A810E00A0DBE0 /* Models */ = { 286 | isa = PBXGroup; 287 | children = ( 288 | 8DDE5FCF2C8A811A00A0DBE0 /* DTOs.swift */, 289 | ); 290 | path = Models; 291 | sourceTree = ""; 292 | }; 293 | 8DEC814D2CAED9D3006C4DA5 /* Validators */ = { 294 | isa = PBXGroup; 295 | children = ( 296 | 8DEC814E2CAED9DD006C4DA5 /* TokenValidator.swift */, 297 | ); 298 | path = Validators; 299 | sourceTree = ""; 300 | }; 301 | 8DF13AEE2C99261D00898DFF /* Utilities */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | 8DF13AEF2C99262700898DFF /* ImagePicker.swift */, 305 | ); 306 | path = Utilities; 307 | sourceTree = ""; 308 | }; 309 | /* End PBXGroup section */ 310 | 311 | /* Begin PBXNativeTarget section */ 312 | 8DDE5FB02C8A7D9300A0DBE0 /* hello-market-client */ = { 313 | isa = PBXNativeTarget; 314 | buildConfigurationList = 8DDE5FBF2C8A7D9400A0DBE0 /* Build configuration list for PBXNativeTarget "hello-market-client" */; 315 | buildPhases = ( 316 | 8DDE5FAD2C8A7D9300A0DBE0 /* Sources */, 317 | 8DDE5FAE2C8A7D9300A0DBE0 /* Frameworks */, 318 | 8DDE5FAF2C8A7D9300A0DBE0 /* Resources */, 319 | ); 320 | buildRules = ( 321 | ); 322 | dependencies = ( 323 | ); 324 | name = "hello-market-client"; 325 | packageProductDependencies = ( 326 | 8D20503E2CACC09C0088CFAF /* JWTDecode */, 327 | 8D5B74452CAF70C200AA4BAC /* JWTDecode */, 328 | 8D80217C2CFCF28E006C709C /* Stripe */, 329 | 8D80217E2CFCF28E006C709C /* StripeApplePay */, 330 | 8D8021802CFCF28E006C709C /* StripeCardScan */, 331 | 8D8021822CFCF28E006C709C /* StripeFinancialConnections */, 332 | 8D8021842CFCF28E006C709C /* StripeIdentity */, 333 | 8D8021862CFCF28E006C709C /* StripePaymentSheet */, 334 | 8D8021882CFCF28E006C709C /* StripePayments */, 335 | 8D80218A2CFCF28F006C709C /* StripePaymentsUI */, 336 | ); 337 | productName = "hello-market-client"; 338 | productReference = 8DDE5FB12C8A7D9300A0DBE0 /* hello-market-client.app */; 339 | productType = "com.apple.product-type.application"; 340 | }; 341 | /* End PBXNativeTarget section */ 342 | 343 | /* Begin PBXProject section */ 344 | 8DDE5FA92C8A7D9300A0DBE0 /* Project object */ = { 345 | isa = PBXProject; 346 | attributes = { 347 | BuildIndependentTargetsInParallel = 1; 348 | LastSwiftUpdateCheck = 1520; 349 | LastUpgradeCheck = 1520; 350 | TargetAttributes = { 351 | 8DDE5FB02C8A7D9300A0DBE0 = { 352 | CreatedOnToolsVersion = 15.2; 353 | }; 354 | }; 355 | }; 356 | buildConfigurationList = 8DDE5FAC2C8A7D9300A0DBE0 /* Build configuration list for PBXProject "hello-market-client" */; 357 | compatibilityVersion = "Xcode 14.0"; 358 | developmentRegion = en; 359 | hasScannedForEncodings = 0; 360 | knownRegions = ( 361 | en, 362 | Base, 363 | ); 364 | mainGroup = 8DDE5FA82C8A7D9300A0DBE0; 365 | packageReferences = ( 366 | 8D20503D2CACC09C0088CFAF /* XCRemoteSwiftPackageReference "JWTDecode.swift" */, 367 | 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */, 368 | ); 369 | productRefGroup = 8DDE5FB22C8A7D9300A0DBE0 /* Products */; 370 | projectDirPath = ""; 371 | projectRoot = ""; 372 | targets = ( 373 | 8DDE5FB02C8A7D9300A0DBE0 /* hello-market-client */, 374 | ); 375 | }; 376 | /* End PBXProject section */ 377 | 378 | /* Begin PBXResourcesBuildPhase section */ 379 | 8DDE5FAF2C8A7D9300A0DBE0 /* Resources */ = { 380 | isa = PBXResourcesBuildPhase; 381 | buildActionMask = 2147483647; 382 | files = ( 383 | 8D1F69E22CFE3726007DD3D6 /* .env in Resources */, 384 | 8DDE5FBC2C8A7D9400A0DBE0 /* Preview Assets.xcassets in Resources */, 385 | 8DDE5FB92C8A7D9400A0DBE0 /* Assets.xcassets in Resources */, 386 | ); 387 | runOnlyForDeploymentPostprocessing = 0; 388 | }; 389 | /* End PBXResourcesBuildPhase section */ 390 | 391 | /* Begin PBXSourcesBuildPhase section */ 392 | 8DDE5FAD2C8A7D9300A0DBE0 /* Sources */ = { 393 | isa = PBXSourcesBuildPhase; 394 | buildActionMask = 2147483647; 395 | files = ( 396 | 8DDE5FC42C8A7DB500A0DBE0 /* RegistrationScreen.swift in Sources */, 397 | 8DDE5FCA2C8A7E3700A0DBE0 /* HTTPClient.swift in Sources */, 398 | 8DF13AF02C99262700898DFF /* ImagePicker.swift in Sources */, 399 | 8D9FFECC2CFE100700942B57 /* OrderItemView.swift in Sources */, 400 | 8D5B744A2CAF769800AA4BAC /* RequireAuthentication.swift in Sources */, 401 | 8DDEA1A82CC9C8B000D24D3B /* PasswordField.swift in Sources */, 402 | 8D906AD92CF22288003D382A /* UserStore.swift in Sources */, 403 | 8D5FE4852CB968A7000F47C8 /* ImageUploaderDownloader.swift in Sources */, 404 | 8D3EE2402C953384004E4B6D /* MyProductListScreen.swift in Sources */, 405 | 8DE398FF2D0E141800AA3029 /* EnvironmentValues+Extensions.swift in Sources */, 406 | 8D80219A2CFD69C0006C709C /* OrderConfirmationScreen.swift in Sources */, 407 | 8D995CA32C8AA1FA003AFA2C /* LoginScreen.swift in Sources */, 408 | 8D3EE22F2C92698B004E4B6D /* (null) in Sources */, 409 | 8D995CA72C8B369D003AFA2C /* KeychainWrapper.swift in Sources */, 410 | 8D995CAA2C8B374D003AFA2C /* Errors.swift in Sources */, 411 | 8D3EE23C2C9281AA004E4B6D /* ProductStore.swift in Sources */, 412 | 8D995CB02C8CE054003AFA2C /* WithMessageView.swift in Sources */, 413 | 8DBEF9DE2CD8710F00B24511 /* CartItemQuantityView.swift in Sources */, 414 | 8DDE5FC72C8A7DC700A0DBE0 /* AuthenticationController.swift in Sources */, 415 | 8D9FFECA2CFE08C300942B57 /* OrderHistoryScreen.swift in Sources */, 416 | 8D3EE2352C927136004E4B6D /* (null) in Sources */, 417 | 8D3EE2392C92819C004E4B6D /* HomeScreen.swift in Sources */, 418 | 8D6668562CD676C0001E1D52 /* CartStore.swift in Sources */, 419 | 8D995CA12C8A9996003AFA2C /* String+Extensions.swift in Sources */, 420 | 8DEC814F2CAED9DD006C4DA5 /* TokenValidator.swift in Sources */, 421 | 8D3EE23A2C92819C004E4B6D /* ProductListScreen.swift in Sources */, 422 | 8DBB77FB2CD6CAD700B0C3E0 /* CartItemListView.swift in Sources */, 423 | 8D7B081D2C8BEFB0003D7E91 /* MessageView.swift in Sources */, 424 | 8D8021982CFD337E006C709C /* OrderStore.swift in Sources */, 425 | 8DDE5FD02C8A811A00A0DBE0 /* DTOs.swift in Sources */, 426 | 8D3EE23E2C951CD2004E4B6D /* ProductDetailScreen.swift in Sources */, 427 | 8DDE5FB52C8A7D9300A0DBE0 /* HelloMarketClientApp.swift in Sources */, 428 | 8D3EE2422C968D59004E4B6D /* AddProductScreen.swift in Sources */, 429 | 8D4536982CED23C200BD7BFF /* CheckoutScreen.swift in Sources */, 430 | 8D5B74482CAF737200AA4BAC /* ProfileScreen.swift in Sources */, 431 | 8DDE5FCD2C8A7FE900A0DBE0 /* Constants.swift in Sources */, 432 | 8DAD14C42CC3F8BB00417FE7 /* MyProductDetailScreen.swift in Sources */, 433 | 8D3EE2322C927112004E4B6D /* (null) in Sources */, 434 | 8D405AF32CD6B383004BCF0E /* CartScreen.swift in Sources */, 435 | 8D405AF52CD6C519004BCF0E /* CartItemView.swift in Sources */, 436 | 8D0FC8B32C90E03100767285 /* (null) in Sources */, 437 | 8D8021922CFD27EE006C709C /* PaymentController.swift in Sources */, 438 | ); 439 | runOnlyForDeploymentPostprocessing = 0; 440 | }; 441 | /* End PBXSourcesBuildPhase section */ 442 | 443 | /* Begin XCBuildConfiguration section */ 444 | 8DDE5FBD2C8A7D9400A0DBE0 /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | ALWAYS_SEARCH_USER_PATHS = NO; 448 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 449 | CLANG_ANALYZER_NONNULL = YES; 450 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 451 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 452 | CLANG_ENABLE_MODULES = YES; 453 | CLANG_ENABLE_OBJC_ARC = YES; 454 | CLANG_ENABLE_OBJC_WEAK = YES; 455 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 456 | CLANG_WARN_BOOL_CONVERSION = YES; 457 | CLANG_WARN_COMMA = YES; 458 | CLANG_WARN_CONSTANT_CONVERSION = YES; 459 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 460 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 461 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 462 | CLANG_WARN_EMPTY_BODY = YES; 463 | CLANG_WARN_ENUM_CONVERSION = YES; 464 | CLANG_WARN_INFINITE_RECURSION = YES; 465 | CLANG_WARN_INT_CONVERSION = YES; 466 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 467 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 468 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 469 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 470 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 471 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 472 | CLANG_WARN_STRICT_PROTOTYPES = YES; 473 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 474 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 475 | CLANG_WARN_UNREACHABLE_CODE = YES; 476 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 477 | COPY_PHASE_STRIP = NO; 478 | DEBUG_INFORMATION_FORMAT = dwarf; 479 | ENABLE_STRICT_OBJC_MSGSEND = YES; 480 | ENABLE_TESTABILITY = YES; 481 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 482 | GCC_C_LANGUAGE_STANDARD = gnu17; 483 | GCC_DYNAMIC_NO_PIC = NO; 484 | GCC_NO_COMMON_BLOCKS = YES; 485 | GCC_OPTIMIZATION_LEVEL = 0; 486 | GCC_PREPROCESSOR_DEFINITIONS = ( 487 | "DEBUG=1", 488 | "$(inherited)", 489 | ); 490 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 491 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 492 | GCC_WARN_UNDECLARED_SELECTOR = YES; 493 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 494 | GCC_WARN_UNUSED_FUNCTION = YES; 495 | GCC_WARN_UNUSED_VARIABLE = YES; 496 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 497 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 498 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 499 | MTL_FAST_MATH = YES; 500 | ONLY_ACTIVE_ARCH = YES; 501 | SDKROOT = iphoneos; 502 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 503 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 504 | }; 505 | name = Debug; 506 | }; 507 | 8DDE5FBE2C8A7D9400A0DBE0 /* Release */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | ALWAYS_SEARCH_USER_PATHS = NO; 511 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 512 | CLANG_ANALYZER_NONNULL = YES; 513 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 514 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 515 | CLANG_ENABLE_MODULES = YES; 516 | CLANG_ENABLE_OBJC_ARC = YES; 517 | CLANG_ENABLE_OBJC_WEAK = YES; 518 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 519 | CLANG_WARN_BOOL_CONVERSION = YES; 520 | CLANG_WARN_COMMA = YES; 521 | CLANG_WARN_CONSTANT_CONVERSION = YES; 522 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 523 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 524 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 525 | CLANG_WARN_EMPTY_BODY = YES; 526 | CLANG_WARN_ENUM_CONVERSION = YES; 527 | CLANG_WARN_INFINITE_RECURSION = YES; 528 | CLANG_WARN_INT_CONVERSION = YES; 529 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 530 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 531 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 532 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 533 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 534 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 535 | CLANG_WARN_STRICT_PROTOTYPES = YES; 536 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 537 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 538 | CLANG_WARN_UNREACHABLE_CODE = YES; 539 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 540 | COPY_PHASE_STRIP = NO; 541 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 542 | ENABLE_NS_ASSERTIONS = NO; 543 | ENABLE_STRICT_OBJC_MSGSEND = YES; 544 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 545 | GCC_C_LANGUAGE_STANDARD = gnu17; 546 | GCC_NO_COMMON_BLOCKS = YES; 547 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 548 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 549 | GCC_WARN_UNDECLARED_SELECTOR = YES; 550 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 551 | GCC_WARN_UNUSED_FUNCTION = YES; 552 | GCC_WARN_UNUSED_VARIABLE = YES; 553 | IPHONEOS_DEPLOYMENT_TARGET = 17.2; 554 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 555 | MTL_ENABLE_DEBUG_INFO = NO; 556 | MTL_FAST_MATH = YES; 557 | SDKROOT = iphoneos; 558 | SWIFT_COMPILATION_MODE = wholemodule; 559 | VALIDATE_PRODUCT = YES; 560 | }; 561 | name = Release; 562 | }; 563 | 8DDE5FC02C8A7D9400A0DBE0 /* Debug */ = { 564 | isa = XCBuildConfiguration; 565 | buildSettings = { 566 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 567 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 568 | CODE_SIGN_STYLE = Automatic; 569 | CURRENT_PROJECT_VERSION = 1; 570 | DEVELOPMENT_ASSET_PATHS = "\"hello-market-client/Preview Content\""; 571 | DEVELOPMENT_TEAM = B2Q8EGNCQA; 572 | ENABLE_PREVIEWS = YES; 573 | GENERATE_INFOPLIST_FILE = YES; 574 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 575 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 576 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 577 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 578 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 579 | IPHONEOS_DEPLOYMENT_TARGET = 18; 580 | LD_RUNPATH_SEARCH_PATHS = ( 581 | "$(inherited)", 582 | "@executable_path/Frameworks", 583 | ); 584 | MARKETING_VERSION = 1.0; 585 | PRODUCT_BUNDLE_IDENTIFIER = "com.azamsharp.hello-market-client"; 586 | PRODUCT_NAME = "$(TARGET_NAME)"; 587 | SWIFT_EMIT_LOC_STRINGS = YES; 588 | SWIFT_VERSION = 6.0; 589 | TARGETED_DEVICE_FAMILY = "1,2"; 590 | }; 591 | name = Debug; 592 | }; 593 | 8DDE5FC12C8A7D9400A0DBE0 /* Release */ = { 594 | isa = XCBuildConfiguration; 595 | buildSettings = { 596 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 597 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 598 | CODE_SIGN_STYLE = Automatic; 599 | CURRENT_PROJECT_VERSION = 1; 600 | DEVELOPMENT_ASSET_PATHS = "\"hello-market-client/Preview Content\""; 601 | DEVELOPMENT_TEAM = B2Q8EGNCQA; 602 | ENABLE_PREVIEWS = YES; 603 | GENERATE_INFOPLIST_FILE = YES; 604 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 605 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 606 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 607 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 608 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 609 | IPHONEOS_DEPLOYMENT_TARGET = 18; 610 | LD_RUNPATH_SEARCH_PATHS = ( 611 | "$(inherited)", 612 | "@executable_path/Frameworks", 613 | ); 614 | MARKETING_VERSION = 1.0; 615 | PRODUCT_BUNDLE_IDENTIFIER = "com.azamsharp.hello-market-client"; 616 | PRODUCT_NAME = "$(TARGET_NAME)"; 617 | SWIFT_EMIT_LOC_STRINGS = YES; 618 | SWIFT_VERSION = 6.0; 619 | TARGETED_DEVICE_FAMILY = "1,2"; 620 | }; 621 | name = Release; 622 | }; 623 | /* End XCBuildConfiguration section */ 624 | 625 | /* Begin XCConfigurationList section */ 626 | 8DDE5FAC2C8A7D9300A0DBE0 /* Build configuration list for PBXProject "hello-market-client" */ = { 627 | isa = XCConfigurationList; 628 | buildConfigurations = ( 629 | 8DDE5FBD2C8A7D9400A0DBE0 /* Debug */, 630 | 8DDE5FBE2C8A7D9400A0DBE0 /* Release */, 631 | ); 632 | defaultConfigurationIsVisible = 0; 633 | defaultConfigurationName = Release; 634 | }; 635 | 8DDE5FBF2C8A7D9400A0DBE0 /* Build configuration list for PBXNativeTarget "hello-market-client" */ = { 636 | isa = XCConfigurationList; 637 | buildConfigurations = ( 638 | 8DDE5FC02C8A7D9400A0DBE0 /* Debug */, 639 | 8DDE5FC12C8A7D9400A0DBE0 /* Release */, 640 | ); 641 | defaultConfigurationIsVisible = 0; 642 | defaultConfigurationName = Release; 643 | }; 644 | /* End XCConfigurationList section */ 645 | 646 | /* Begin XCRemoteSwiftPackageReference section */ 647 | 8D20503D2CACC09C0088CFAF /* XCRemoteSwiftPackageReference "JWTDecode.swift" */ = { 648 | isa = XCRemoteSwiftPackageReference; 649 | repositoryURL = "https://github.com/auth0/JWTDecode.swift.git"; 650 | requirement = { 651 | kind = upToNextMajorVersion; 652 | minimumVersion = 3.2.0; 653 | }; 654 | }; 655 | 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */ = { 656 | isa = XCRemoteSwiftPackageReference; 657 | repositoryURL = "https://github.com/stripe/stripe-ios-spm"; 658 | requirement = { 659 | kind = upToNextMajorVersion; 660 | minimumVersion = 24.1.0; 661 | }; 662 | }; 663 | /* End XCRemoteSwiftPackageReference section */ 664 | 665 | /* Begin XCSwiftPackageProductDependency section */ 666 | 8D20503E2CACC09C0088CFAF /* JWTDecode */ = { 667 | isa = XCSwiftPackageProductDependency; 668 | package = 8D20503D2CACC09C0088CFAF /* XCRemoteSwiftPackageReference "JWTDecode.swift" */; 669 | productName = JWTDecode; 670 | }; 671 | 8D5B74452CAF70C200AA4BAC /* JWTDecode */ = { 672 | isa = XCSwiftPackageProductDependency; 673 | package = 8D20503D2CACC09C0088CFAF /* XCRemoteSwiftPackageReference "JWTDecode.swift" */; 674 | productName = JWTDecode; 675 | }; 676 | 8D80217C2CFCF28E006C709C /* Stripe */ = { 677 | isa = XCSwiftPackageProductDependency; 678 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 679 | productName = Stripe; 680 | }; 681 | 8D80217E2CFCF28E006C709C /* StripeApplePay */ = { 682 | isa = XCSwiftPackageProductDependency; 683 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 684 | productName = StripeApplePay; 685 | }; 686 | 8D8021802CFCF28E006C709C /* StripeCardScan */ = { 687 | isa = XCSwiftPackageProductDependency; 688 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 689 | productName = StripeCardScan; 690 | }; 691 | 8D8021822CFCF28E006C709C /* StripeFinancialConnections */ = { 692 | isa = XCSwiftPackageProductDependency; 693 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 694 | productName = StripeFinancialConnections; 695 | }; 696 | 8D8021842CFCF28E006C709C /* StripeIdentity */ = { 697 | isa = XCSwiftPackageProductDependency; 698 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 699 | productName = StripeIdentity; 700 | }; 701 | 8D8021862CFCF28E006C709C /* StripePaymentSheet */ = { 702 | isa = XCSwiftPackageProductDependency; 703 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 704 | productName = StripePaymentSheet; 705 | }; 706 | 8D8021882CFCF28E006C709C /* StripePayments */ = { 707 | isa = XCSwiftPackageProductDependency; 708 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 709 | productName = StripePayments; 710 | }; 711 | 8D80218A2CFCF28F006C709C /* StripePaymentsUI */ = { 712 | isa = XCSwiftPackageProductDependency; 713 | package = 8D80217B2CFCF28E006C709C /* XCRemoteSwiftPackageReference "stripe-ios-spm" */; 714 | productName = StripePaymentsUI; 715 | }; 716 | /* End XCSwiftPackageProductDependency section */ 717 | }; 718 | rootObject = 8DDE5FA92C8A7D9300A0DBE0 /* Project object */; 719 | } 720 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "8cf5bc47df8e1fe3c863fdb68b0b3fedb260d19d5cd82b2bac411e9325e2dbd4", 3 | "pins" : [ 4 | { 5 | "identity" : "jwtdecode.swift", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/auth0/JWTDecode.swift.git", 8 | "state" : { 9 | "revision" : "1e153ef009969543191970c66b7c60163c0b4a65", 10 | "version" : "3.2.0" 11 | } 12 | }, 13 | { 14 | "identity" : "stripe-ios-spm", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/stripe/stripe-ios-spm", 17 | "state" : { 18 | "revision" : "999143c82704e6395b62a9b8259ecfbee131eff9", 19 | "version" : "24.1.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/project.xcworkspace/xcuserdata/azamsharp.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-client/hello-market-client.xcodeproj/project.xcworkspace/xcuserdata/azamsharp.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/xcuserdata/azamsharp.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client.xcodeproj/xcuserdata/azamsharp.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | JWTDecode (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 3 13 | 14 | JWTDecode (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 4 20 | 21 | JWTDecode (Playground) 3.xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 2 27 | 28 | JWTDecode (Playground) 4.xcscheme 29 | 30 | isShown 31 | 32 | orderHint 33 | 5 34 | 35 | JWTDecode (Playground) 5.xcscheme 36 | 37 | isShown 38 | 39 | orderHint 40 | 6 41 | 42 | JWTDecode (Playground) 6.xcscheme 43 | 44 | isShown 45 | 46 | orderHint 47 | 7 48 | 49 | JWTDecode (Playground) 7.xcscheme 50 | 51 | isShown 52 | 53 | orderHint 54 | 8 55 | 56 | JWTDecode (Playground) 8.xcscheme 57 | 58 | isShown 59 | 60 | orderHint 61 | 9 62 | 63 | JWTDecode (Playground).xcscheme 64 | 65 | isShown 66 | 67 | orderHint 68 | 0 69 | 70 | hello-market-client.xcscheme_^#shared#^_ 71 | 72 | orderHint 73 | 0 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/.gitignore: -------------------------------------------------------------------------------- 1 | # macOS System Files 2 | .DS_Store 3 | *.swp 4 | *.lock 5 | *.xcuserstate 6 | 7 | # Build Files 8 | DerivedData/ 9 | build/ 10 | *.log 11 | 12 | # Xcode Workspace and Project Files 13 | xcuserdata/ 14 | *.xcscmblueprint 15 | 16 | # Swift Package Manager 17 | .swiftpm/ 18 | .build/ 19 | Package.resolved 20 | 21 | # CocoaPods 22 | Pods/ 23 | Podfile.lock 24 | 25 | # Carthage 26 | Carthage/Build/ 27 | 28 | # Archives 29 | *.xcarchive 30 | 31 | # Xcode Automatically Generated Files 32 | *.moved-aside 33 | *.xcuserstate 34 | *.xcworkspace 35 | 36 | # Device-Specific 37 | *.plist 38 | *.pbxuser 39 | 40 | # Temporary Files 41 | *.mode1v3 42 | *.mode2v3 43 | *.perspectivev3 44 | 45 | # Fastlane 46 | fastlane/report.xml 47 | fastlane/Preview.html 48 | fastlane/screenshots 49 | fastlane/test_output 50 | fastlane/*.lock 51 | 52 | # App Code 53 | *.orig 54 | 55 | # IDE Specific 56 | .idea/ 57 | *.hmap 58 | 59 | # Playgrounds 60 | timeline.xctimeline 61 | playground.xcworkspace 62 | 63 | # Environment Files 64 | .env 65 | .env.* 66 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Controllers/AuthenticationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationController.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AuthenticationController { 11 | 12 | private let httpClient: HTTPClient 13 | 14 | init(httpClient: HTTPClient) { 15 | self.httpClient = httpClient 16 | } 17 | 18 | func register(username: String, password: String) async throws -> RegisterResponse { 19 | 20 | let body = ["username": username, "password": password] 21 | let bodyData = try JSONEncoder().encode(body) 22 | 23 | // make the request 24 | let resource = Resource(url: Constants.Urls.register, method: .post(bodyData), modelType: RegisterResponse.self) 25 | let response = try await httpClient.load(resource) 26 | 27 | return response 28 | } 29 | 30 | func login(username: String, password: String) async throws { 31 | 32 | let body = ["username": username, "password": password] 33 | let bodyData = try JSONEncoder().encode(body) 34 | 35 | let resource = Resource(url: Constants.Urls.login, method: .post(bodyData), modelType: LoginResponse.self) 36 | let response = try await httpClient.load(resource) 37 | 38 | if let token = response.token, response.success { 39 | // save the token in the Keychain 40 | Keychain.set(token, forKey: "jwttoken") 41 | } else { 42 | throw LoginError.loginFailed(response.message ?? "Unable to login.") 43 | } 44 | } 45 | } 46 | 47 | extension AuthenticationController { 48 | 49 | static var development: AuthenticationController { 50 | AuthenticationController(httpClient: HTTPClient()) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Controllers/PaymentController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PaymentService.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/1/24. 6 | // 7 | 8 | import Foundation 9 | import Stripe 10 | import StripePaymentSheet 11 | 12 | struct PaymentController { 13 | 14 | private let httpClient: HTTPClient // Inject the HTTP client for API calls 15 | 16 | init(httpClient: HTTPClient) { 17 | self.httpClient = httpClient 18 | } 19 | 20 | @MainActor 21 | func preparePaymentSheet(for cart: Cart) async throws -> PaymentSheet { 22 | 23 | let params = ["totalAmount": cart.total] 24 | let paramsData = try JSONEncoder().encode(params) 25 | 26 | let resource = Resource( 27 | url: Constants.Urls.createPaymentIntent, 28 | method: .post(paramsData), 29 | modelType: CreatePaymentIntentResponse.self 30 | ) 31 | 32 | let response = try await httpClient.load(resource) 33 | 34 | guard let customerId = response.customerId, 35 | let customerEphemeralKeySecret = response.customerEphemeralKeySecret, 36 | let paymentIntentClientSecret = response.paymentIntentClientSecret 37 | else { 38 | throw PaymentServiceError.missingPaymentDetails 39 | } 40 | 41 | STPAPIClient.shared.publishableKey = response.publishableKey 42 | 43 | // Create PaymentSheet instance 44 | var configuration = PaymentSheet.Configuration() 45 | configuration.merchantDisplayName = "HelloMarket, Inc." 46 | configuration.customer = .init(id: customerId, ephemeralKeySecret: customerEphemeralKeySecret) 47 | 48 | return PaymentSheet( 49 | paymentIntentClientSecret: paymentIntentClientSecret, 50 | configuration: configuration 51 | ) 52 | } 53 | 54 | 55 | } 56 | 57 | // MARK: - Custom Error Types 58 | enum PaymentServiceError: Error { 59 | case missingPaymentDetails 60 | } 61 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Custom Errors/Errors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Errors.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LoginError: LocalizedError { 11 | case loginFailed(String) 12 | 13 | var errorDescription: String? { 14 | switch self { 15 | case .loginFailed: 16 | return NSLocalizedString("Login failed. Please check your username and password.", comment: "Login failure") 17 | } 18 | } 19 | 20 | var recoverySuggestion: String? { 21 | switch self { 22 | case .loginFailed: 23 | return NSLocalizedString("Make sure your credentials are correct and try again.", comment: "Login failure recovery suggestion") 24 | } 25 | } 26 | } 27 | 28 | enum UserError: Error { 29 | case operationFailed(String) 30 | } 31 | 32 | enum ProductError: Error { 33 | case invalidPrice 34 | case operationFailed(String) 35 | case missingImage 36 | case uploadFailed 37 | case productNotFound 38 | } 39 | 40 | enum CartError: Error { 41 | case invalidQuantity 42 | case operationFailed(String) 43 | } 44 | 45 | enum OrderError: Error { 46 | case saveFailed(String) 47 | } 48 | 49 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Extensions/EnvironmentValues+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnvironmentValues+Extensions.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/14/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | extension EnvironmentValues { 12 | @Entry var paymentController = PaymentController(httpClient: HTTPClient()) 13 | } 14 | 15 | extension EnvironmentValues { 16 | @Entry var httpClient = HTTPClient() 17 | } 18 | 19 | extension EnvironmentValues { 20 | @Entry var authenticationController = AuthenticationController(httpClient: HTTPClient()) 21 | } 22 | 23 | struct ShowMessageAction { 24 | typealias Action = (String, MessageType, Double) -> () 25 | let action: Action 26 | 27 | func callAsFunction(_ message: String, _ messageType: MessageType = .error, delay: Double = 2.0) { 28 | action(message, messageType, delay) 29 | } 30 | } 31 | 32 | extension EnvironmentValues { 33 | @Entry var showMessage: ShowMessageAction = ShowMessageAction { _, _, _ in } 34 | } 35 | 36 | extension EnvironmentValues { 37 | @Entry var uploaderDownloader = ImageUploaderDownloader(httpClient: HTTPClient()) 38 | } 39 | 40 | 41 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | var isEmptyOrWhitespace: Bool { 13 | self.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 14 | } 15 | 16 | var isZipCode: Bool { 17 | // Adjust this regex for your ZIP code requirements (US format example here) 18 | let zipCodeRegex = "^[0-9]{5}(-[0-9]{4})?$" 19 | return NSPredicate(format: "SELF MATCHES %@", zipCodeRegex).evaluate(with: self) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/HelloMarketClientApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // hello_market_clientApp.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | import JWTDecode 11 | @preconcurrency import Stripe 12 | 13 | @main 14 | struct HelloMarketClientApp: App { 15 | 16 | @State private var productStore = ProductStore(httpClient: HTTPClient()) 17 | @State private var cartStore = CartStore(httpClient: HTTPClient()) 18 | @State private var userStore = UserStore(httpClient: HTTPClient()) 19 | @State private var authenticationController = AuthenticationController(httpClient: HTTPClient()) 20 | @State private var paymentController = PaymentController(httpClient: HTTPClient()) 21 | @State private var orderStore = OrderStore(httpClient: HTTPClient()) 22 | 23 | @AppStorage("isAuthenticated") private var isAuthenticated = false 24 | 25 | init() { 26 | StripeAPI.defaultPublishableKey = ProcessInfo.processInfo.environment["STRIPE_PUBLISHABLE_KEY"] ?? "" 27 | } 28 | 29 | private func loadUserInfoAndCart() async { 30 | 31 | await cartStore.loadCart() 32 | 33 | do { 34 | try await userStore.loadUserInfo() 35 | } catch { 36 | print(error.localizedDescription) 37 | } 38 | } 39 | 40 | var body: some Scene { 41 | WindowGroup { 42 | HomeScreen() 43 | .environment(productStore) 44 | .environment(cartStore) 45 | .environment(userStore) 46 | .environment(orderStore) 47 | .environment(\.authenticationController, authenticationController) 48 | .environment(\.paymentController, paymentController) 49 | .environment(\.uploaderDownloader, ImageUploaderDownloader(httpClient: .development)) 50 | .withMessageView() 51 | .task(id: isAuthenticated) { 52 | if isAuthenticated { 53 | await loadUserInfoAndCart() 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | 61 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Models/DTOs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RegisterResponse.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RegisterResponse: Codable { 11 | let message: String? 12 | let success: Bool 13 | } 14 | 15 | struct LoginResponse: Codable { 16 | let message: String? 17 | let token: String? 18 | let success: Bool 19 | let username: String? 20 | } 21 | 22 | struct ErrorResponse: Codable { 23 | let message: String? 24 | } 25 | 26 | struct UploadDataResponse: Codable { 27 | let message: String? 28 | let success: Bool 29 | let downloadURL: URL? 30 | } 31 | 32 | struct ProductRequest: Codable { 33 | 34 | var id: Int? 35 | let name: String 36 | let description: String 37 | let price: Double 38 | let photoUrl: URL? 39 | 40 | private enum CodingKeys: String, CodingKey { 41 | case id, name, description, price 42 | case photoUrl = "photo_url" 43 | } 44 | } 45 | 46 | struct CreateProduct: Codable { 47 | 48 | let name: String 49 | let description: String 50 | let price: Double 51 | let photoUrl: URL? 52 | 53 | private enum CodingKeys: String, CodingKey { 54 | case name, description, price 55 | case photoUrl = "photo_url" 56 | } 57 | } 58 | 59 | struct Product: Codable, Identifiable, Hashable { 60 | 61 | var id: Int? 62 | let name: String 63 | let description: String 64 | let price: Double 65 | let photoUrl: URL? 66 | 67 | private enum CodingKeys: String, CodingKey { 68 | case id, name, description, price 69 | case photoUrl = "photo_url" 70 | } 71 | } 72 | 73 | extension Product { 74 | 75 | static var preview: Product { 76 | Product(id: 1, name: "Mirra Chair", description: "The Mirra chair by Herman Miller is an ergonomic office chair designed for comfort and support. It features an adjustable backrest, seat, and armrests, along with a flexible back that adapts to body movements. The chair's breathable mesh promotes airflow, while its responsive design encourages proper posture, making it ideal for long periods of sitting.", price: 850, photoUrl: URL(string: "http://localhost:8080/images/chair.jpg")!) 77 | } 78 | 79 | func encode() -> Data? { 80 | try? JSONEncoder().encode(self) 81 | } 82 | 83 | } 84 | 85 | struct CreateProductResponse: Codable { 86 | let success: Bool 87 | let product: Product? 88 | let message: String? 89 | } 90 | 91 | struct DeleteProductResponse: Codable { 92 | let success: Bool 93 | let message: String? 94 | } 95 | 96 | struct UpdateProductResponse: Codable { 97 | let success: Bool 98 | let message: String? 99 | let product: Product? 100 | } 101 | 102 | // Cart 103 | struct Cart: Codable, Identifiable { 104 | var id: Int? 105 | var cartItems: [CartItem] = [] 106 | 107 | private enum CodingKeys: String, CodingKey { 108 | case id, cartItems 109 | } 110 | } 111 | 112 | extension Cart { 113 | 114 | var total: Double { 115 | cartItems.reduce(0.0, { total, cartItem in 116 | total + (cartItem.product.price * Double(cartItem.quantity)) 117 | }) 118 | } 119 | 120 | var itemsCount: Int { 121 | cartItems.reduce(0) { total, item in 122 | total + item.quantity 123 | } 124 | } 125 | } 126 | 127 | extension Cart { 128 | static var preview: Cart { 129 | return Cart( 130 | id: 1, 131 | cartItems: [ 132 | CartItem( 133 | id: 1, 134 | product: Product( 135 | id: 201, 136 | name: "Coffee", 137 | description: "A rich, aromatic blend of premium coffee beans.", 138 | price: 5.99, 139 | photoUrl: URL(string: "https://picsum.photos/200/300") 140 | ), 141 | quantity: 2 142 | ), 143 | CartItem( 144 | id: 2, 145 | product: Product( 146 | id: 202, 147 | name: "Tea", 148 | description: "Refreshing green tea with hints of mint.", 149 | price: 3.49, 150 | photoUrl: URL(string: "https://picsum.photos/200/300") 151 | ), 152 | quantity: 1 153 | ), 154 | CartItem( 155 | id: 3, 156 | product: Product( 157 | id: 203, 158 | name: "Hot Chocolate", 159 | description: "Smooth and creamy hot chocolate.", 160 | price: 4.99, 161 | photoUrl: URL(string: "https://picsum.photos/200/300") 162 | ), 163 | quantity: 3 164 | ) 165 | ] 166 | ) 167 | } 168 | } 169 | 170 | 171 | struct CartItem: Codable, Identifiable, Hashable { 172 | let id: Int? 173 | let product: Product 174 | var quantity: Int = 1 175 | } 176 | 177 | extension CartItem { 178 | 179 | static var preview: CartItem { 180 | CartItem(id: 1, product: Product.preview, quantity: 2) 181 | } 182 | } 183 | 184 | struct CartItemResponse: Codable { 185 | let success: Bool 186 | let message: String? 187 | let cartItem: CartItem? 188 | } 189 | 190 | struct CartResponse: Codable { 191 | let success: Bool 192 | let message: String? 193 | let cart: Cart? 194 | } 195 | 196 | struct DeleteCartItemResponse: Codable { 197 | let success: Bool 198 | let message: String? 199 | } 200 | 201 | struct UserInfo: Codable, Equatable { 202 | let firstName: String? 203 | let lastName: String? 204 | let street: String? 205 | let city: String? 206 | let state: String? 207 | let zipCode: String? 208 | let country: String? 209 | 210 | private enum CodingKeys: String, CodingKey { 211 | case firstName = "first_name" 212 | case lastName = "last_name" 213 | case zipCode = "zip_code" 214 | case street, city, state, country 215 | } 216 | 217 | var address: String { 218 | [ 219 | street, 220 | [city, state].compactMap { $0 }.joined(separator: " "), 221 | zipCode, 222 | country 223 | ] 224 | .compactMap { $0 } 225 | .joined(separator: ", ") 226 | } 227 | 228 | var fullName: String { 229 | [firstName, lastName] 230 | .compactMap { $0 } 231 | .joined(separator: " ") 232 | } 233 | } 234 | 235 | struct UserInfoResponse: Codable { 236 | let success: Bool 237 | let message: String? 238 | let userInfo: UserInfo? 239 | } 240 | 241 | struct CreatePaymentIntentResponse: Codable { 242 | let paymentIntentClientSecret: String? 243 | let customerId: String? 244 | let customerEphemeralKeySecret: String? 245 | let publishableKey: String? 246 | 247 | private enum CodingKeys: String, CodingKey { 248 | case publishableKey 249 | case paymentIntentClientSecret = "paymentIntent" 250 | case customerId = "customer" 251 | case customerEphemeralKeySecret = "ephemeralKey" 252 | 253 | } 254 | } 255 | 256 | struct OrderItem: Codable, Hashable, Identifiable { 257 | var id: Int? 258 | let product: Product 259 | var quantity: Int = 1 260 | 261 | init(from cartItem: CartItem) { 262 | self.id = nil 263 | self.product = cartItem.product 264 | self.quantity = cartItem.quantity 265 | } 266 | } 267 | 268 | struct Order: Codable, Hashable, Identifiable { 269 | var id: Int? 270 | var total: Double 271 | var items: [OrderItem] 272 | 273 | init(from cart: Cart) { 274 | self.id = nil 275 | self.total = cart.total 276 | self.items = cart.cartItems.map(OrderItem.init) 277 | } 278 | 279 | private enum CodingKeys: String, CodingKey { 280 | case id, total, items 281 | } 282 | 283 | func toRequestBody() -> [String: Any] { 284 | return [ 285 | "total": total, 286 | "order_items": items.map { item in 287 | [ 288 | "product_id": item.product.id, 289 | "quantity": item.quantity 290 | ] 291 | } 292 | ] 293 | } 294 | } 295 | 296 | extension Order { 297 | static var preview: Order { 298 | Order(from: Cart.preview) 299 | } 300 | } 301 | 302 | 303 | 304 | struct SaveOrderResponse: Codable { 305 | let success: Bool 306 | let message: String? 307 | } 308 | 309 | 310 | 311 | 312 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Networking/HTTPClient.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPClient.swift 3 | // GroceryApp 4 | // 5 | // Created by Mohammad Azam on 5/7/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkError: Error { 11 | case badRequest 12 | case decodingError(Error) 13 | case invalidResponse 14 | case errorResponse(ErrorResponse) 15 | } 16 | 17 | extension NetworkError: LocalizedError { 18 | 19 | var errorDescription: String? { 20 | switch self { 21 | case .badRequest: 22 | return NSLocalizedString("Bad Request (400): Unable to perform the request.", comment: "badRequestError") 23 | case .decodingError(let error): 24 | return NSLocalizedString("Unable to decode successfully. \(error)", comment: "decodingError") 25 | case .invalidResponse: 26 | return NSLocalizedString("Invalid response.", comment: "invalidResponse") 27 | case .errorResponse(let errorResponse): 28 | return NSLocalizedString("Error \(errorResponse.message ?? "")", comment: "Error Response") 29 | } 30 | } 31 | } 32 | 33 | enum HTTPMethod { 34 | case get([URLQueryItem]) 35 | case post(Data?) 36 | case delete 37 | case put(Data?) 38 | 39 | var name: String { 40 | switch self { 41 | case .get: 42 | return "GET" 43 | case .post: 44 | return "POST" 45 | case .delete: 46 | return "DELETE" 47 | case .put: 48 | return "PUT" 49 | } 50 | } 51 | } 52 | 53 | struct Resource { 54 | let url: URL 55 | var method: HTTPMethod = .get([]) 56 | var headers: [String: String]? = nil 57 | var modelType: T.Type 58 | } 59 | 60 | 61 | struct HTTPClient { 62 | 63 | private let session: URLSession 64 | 65 | 66 | init() { 67 | 68 | let configuration = URLSessionConfiguration.default 69 | configuration.httpAdditionalHeaders = ["Content-Type": "application/json"] 70 | self.session = URLSession(configuration: configuration) 71 | } 72 | 73 | func load(_ resource: Resource) async throws -> T { 74 | 75 | var headers: [String: String] = [: ] 76 | 77 | // Get the token from keychain 78 | if let token = Keychain.get("jwttoken") { 79 | headers["Authorization"] = "Bearer \(token)" 80 | } 81 | 82 | var request = URLRequest(url: resource.url) 83 | 84 | // Add headers to the request 85 | for (key, value) in headers { 86 | request.setValue(value, forHTTPHeaderField: key) 87 | } 88 | 89 | // Set HTTP method and body if needed 90 | switch resource.method { 91 | case .get(let queryItems): 92 | var components = URLComponents(url: resource.url, resolvingAgainstBaseURL: false) 93 | components?.queryItems = queryItems 94 | guard let url = components?.url else { 95 | throw NetworkError.badRequest 96 | } 97 | request.url = url 98 | 99 | case .post(let data), .put(let data): 100 | request.httpMethod = resource.method.name 101 | request.httpBody = data 102 | 103 | case .delete: 104 | request.httpMethod = resource.method.name 105 | } 106 | 107 | // Set custom headers 108 | if let headers = resource.headers { 109 | for (key, value) in headers { 110 | request.setValue(value, forHTTPHeaderField: key) 111 | } 112 | } 113 | 114 | let (data, response) = try await session.data(for: request) 115 | 116 | guard let httpResponse = response as? HTTPURLResponse else { 117 | throw NetworkError.invalidResponse 118 | } 119 | 120 | // Check for specific HTTP errors 121 | switch httpResponse.statusCode { 122 | case 200...299: 123 | break // Success 124 | default: 125 | let errorResponse = try JSONDecoder().decode(ErrorResponse.self, from: data) 126 | throw NetworkError.errorResponse(errorResponse) 127 | } 128 | 129 | do { 130 | 131 | let decoder = JSONDecoder() 132 | decoder.dateDecodingStrategy = .iso8601 133 | let result = try decoder.decode(resource.modelType, from: data) 134 | return result 135 | } catch { 136 | throw NetworkError.decodingError(error) 137 | } 138 | } 139 | 140 | private func createMultipartFormDataBody(data: Data) -> Data? { 141 | return nil 142 | } 143 | } 144 | 145 | extension HTTPClient { 146 | 147 | static var development: HTTPClient { 148 | HTTPClient() 149 | } 150 | 151 | } 152 | 153 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/AddProductScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddProductScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/14/24. 6 | // 7 | 8 | import SwiftUI 9 | import PhotosUI 10 | 11 | struct AddProductScreen: View { 12 | 13 | let product: Product? 14 | 15 | @State private var name: String = "" 16 | @State private var description: String = "" 17 | @State private var price: Double? 18 | 19 | @Environment(\.showMessage) private var showMessage 20 | 21 | @Environment(\.dismiss) private var dismiss 22 | @Environment(ProductStore.self) private var productStore 23 | 24 | @Environment(\.uploaderDownloader) private var uploaderDownloader 25 | 26 | @State private var uiImage: UIImage? 27 | @State private var selectedPhotoItem: PhotosPickerItem? = nil 28 | @State private var isCameraSelected: Bool = false 29 | 30 | @State private var isLoading: Bool = false 31 | 32 | init(product: Product? = nil) { 33 | self.product = product 34 | } 35 | 36 | private var isFormValid: Bool { 37 | !name.isEmptyOrWhitespace && !description.isEmptyOrWhitespace 38 | && (price ?? 0) > 0 39 | } 40 | 41 | private var actionTitle: String { 42 | product != nil ? "Update Product": "Add Product" 43 | } 44 | 45 | private func saveOrUpdateProduct() async { 46 | isLoading = true 47 | defer { isLoading = false } // Ensures loading state is turned off, even if an error occurs 48 | 49 | do { 50 | guard let uiImage = uiImage, let imageData = uiImage.pngData() else { 51 | throw ProductError.missingImage 52 | } 53 | 54 | guard let photoURL = try await uploaderDownloader.upload(data: imageData) else { 55 | throw ProductError.uploadFailed 56 | } 57 | 58 | guard let price = price, price > 0 else { 59 | throw ProductError.invalidPrice 60 | } 61 | 62 | let product = Product(id: self.product?.id, name: name, description: description, price: price, photoUrl: photoURL) 63 | 64 | if self.product != nil { 65 | try await productStore.updateProduct(product) 66 | } else { 67 | try await productStore.saveProduct(product) 68 | } 69 | 70 | dismiss() 71 | 72 | } catch { 73 | showMessage("Error saving product: \(error.localizedDescription)") 74 | } 75 | } 76 | 77 | 78 | var body: some View { 79 | 80 | Form { 81 | TextField("Enter name", text: $name) 82 | TextEditor(text: $description) 83 | .frame(height: 100) 84 | TextField("Enter price", value: $price, format: .number) 85 | 86 | HStack { 87 | Button(action: { 88 | 89 | if UIImagePickerController.isSourceTypeAvailable( .camera) { 90 | isCameraSelected = true 91 | } else { 92 | // show message 93 | showMessage("Camera is not supported on this device.") 94 | } 95 | }, label: { 96 | Image(systemName: "camera.fill") 97 | }) 98 | Spacer().frame(width: 20) 99 | 100 | PhotosPicker(selection: $selectedPhotoItem, matching: .images, photoLibrary: .shared()) { 101 | Image(systemName: "photo.on.rectangle") 102 | } 103 | 104 | }.font(.title) 105 | 106 | 107 | if let uiImage { 108 | Image(uiImage: uiImage) 109 | .resizable() 110 | .aspectRatio(contentMode: .fit) 111 | } 112 | 113 | } 114 | .task(id: selectedPhotoItem, { 115 | if let selectedPhotoItem { 116 | do { 117 | if let data = try await selectedPhotoItem.loadTransferable(type: Data.self) { 118 | uiImage = UIImage(data: data) 119 | } 120 | } catch { 121 | showMessage("Unable to select an image!") 122 | } 123 | } 124 | }) 125 | .sheet(isPresented: $isCameraSelected, content: { 126 | ImagePicker(image: $uiImage, sourceType: .camera) 127 | }) 128 | 129 | .buttonStyle(.bordered) 130 | .navigationTitle(actionTitle) 131 | .toolbar { 132 | ToolbarItem(placement: .topBarTrailing) { 133 | Button(actionTitle) { 134 | Task { 135 | await saveOrUpdateProduct() 136 | } 137 | }.disabled(!isFormValid) 138 | } 139 | }.overlay(alignment: .center) { 140 | if isLoading { 141 | ProgressView("Loading...") 142 | } 143 | } 144 | .task { 145 | do { 146 | 147 | guard let product = product else { return } 148 | 149 | name = product.name 150 | description = product.description 151 | price = product.price 152 | 153 | if let photoUrl = product.photoUrl { 154 | guard let data = try await uploaderDownloader.download(from: photoUrl) else { 155 | return 156 | } 157 | 158 | uiImage = UIImage(data: data) 159 | } 160 | 161 | } catch { 162 | print(error.localizedDescription) 163 | } 164 | } 165 | } 166 | } 167 | 168 | #Preview { 169 | NavigationStack { 170 | AddProductScreen(product: Product.preview) 171 | } 172 | .environment(ProductStore(httpClient: .development)) 173 | .environment(\.uploaderDownloader, ImageUploaderDownloader(httpClient: .development)) 174 | .withMessageView() 175 | } 176 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/CartScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/2/24. 6 | // 7 | 8 | import SwiftUI 9 | import Stripe 10 | import StripePaymentSheet 11 | 12 | struct CartScreen: View { 13 | 14 | @Environment(CartStore.self) private var cartStore 15 | @Environment(UserStore.self) private var userStore 16 | @Environment(\.showMessage) private var showMessage 17 | 18 | @State private var proceedToCheckout: Bool = false 19 | 20 | var body: some View { 21 | List { 22 | if let cart = cartStore.cart { 23 | HStack { 24 | Text("Total: ") 25 | .font(.title) 26 | Text(cart.total, format: .currency(code: "USD")) 27 | .font(.title) 28 | .bold() 29 | } 30 | 31 | Button(action: { 32 | proceedToCheckout = true 33 | }) { 34 | 35 | Text("Proceed to checkout ^[(\(cart.itemsCount) Item](inflect: true))") 36 | .bold() 37 | .frame(maxWidth: .infinity) 38 | .padding() 39 | .background(Color.green) 40 | .foregroundStyle(.white) 41 | .cornerRadius(8) 42 | }.buttonStyle(.borderless) 43 | 44 | CartItemListView(cartItems: cart.cartItems) 45 | 46 | } else { 47 | ContentUnavailableView("No items in the cart.", systemImage: "cart") 48 | } 49 | } 50 | .navigationDestination(isPresented: $proceedToCheckout, destination: { 51 | if let cart = cartStore.cart { 52 | CheckoutScreen(cart: cart) 53 | } 54 | }) 55 | .listStyle(.plain) 56 | .navigationTitle("Cart") 57 | } 58 | } 59 | 60 | #Preview { 61 | NavigationStack { 62 | CartScreen() 63 | .environment(CartStore(httpClient: .development)) 64 | .environment(UserStore(httpClient: .development)) 65 | .environment(\.httpClient, .development) 66 | .withMessageView() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/CheckoutScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CheckoutScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/19/24. 6 | // 7 | 8 | import SwiftUI 9 | import Stripe 10 | import StripePaymentSheet 11 | 12 | struct CheckoutScreen: View { 13 | 14 | let cart: Cart 15 | 16 | @Environment(\.paymentController) private var paymentController 17 | 18 | @Environment(UserStore.self) private var userStore 19 | @Environment(OrderStore.self) private var orderStore 20 | @Environment(CartStore.self) private var cartStore 21 | 22 | @State private var paymentSheet: PaymentSheet? 23 | @State private var presentOrderConfirmationScreen: Bool = false 24 | 25 | @Environment(\.dismiss) private var dismiss 26 | 27 | private func paymentCompletion(result: PaymentSheetResult) { 28 | switch result { 29 | case .completed: 30 | Task { 31 | do { 32 | guard let cart = cartStore.cart else { 33 | throw CartError.operationFailed("Missing cart") 34 | } 35 | 36 | // Convert Cart to Order 37 | let order = Order(from: cart) 38 | 39 | // Save the order 40 | try await orderStore.saveOrder(order: order) 41 | 42 | // Empty the cart 43 | cartStore.emptyCart() 44 | 45 | // Present order confirmation 46 | presentOrderConfirmationScreen = true 47 | } catch { 48 | print("Error processing payment: \(error)") 49 | } 50 | } 51 | case .canceled: 52 | print("Payment canceled") 53 | case .failed(let error): 54 | print("Payment failed: \(error.localizedDescription)") 55 | } 56 | } 57 | 58 | 59 | var body: some View { 60 | List { 61 | 62 | VStack(spacing: 10) { 63 | Text("Place your order") 64 | .font(.title3) 65 | 66 | HStack { 67 | Text("Items:") 68 | Spacer() 69 | Text(cart.total, format: .currency(code: "USD")) 70 | } 71 | 72 | 73 | if let userInfo = userStore.userInfo, let _ = userInfo.firstName { 74 | 75 | Text("Delivering to \(userInfo.fullName)") 76 | .bold() 77 | .frame(maxWidth: .infinity, alignment: .leading) 78 | Text("\(userInfo.address)") 79 | .frame(maxWidth: .infinity, alignment: .leading) 80 | 81 | } else { 82 | Text("Please update your profile and add the missing information.") 83 | .foregroundStyle(.red) 84 | } 85 | 86 | } 87 | .padding() 88 | 89 | ForEach(cart.cartItems) { cartItem in 90 | CartItemView(cartItem: cartItem) 91 | } 92 | 93 | // payment sheet button 94 | if let paymentSheet = paymentSheet { 95 | PaymentSheet.PaymentButton( 96 | paymentSheet: paymentSheet, 97 | onCompletion: paymentCompletion 98 | ) { 99 | Text("Place your order") 100 | .bold() 101 | .frame(maxWidth: .infinity) 102 | .padding() 103 | .background(Color.green) 104 | .foregroundStyle(.white) 105 | .cornerRadius(8) 106 | .padding() 107 | .buttonStyle(.borderless) 108 | } 109 | } 110 | 111 | Spacer() 112 | } 113 | .navigationDestination(isPresented: $presentOrderConfirmationScreen, destination: { 114 | OrderConfirmationScreen() 115 | .navigationBarBackButtonHidden() 116 | }) 117 | .task { 118 | do { 119 | paymentSheet = try await paymentController.preparePaymentSheet(for: cart) 120 | } catch { 121 | print(error) 122 | } 123 | } 124 | 125 | .listStyle(.plain) 126 | 127 | .navigationTitle("Place Your Order") 128 | .navigationBarTitleDisplayMode(.inline) 129 | } 130 | } 131 | 132 | #Preview { 133 | NavigationStack { 134 | CheckoutScreen(cart: Cart.preview) 135 | .withMessageView() 136 | } 137 | .environment(CartStore(httpClient: .development)) 138 | .environment(UserStore(httpClient: .development)) 139 | .environment(OrderStore(httpClient: .development)) 140 | .environment(\.httpClient, .development) 141 | 142 | 143 | } 144 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/HomeScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum AppScreen: Hashable, Identifiable, CaseIterable { 11 | 12 | case home 13 | case myProducts 14 | case cart 15 | case profile 16 | 17 | var id: AppScreen { self } 18 | } 19 | 20 | extension AppScreen { 21 | 22 | @ViewBuilder 23 | var label: some View { 24 | switch self { 25 | case .home: 26 | Label("Home", systemImage: "heart") 27 | case .myProducts: 28 | Label("My Products", systemImage: "star") 29 | case .cart: 30 | Label("Cart", systemImage: "cart") 31 | case .profile: 32 | Label("Profile", systemImage: "person.fill") 33 | } 34 | } 35 | 36 | @MainActor 37 | @ViewBuilder 38 | var destination: some View { 39 | switch self { 40 | case .home: 41 | NavigationStack { 42 | ProductListScreen() 43 | } 44 | case .myProducts: 45 | NavigationStack { 46 | MyProductListScreen() 47 | .requiresAuthentication() 48 | } 49 | case .cart: 50 | NavigationStack { 51 | CartScreen() 52 | .requiresAuthentication() 53 | } 54 | case .profile: 55 | NavigationStack { 56 | ProfileScreen() 57 | .requiresAuthentication() 58 | } 59 | } 60 | } 61 | } 62 | 63 | struct HomeScreen: View { 64 | 65 | @State var selection: AppScreen? 66 | @Environment(CartStore.self) private var cartStore 67 | 68 | var body: some View { 69 | TabView(selection: $selection) { 70 | ForEach(AppScreen.allCases) { screen in 71 | screen.destination 72 | .tag(screen as AppScreen?) 73 | .tabItem { screen.label } 74 | .badge((screen as AppScreen?) == .cart ? cartStore.cart?.itemsCount ?? 0: 0) 75 | } 76 | }.navigationBarBackButtonHidden(true) 77 | } 78 | } 79 | 80 | #Preview { 81 | HomeScreen() 82 | .environment(ProductStore(httpClient: .development)) 83 | .environment(CartStore(httpClient: .development)) 84 | .environment(UserStore(httpClient: .development)) 85 | } 86 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/LoginScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LoginScreen: View { 11 | 12 | @Environment(\.authenticationController) private var authenticationController 13 | @Environment(\.showMessage) private var showMessage 14 | @Environment(\.dismiss) private var dismiss 15 | 16 | @State private var username: String = "" 17 | @State private var password: String = "" 18 | @State private var message: String? 19 | @State private var isLoading: Bool = false 20 | @State private var isRegistrationPresented: Bool = false 21 | 22 | @AppStorage("isAuthenticated") private var isAuthenticated: Bool = false 23 | 24 | private var isFormValid: Bool { 25 | !username.isEmptyOrWhitespace && !password.isEmptyOrWhitespace 26 | } 27 | 28 | private func login() async { 29 | 30 | message = nil 31 | isLoading = true 32 | defer { isLoading = false } 33 | 34 | do { 35 | try await authenticationController.login(username: username, password: password) 36 | isAuthenticated = true 37 | dismiss() 38 | } catch { 39 | showMessage(error.localizedDescription) 40 | } 41 | } 42 | 43 | var body: some View { 44 | 45 | VStack { 46 | 47 | HStack { 48 | Text("Don't have an account?") 49 | Button("Register") { 50 | isRegistrationPresented = true 51 | }.foregroundColor(.blue) 52 | } 53 | .font(.subheadline) 54 | .padding([.bottom, .top], 20) 55 | 56 | Form { 57 | TextField("User name", text: $username) 58 | .textInputAutocapitalization(.never) 59 | .padding() 60 | .background(Color(.systemGray6)) 61 | .cornerRadius(8) 62 | .overlay( 63 | RoundedRectangle(cornerRadius: 8) 64 | .stroke(Color.gray.opacity(0.5)) 65 | ) 66 | 67 | PasswordField(password: $password) 68 | 69 | HStack { 70 | Button(action: { 71 | Task { 72 | await login() 73 | } 74 | }) { 75 | Text("Login") 76 | .bold() 77 | .frame(maxWidth: .infinity) 78 | .padding() 79 | .background(isFormValid ? Color.purple : Color.gray) 80 | .foregroundColor(.white) 81 | .cornerRadius(8) 82 | } 83 | .disabled(!isFormValid) 84 | .padding(.horizontal) 85 | .padding(.top, 20) 86 | 87 | Spacer() 88 | 89 | }.buttonStyle(.borderless) 90 | 91 | } .sheet(isPresented: $isRegistrationPresented, content: { 92 | NavigationStack { 93 | RegistrationScreen() 94 | .withMessageView() 95 | } 96 | }) 97 | .navigationTitle("Login") 98 | .overlay(alignment: .center) { 99 | if isLoading { 100 | ProgressView("Loading...") 101 | } 102 | } 103 | } 104 | 105 | 106 | 107 | 108 | } 109 | } 110 | 111 | struct LoginScreenContainer: View { 112 | 113 | var body: some View { 114 | NavigationStack { 115 | LoginScreen() 116 | } 117 | .environment(\.authenticationController, .development) 118 | .withMessageView() 119 | .environment(ProductStore(httpClient: .development)) 120 | } 121 | } 122 | 123 | #Preview { 124 | LoginScreenContainer() 125 | } 126 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/MyProductDetailScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDetailScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/13/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MyProductDetailScreen: View { 11 | 12 | let product: Product 13 | 14 | @State private var isPresented: Bool = false 15 | @Environment(ProductStore.self) private var productStore 16 | 17 | @Environment(\.dismiss) private var dismiss 18 | 19 | private func deleteProduct() async { 20 | 21 | do { 22 | try await productStore.removeProduct(product) 23 | } catch { 24 | print(error.localizedDescription) 25 | } 26 | } 27 | 28 | var body: some View { 29 | ScrollView(showsIndicators: false) { 30 | AsyncImage(url: product.photoUrl) { img in 31 | img.resizable() 32 | .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) 33 | .scaledToFit() 34 | } placeholder: { 35 | ProgressView("Loading...") 36 | } 37 | 38 | Text(product.name) 39 | .font(.largeTitle) 40 | .frame(maxWidth: .infinity, alignment: .leading) 41 | 42 | Text(product.description) 43 | .padding([.top], 5) 44 | .frame(maxWidth: .infinity, alignment: .leading) 45 | 46 | Text(product.price, format: .currency(code: "USD")) 47 | .frame(maxWidth: .infinity, alignment: .leading) 48 | .font(.title) 49 | .bold() 50 | .padding([.top], 2) 51 | 52 | Button(role: .destructive) { 53 | Task { 54 | await deleteProduct() 55 | dismiss() 56 | } 57 | } label: { 58 | Text("Delete") 59 | .frame(maxWidth: .infinity) 60 | .frame(height: 44) 61 | .cornerRadius(25) 62 | }.buttonStyle(.borderedProminent) 63 | 64 | }.padding() 65 | .toolbar { 66 | ToolbarItem(placement: .topBarTrailing) { 67 | Button("Update") { 68 | isPresented = true 69 | } 70 | } 71 | } 72 | .sheet(isPresented: $isPresented, content: { 73 | NavigationStack { 74 | AddProductScreen(product: product) 75 | } 76 | }) 77 | } 78 | } 79 | 80 | #Preview { 81 | NavigationStack { 82 | MyProductDetailScreen(product: Product.preview) 83 | } 84 | .environment(ProductStore(httpClient: .development)) 85 | .environment(\.uploaderDownloader, ImageUploaderDownloader(httpClient: .development)) 86 | .withMessageView() 87 | } 88 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/MyProductListScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyProductListScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/13/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct MyProductListScreen: View { 11 | 12 | @Environment(\.showMessage) private var showMessage 13 | @Environment(ProductStore.self) private var productStore 14 | @State private var isPresented: Bool = false 15 | 16 | private func loadMyProducts() async { 17 | 18 | do { 19 | try await productStore.loadMyProducts() 20 | } catch { 21 | print(error.localizedDescription) 22 | } 23 | } 24 | 25 | var body: some View { 26 | List(productStore.myProducts) { product in 27 | NavigationLink { 28 | MyProductDetailScreen(product: product) 29 | } label: { 30 | MyProductCellView(product: product) 31 | } 32 | } 33 | .listStyle(.plain) 34 | .listRowSeparator(.hidden) 35 | .task { 36 | await loadMyProducts() 37 | }.navigationTitle("My Products") 38 | .toolbar { 39 | ToolbarItem(placement: .topBarTrailing) { 40 | Button("Add Product") { 41 | isPresented = true 42 | } 43 | } 44 | } 45 | .sheet(isPresented: $isPresented, content: { 46 | NavigationStack { 47 | AddProductScreen() 48 | .withMessageView() 49 | } 50 | }) 51 | .overlay(alignment: .center) { 52 | if productStore.myProducts.isEmpty { 53 | ContentUnavailableView("No products available.", systemImage: "cart") 54 | } 55 | } 56 | } 57 | } 58 | 59 | struct MyProductCellView: View { 60 | 61 | let product: Product 62 | 63 | var body: some View { 64 | 65 | HStack(alignment: .top) { 66 | AsyncImage(url: product.photoUrl) { img in 67 | img.resizable() 68 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 69 | .frame(width: 100, height: 100) 70 | } placeholder: { 71 | ProgressView("Loading...") 72 | } 73 | Spacer() 74 | .frame(width: 20) 75 | VStack { 76 | Text(product.name) 77 | .font(.title3) 78 | .frame(maxWidth: .infinity, alignment: .leading) 79 | Text(product.price, format: .currency(code: "USD")) 80 | .frame(maxWidth: .infinity, alignment: .leading) 81 | } 82 | } 83 | } 84 | } 85 | 86 | #Preview { 87 | NavigationStack { 88 | MyProductListScreen() 89 | }.environment(ProductStore(httpClient: .development)) 90 | } 91 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/OrderConfirmationScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderConfirmationScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/1/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OrderConfirmationScreen: View { 11 | var body: some View { 12 | VStack(spacing: 20) { 13 | Spacer() 14 | 15 | // Confirmation Icon 16 | Image(systemName: "checkmark.circle.fill") 17 | .resizable() 18 | .scaledToFit() 19 | .frame(width: 100, height: 100) 20 | .foregroundColor(.green) 21 | 22 | // Title 23 | Text("Order Placed Successfully!") 24 | .font(.title) 25 | .fontWeight(.bold) 26 | .multilineTextAlignment(.center) 27 | .padding(.horizontal) 28 | 29 | // Description 30 | Text("Thank you for your purchase! Your order has been successfully placed. We are preparing it for shipment.") 31 | .font(.body) 32 | .multilineTextAlignment(.center) 33 | .foregroundColor(.secondary) 34 | .padding(.horizontal, 20) 35 | 36 | Spacer() 37 | 38 | } 39 | .padding() 40 | .navigationTitle("Order Confirmation") 41 | .navigationBarTitleDisplayMode(.inline) 42 | } 43 | } 44 | 45 | #Preview { 46 | NavigationStack { 47 | OrderConfirmationScreen() 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/OrderHistoryScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderHistoryScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/2/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OrderHistoryScreen: View { 11 | 12 | @Environment(OrderStore.self) private var orderStore 13 | 14 | var body: some View { 15 | List(orderStore.orders) { order in 16 | VStack(alignment: .leading) { 17 | Text("Order ID: \(order.id!)") 18 | .font(.title) 19 | Text(order.total, format: .currency(code: "USD")) 20 | .bold() 21 | ForEach(order.items) { orderItem in 22 | OrderItemView(orderItem: orderItem) 23 | } 24 | } 25 | } 26 | .navigationTitle("Order History") 27 | .listStyle(.plain) 28 | .task { 29 | do { 30 | try await orderStore.loadOrders() 31 | } catch { 32 | print(error.localizedDescription) 33 | } 34 | } 35 | } 36 | } 37 | 38 | #Preview { 39 | NavigationStack { 40 | OrderHistoryScreen() 41 | .environment(OrderStore(httpClient: .development)) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/ProductDetailScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductDetailScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/13/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductDetailScreen: View { 11 | 12 | let product: Product 13 | @Environment(CartStore.self) private var cartStore 14 | 15 | @State private var quantity: Int = 1 16 | 17 | private func addToCart() async throws { 18 | 19 | guard let productId = product.id else { 20 | throw ProductError.productNotFound 21 | } 22 | 23 | try await cartStore.addItemToCart(productId: productId, quantity: quantity) 24 | } 25 | 26 | var body: some View { 27 | ScrollView { 28 | AsyncImage(url: product.photoUrl) { img in 29 | img.resizable() 30 | .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) 31 | .scaledToFit() 32 | } placeholder: { 33 | ProgressView("Loading...") 34 | } 35 | 36 | Text(product.name) 37 | .font(.largeTitle) 38 | .frame(maxWidth: .infinity, alignment: .leading) 39 | 40 | Text(product.description) 41 | .padding([.top], 5) 42 | Text(product.price, format: .currency(code: "USD")) 43 | .frame(maxWidth: .infinity, alignment: .leading) 44 | .font(.title) 45 | .bold() 46 | .padding([.top], 2) 47 | 48 | Stepper(value: $quantity) { 49 | Text("Quantity: \(quantity)") 50 | } 51 | 52 | Button { 53 | Task { 54 | do { 55 | try await addToCart() 56 | } catch { 57 | print(error.localizedDescription) 58 | } 59 | } 60 | } label: { 61 | Text("Add to cart") 62 | .frame(maxWidth: .infinity) 63 | .frame(height: 44) 64 | .foregroundColor(.white) 65 | .background(.orange) 66 | .cornerRadius(25) 67 | } 68 | 69 | 70 | }.padding() 71 | } 72 | } 73 | 74 | #Preview { 75 | ProductDetailScreen(product: Product.preview) 76 | .environment(CartStore(httpClient: .development)) 77 | .withMessageView() 78 | } 79 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/ProductListScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductListScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/11/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProductListScreen: View { 11 | 12 | @Environment(ProductStore.self) private var productStore 13 | 14 | private func loadAllProducts() async { 15 | do { 16 | try await productStore.loadAllProducts() 17 | } catch { 18 | print(error.localizedDescription) 19 | } 20 | } 21 | 22 | var body: some View { 23 | List(productStore.products) { product in 24 | 25 | ZStack { 26 | ProductCellView(product: product) 27 | 28 | NavigationLink(destination: ProductDetailScreen(product: product)) { 29 | EmptyView() 30 | } 31 | 32 | }.listRowSeparator(.hidden) 33 | } 34 | .refreshable(action: { 35 | await loadAllProducts() 36 | }) 37 | .navigationTitle("New Arrivals") 38 | .listStyle(.plain) 39 | .task { 40 | await loadAllProducts() 41 | } 42 | } 43 | } 44 | 45 | struct ProductCellView: View { 46 | 47 | let product: Product 48 | 49 | var body: some View { 50 | VStack(alignment: .leading, spacing: 10) { 51 | AsyncImage(url: product.photoUrl) { img in 52 | img.resizable() 53 | .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) 54 | .scaledToFit() 55 | } placeholder: { 56 | ProgressView("Loading...") 57 | } 58 | Text(product.name) 59 | .font(.title) 60 | Text(product.price, format: .currency(code: "USD")) 61 | .font(.title2) 62 | } 63 | } 64 | } 65 | 66 | #Preview { 67 | NavigationStack { 68 | ProductListScreen() 69 | } .environment(ProductStore(httpClient: .development)) 70 | } 71 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/ProfileScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileScreen.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 10/3/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ProfileScreen: View { 11 | 12 | @Environment(CartStore.self) private var cartStore 13 | @Environment(UserStore.self) private var userStore 14 | 15 | @Environment(\.showMessage) private var showMessage 16 | 17 | @State private var firstName: String = "" 18 | @State private var lastName: String = "" 19 | @State private var street: String = "" 20 | @State private var city: String = "" 21 | @State private var state: String = "" 22 | @State private var zipCode: String = "" 23 | @State private var country: String = "" 24 | 25 | @State private var validationErrors: [String] = [] 26 | @State private var updatingUserInfo: Bool = false 27 | 28 | @State private var isPresented: Bool = false 29 | @AppStorage("isAuthenticated") private var isAuthenticated = false 30 | 31 | private func validateForm() -> Bool { 32 | 33 | validationErrors = [] 34 | 35 | if firstName.isEmptyOrWhitespace { 36 | validationErrors.append("First name is required.") 37 | } 38 | if lastName.isEmptyOrWhitespace { 39 | validationErrors.append("Last name is required.") 40 | } 41 | if street.isEmptyOrWhitespace { 42 | validationErrors.append("Street is required.") 43 | } 44 | if city.isEmptyOrWhitespace { 45 | validationErrors.append("City is required.") 46 | } 47 | if state.isEmptyOrWhitespace { 48 | validationErrors.append("State is required.") 49 | } 50 | if !zipCode.isZipCode { 51 | validationErrors.append("Invalid ZIP code.") 52 | } 53 | if country.isEmptyOrWhitespace { 54 | validationErrors.append("Country is required.") 55 | } 56 | 57 | return validationErrors.isEmpty 58 | } 59 | 60 | private func updateUserInfo() async { 61 | 62 | do { 63 | let userInfo = UserInfo(firstName: firstName, lastName: lastName, street: street, city: city, state: state, zipCode: zipCode, country: country) 64 | try await userStore.updateUserInfo(userInfo: userInfo) 65 | showMessage("User profile has been updated.", .success) 66 | } catch { 67 | print(error.localizedDescription.localizedLowercase) 68 | } 69 | } 70 | 71 | var body: some View { 72 | let _ = Self._printChanges() 73 | 74 | List { 75 | Section("Personal Information") { 76 | TextField("First name", text: $firstName) 77 | TextField("Last name", text: $lastName) 78 | } 79 | 80 | Section("Address") { 81 | TextField("Street", text: $street) 82 | TextField("City", text: $city) 83 | TextField("State", text: $state) 84 | TextField("Zipcode", text: $zipCode) 85 | TextField("Country", text: $country) 86 | } 87 | 88 | Button("View Order History") { 89 | isPresented = true 90 | } 91 | 92 | Button("Signout") { 93 | let _ = Keychain.delete("jwttoken") 94 | isAuthenticated = false 95 | cartStore.emptyCart() 96 | userStore.userInfo = nil 97 | }.buttonStyle(.borderless) 98 | } 99 | .sheet(isPresented: $isPresented, content: { 100 | NavigationStack { 101 | OrderHistoryScreen() 102 | .environment(OrderStore(httpClient: HTTPClient())) 103 | } 104 | }) 105 | 106 | .onChange(of: userStore.userInfo, initial: true, { 107 | if let userInfo = userStore.userInfo { 108 | firstName = userInfo.firstName ?? "" 109 | lastName = userInfo.lastName ?? "" 110 | street = userInfo.street ?? "" 111 | city = userInfo.city ?? "" 112 | state = userInfo.state ?? "" 113 | zipCode = userInfo.zipCode ?? "" 114 | country = userInfo.country ?? "" 115 | } 116 | }) 117 | 118 | .toolbar(content: { 119 | ToolbarItem(placement: .topBarTrailing) { 120 | Button("Save") { 121 | if validateForm() { 122 | updatingUserInfo = true 123 | } else { 124 | // show message 125 | showMessage(validationErrors.joined(separator: "\n")) 126 | } 127 | } 128 | } 129 | }) 130 | .task(id: updatingUserInfo, { 131 | if updatingUserInfo { 132 | await updateUserInfo() 133 | } 134 | 135 | updatingUserInfo = false 136 | }) 137 | .navigationTitle("Profile") 138 | } 139 | } 140 | 141 | #Preview { 142 | 143 | NavigationStack { 144 | ProfileScreen() 145 | .withMessageView() 146 | } 147 | .environment(CartStore(httpClient: .development)) 148 | .environment(UserStore(httpClient: .development)) 149 | } 150 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Screens/RegistrationScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct RegistrationScreen: View { 11 | 12 | @Environment(\.showMessage) private var showMessage 13 | @Environment(\.authenticationController) private var authenticationController 14 | @Environment(\.dismiss) private var dismiss 15 | 16 | @State private var username: String = "" 17 | @State private var password: String = "" 18 | 19 | private var isFormValid: Bool { 20 | !username.isEmptyOrWhitespace && !password.isEmptyOrWhitespace 21 | } 22 | 23 | private func register() async { 24 | do { 25 | let response = try await authenticationController.register(username: username, password: password) 26 | if response.success { 27 | // dismiss the current screen 28 | dismiss() 29 | } else { 30 | showMessage(response.message ?? "") 31 | } 32 | 33 | } catch { 34 | showMessage(error.localizedDescription) 35 | } 36 | } 37 | 38 | var body: some View { 39 | Form { 40 | TextField("User name", text: $username) 41 | .padding() 42 | .background(Color(.systemGray6)) 43 | .cornerRadius(8) 44 | .overlay( 45 | RoundedRectangle(cornerRadius: 8) 46 | .stroke(Color.gray.opacity(0.5)) 47 | ) 48 | .textInputAutocapitalization(.never) 49 | .padding(.bottom, 10) // for spacing 50 | 51 | SecureField("Password", text: $password) 52 | .padding() 53 | .background(Color(.systemGray6)) 54 | .cornerRadius(8) 55 | .overlay( 56 | RoundedRectangle(cornerRadius: 8) 57 | .stroke(Color.gray.opacity(0.5)) 58 | ) 59 | .padding(.bottom, 20) // for spacing 60 | 61 | Button(action: { 62 | Task { 63 | await register() 64 | } 65 | }) { 66 | Text("Register") 67 | .bold() 68 | .frame(maxWidth: .infinity) // makes it fill the width 69 | .padding() 70 | .background(isFormValid ? Color.purple : Color.gray) 71 | .foregroundColor(.white) 72 | .cornerRadius(8) 73 | } 74 | .disabled(!isFormValid) 75 | .padding(.vertical, 10) 76 | .buttonStyle(.borderless) 77 | 78 | 79 | } 80 | .navigationTitle("Register") 81 | } 82 | } 83 | 84 | #Preview { 85 | NavigationStack { 86 | RegistrationScreen() 87 | 88 | } 89 | .environment(\.authenticationController, .development) 90 | .withMessageView() 91 | 92 | } 93 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Services/ImageUploaderDownloader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Uploader.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 10/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MimeType: String { 11 | case jpg = "image/jpg" 12 | case png = "image/png" 13 | case gif = "image/gif" 14 | case bmp = "image/bmp" 15 | case tiff = "image/tiff" 16 | case pdf = "application/pdf" 17 | case json = "application/json" 18 | case html = "text/html" 19 | case plainText = "text/plain" 20 | 21 | var value: String { 22 | return self.rawValue 23 | } 24 | } 25 | 26 | struct ImageUploaderDownloader { 27 | 28 | let httpClient: HTTPClient 29 | 30 | init(httpClient: HTTPClient) { 31 | self.httpClient = httpClient 32 | } 33 | 34 | func upload(data: Data, mimeType: MimeType = .png) async throws -> URL? { 35 | 36 | let boundary = UUID().uuidString 37 | let headers = ["Content-Type": "multipart/form-data; boundary=\(boundary)"] 38 | 39 | let body = createMultipartFormDataBody(data: data, boundary: boundary) 40 | let resource = Resource(url: Constants.Urls.uploadProductImage, method: .post(body), headers: headers, modelType: UploadDataResponse.self) 41 | 42 | let response = try await httpClient.load(resource) 43 | return response.downloadURL 44 | } 45 | 46 | func download(from url: URL) async throws -> Data? { 47 | let (data, _) = try await URLSession.shared.data(from: url) 48 | return data 49 | } 50 | 51 | private func createMultipartFormDataBody(data: Data, mimeType: MimeType = .png, boundary: String) -> Data { 52 | var body = Data() 53 | 54 | let lineBreak = "\r\n" 55 | 56 | // Add the file data 57 | body.append("--\(boundary)\(lineBreak)".data(using: .utf8)!) 58 | 59 | // Specify the Content-Disposition with name and filename 60 | body.append("Content-Disposition: form-data; name=\"image\"; filename=\"upload.png\"\(lineBreak)".data(using: .utf8)!) 61 | 62 | // Specify the Content-Type of the file 63 | body.append("Content-Type: \(mimeType.value)\(lineBreak)\(lineBreak)".data(using: .utf8)!) 64 | 65 | // Add the actual file data 66 | body.append(data) 67 | body.append(lineBreak.data(using: .utf8)!) 68 | 69 | // Add the closing boundary 70 | body.append("--\(boundary)--\(lineBreak)".data(using: .utf8)!) 71 | 72 | return body 73 | } 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Stores/CartStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartStore.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/2/24. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @MainActor 12 | @Observable 13 | class CartStore { 14 | 15 | let httpClient: HTTPClient 16 | var cart: Cart? 17 | 18 | var lastError: Error? 19 | 20 | init(httpClient: HTTPClient) { 21 | self.httpClient = httpClient 22 | } 23 | 24 | /* 25 | var total: Double { 26 | cart?.cartItems.reduce(0.0, { total, cartItem in 27 | total + (cartItem.product.price * Double(cartItem.quantity)) 28 | }) ?? 0.0 29 | } */ 30 | 31 | /* 32 | var itemsCount: Int { 33 | cart?.cartItems.reduce(0) { total, item in 34 | total + item.quantity 35 | } ?? 0 36 | } */ 37 | 38 | func emptyCart() { 39 | cart = nil 40 | } 41 | 42 | func loadCart() async { 43 | 44 | let resource = Resource(url: Constants.Urls.loadCart, modelType: CartResponse.self) 45 | do { 46 | let response = try await httpClient.load(resource) 47 | 48 | if let cart = response.cart, response.success { 49 | self.cart = cart 50 | } 51 | } catch { 52 | lastError = error 53 | } 54 | } 55 | 56 | func deleteCartItem(cartItemId: Int) async throws { 57 | 58 | let resource = Resource(url: Constants.Urls.deleteCartItem(cartItemId), method: .delete, modelType: DeleteCartItemResponse.self) 59 | 60 | let response = try await httpClient.load(resource) 61 | 62 | if response.success { 63 | // remove the cartItem from cartItems 64 | if let cart = cart { 65 | self.cart?.cartItems = cart.cartItems.filter { $0.id != cartItemId } 66 | } 67 | } else { 68 | throw CartError.operationFailed(response.message ?? "") 69 | } 70 | } 71 | 72 | func updateItemQuantity(productId: Int, quantity: Int) async throws { 73 | 74 | try await addItemToCart(productId: productId, quantity: quantity) 75 | } 76 | 77 | func addItemToCart(productId: Int, quantity: Int) async throws { 78 | 79 | let body = ["product_id": productId, "quantity": quantity] 80 | let bodyData = try JSONEncoder().encode(body) 81 | 82 | let resource = Resource(url: Constants.Urls.addCartItem, method: .post(bodyData), modelType: CartItemResponse.self) 83 | let response = try await httpClient.load(resource) 84 | 85 | if let cartItem = response.cartItem, response.success { 86 | // Initialize cart if it's nil 87 | if cart == nil { 88 | cart = Cart() 89 | } 90 | 91 | // if item already exists then update it else insert it 92 | if let index = cart?.cartItems.firstIndex(where: { $0.id == cartItem.id }) { 93 | cart?.cartItems[index] = cartItem 94 | } else { 95 | // new item 96 | cart?.cartItems.append(cartItem) 97 | } 98 | } else { 99 | throw CartError.operationFailed(response.message ?? "") 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Stores/OrderStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderStore.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/1/24. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @MainActor 12 | @Observable 13 | class OrderStore { 14 | 15 | var httpClient: HTTPClient 16 | var orders: [Order] = [] 17 | 18 | init(httpClient: HTTPClient) { 19 | self.httpClient = httpClient 20 | } 21 | 22 | func loadOrders() async throws { 23 | let resource = Resource(url: Constants.Urls.loadOrders, modelType: [Order].self) 24 | orders = try await httpClient.load(resource) 25 | } 26 | 27 | func saveOrder(order: Order) async throws { 28 | 29 | let body = try! JSONSerialization.data(withJSONObject: order.toRequestBody(), options: []) 30 | 31 | let resource = Resource(url: Constants.Urls.saveOrder, method: .post(body), modelType: SaveOrderResponse.self) 32 | let response = try await httpClient.load(resource) 33 | 34 | if !response.success { 35 | throw OrderError.saveFailed(response.message ?? "Unable to save product. Please try again.") 36 | } 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Stores/ProductStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProductStore.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/11/24. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @MainActor 12 | @Observable 13 | class ProductStore { 14 | 15 | let httpClient: HTTPClient 16 | private(set) var products: [Product] = [] 17 | private(set) var myProducts: [Product] = [] 18 | 19 | init(httpClient: HTTPClient) { 20 | self.httpClient = httpClient 21 | } 22 | 23 | func loadAllProducts() async throws { 24 | let resource = Resource(url: Constants.Urls.products, modelType: [Product].self) 25 | products = try await httpClient.load(resource) 26 | } 27 | 28 | func loadMyProducts() async throws { 29 | let resource = Resource(url: Constants.Urls.myProducts, modelType: [Product].self) 30 | myProducts = try await httpClient.load(resource) 31 | } 32 | 33 | func removeProduct(_ product: Product) async throws { 34 | 35 | guard let productId = product.id else { 36 | throw ProductError.productNotFound 37 | } 38 | 39 | let resource = Resource(url: Constants.Urls.deleteProduct(productId), method: .delete, modelType: DeleteProductResponse.self) 40 | let response = try await httpClient.load(resource) 41 | 42 | if response.success { 43 | // Safely find the index and remove the product from myProducts array 44 | if let indexToDelete = myProducts.firstIndex(where: { $0.id == product.id }) { 45 | myProducts.remove(at: indexToDelete) 46 | } else { 47 | throw ProductError.productNotFound 48 | } 49 | } else { 50 | throw ProductError.operationFailed(response.message ?? "") 51 | } 52 | } 53 | 54 | func saveProduct(_ product: Product) async throws { 55 | 56 | let body = try JSONEncoder().encode(product) 57 | 58 | let resource = Resource(url: Constants.Urls.createProduct, method: .post(body), modelType: CreateProductResponse.self) 59 | let response = try await httpClient.load(resource) 60 | if let product = response.product, response.success { 61 | myProducts.append(product) 62 | } else { 63 | throw ProductError.operationFailed(response.message ?? "") 64 | } 65 | } 66 | 67 | func updateProduct(_ product: Product) async throws { 68 | 69 | guard let productId = product.id else { 70 | print("product id is null") 71 | throw ProductError.productNotFound 72 | } 73 | 74 | let resource = Resource(url: Constants.Urls.updateProduct(productId), method: .put(product.encode()), modelType: UpdateProductResponse.self) 75 | let response = try await httpClient.load(resource) 76 | if let updatedProduct = response.product, response.success { 77 | // Safely find the index and remove the product from myProducts array 78 | if let indexToUpdate = myProducts.firstIndex(where: { $0.id == product.id }) { 79 | myProducts[indexToUpdate] = updatedProduct 80 | } else { 81 | throw ProductError.productNotFound 82 | } 83 | } else { 84 | throw ProductError.operationFailed(response.message ?? "") 85 | } 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Stores/UserStore.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserStore.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/23/24. 6 | // 7 | 8 | import Foundation 9 | import Observation 10 | 11 | @MainActor 12 | @Observable 13 | class UserStore { 14 | 15 | var userInfo: UserInfo? 16 | let httpClient: HTTPClient 17 | 18 | init(httpClient: HTTPClient) { 19 | self.httpClient = httpClient 20 | } 21 | 22 | func loadUserInfo() async throws { 23 | 24 | let resource = Resource(url: Constants.Urls.loadUserInfo, modelType: UserInfoResponse.self) 25 | let response = try await httpClient.load(resource) 26 | 27 | if let userInfo = response.userInfo, response.success { 28 | self.userInfo = userInfo 29 | } else { 30 | throw UserError.operationFailed(response.message ?? "") 31 | } 32 | } 33 | 34 | func updateUserInfo(userInfo: UserInfo) async throws { 35 | 36 | let userInfoData = try JSONEncoder().encode(userInfo) 37 | 38 | let resource = Resource(url: Constants.Urls.updateUserInfo, method: .put(userInfoData), modelType: UserInfoResponse.self) 39 | 40 | let response = try await httpClient.load(resource) 41 | 42 | if let userInfo = response.userInfo, response.success { 43 | self.userInfo = userInfo 44 | } else { 45 | throw UserError.operationFailed(response.message ?? "") 46 | } 47 | 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Utilities/ImagePicker.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePicker.swift 3 | // BudgetApp 4 | // 5 | // Created by Mohammad Azam on 7/30/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct ImagePicker: UIViewControllerRepresentable { 12 | 13 | @Environment(\.dismiss) private var dismiss 14 | typealias UIViewControllerType = UIImagePickerController 15 | typealias Coordinator = ImagePickerCoordinator 16 | @Binding var image: UIImage? 17 | var sourceType: UIImagePickerController.SourceType = .camera 18 | 19 | func makeCoordinator() -> ImagePicker.Coordinator { 20 | return ImagePickerCoordinator(self) 21 | } 22 | 23 | func makeUIViewController(context: Context) -> UIImagePickerController { 24 | 25 | let picker = UIImagePickerController() 26 | picker.allowsEditing = false 27 | picker.sourceType = sourceType 28 | picker.delegate = context.coordinator 29 | return picker 30 | } 31 | 32 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) { 33 | 34 | } 35 | 36 | class ImagePickerCoordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate { 37 | 38 | var picker: ImagePicker 39 | 40 | init(_ picker: ImagePicker) { 41 | self.picker = picker 42 | } 43 | 44 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 45 | 46 | 47 | if let uiImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage { 48 | self.picker.image = uiImage 49 | } 50 | 51 | self.picker.dismiss() 52 | } 53 | 54 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 55 | self.picker.dismiss() 56 | } 57 | 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Utility/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Constants { 11 | 12 | struct Urls { 13 | 14 | static let register: URL = URL(string: "http://localhost:8080/api/auth/register")! 15 | static let login: URL = URL(string: "http://localhost:8080/api/auth/login")! 16 | static let products: URL = URL(string: "http://localhost:8080/api/products")! 17 | static let createProduct = URL(string: "http://localhost:8080/api/products")! 18 | static let uploadProductImage = URL(string: "http://localhost:8080/api/products/upload")! 19 | static let addCartItem = URL(string: "http://localhost:8080/api/cart/items")! 20 | static let loadCart = URL(string: "http://localhost:8080/api/cart")! 21 | static let loadUserInfo = URL(string: "http://localhost:8080/api/user")! 22 | static let updateUserInfo = URL(string: "http://localhost:8080/api/user")! 23 | static let createPaymentIntent = URL(string: "http://localhost:8080/api/payment/create-payment-intent")! 24 | static let saveOrder = URL(string: "http://localhost:8080/api/orders")! 25 | static let loadOrders = URL(string: "http://localhost:8080/api/orders")! 26 | static let myProducts = URL(string: "http://localhost:8080/api/products/user")! 27 | 28 | static func deleteCartItem(_ cartItemId: Int) -> URL { 29 | URL(string: "http://localhost:8080/api/cart/item/\(cartItemId)")! 30 | } 31 | 32 | static func deleteProduct(_ productId: Int) -> URL { 33 | URL(string: "http://localhost:8080/api/products/\(productId)")! 34 | } 35 | 36 | static func updateProduct(_ productId: Int) -> URL { 37 | URL(string: "http://localhost:8080/api/products/\(productId)")! 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Utility/KeychainWrapper.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Security 3 | 4 | struct Keychain { 5 | static func set(_ value: T, forKey key: String) { 6 | do { 7 | let data = try JSONEncoder().encode(value) 8 | let query: [CFString: Any] = [ 9 | kSecClass: kSecClassGenericPassword, 10 | kSecAttrAccount: key, 11 | kSecValueData: data 12 | ] 13 | 14 | SecItemDelete(query as CFDictionary) // Delete existing data (if any) 15 | 16 | let status = SecItemAdd(query as CFDictionary, nil) 17 | if status != errSecSuccess { 18 | print("Error loading item to keychain \(status)") 19 | } 20 | } catch { 21 | print("Error encoding data: \(error)") 22 | } 23 | } 24 | 25 | static func get(_ key: String) -> T? { 26 | let query: [CFString: Any] = [ 27 | kSecClass: kSecClassGenericPassword, 28 | kSecAttrAccount: key, 29 | kSecReturnData: kCFBooleanTrue as Any, 30 | kSecMatchLimit: kSecMatchLimitOne 31 | ] 32 | 33 | var item: CFTypeRef? 34 | let status = SecItemCopyMatching(query as CFDictionary, &item) 35 | 36 | if status == errSecSuccess, let data = item as? Data { 37 | do { 38 | let value = try JSONDecoder().decode(T.self, from: data) 39 | return value 40 | } catch { 41 | print("Error decoding data: \(error)") 42 | } 43 | } 44 | return nil 45 | } 46 | 47 | static func delete(_ key: String) -> Bool { 48 | let query: [CFString: Any] = [ 49 | kSecClass: kSecClassGenericPassword, 50 | kSecAttrAccount: key 51 | ] 52 | 53 | let status = SecItemDelete(query as CFDictionary) 54 | return status == errSecSuccess 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Validators/TokenValidator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TokenValidator.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 10/3/24. 6 | // 7 | 8 | import Foundation 9 | import JWTDecode 10 | 11 | struct TokenValidator { 12 | 13 | static func validate(token: String?) -> Bool { 14 | 15 | guard let token = token else { return false } 16 | 17 | do { 18 | 19 | let jwt = try decode(jwt: token) 20 | 21 | // Access specific claim - expiration 22 | if let expirationDate = jwt.expiresAt { 23 | let currentDate = Date() 24 | 25 | // Check if the token has expired 26 | if currentDate >= expirationDate { 27 | return false 28 | } else { 29 | return true 30 | } 31 | } else { 32 | return false // Treat token as invalid if there's no expiration date 33 | } 34 | } catch { 35 | return false // Treat token as invalid if decoding fails 36 | } 37 | 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/View Modifiers/RequireAuthentication.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequiresAuthentication.swift 3 | // SmartShop 4 | // 5 | // Created by Mohammad Azam on 10/9/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct RequiresAuthentication: ViewModifier { 12 | 13 | @State private var isLoading: Bool = true 14 | @AppStorage("isAuthenticated") private var isAuthenticated: Bool = false 15 | 16 | func body(content: Content) -> some View { 17 | Group { 18 | if isLoading { 19 | ProgressView("Loading...") 20 | } else { 21 | if isAuthenticated { 22 | content 23 | } else { 24 | LoginScreen() 25 | .withMessageView() 26 | } 27 | } 28 | }.onAppear(perform: checkAuthentication) 29 | } 30 | 31 | private func checkAuthentication() { 32 | 33 | guard let token = Keychain.get("jwttoken"), TokenValidator.validate(token: token) else { 34 | isLoading = false 35 | isAuthenticated = false 36 | return 37 | } 38 | 39 | isLoading = false 40 | } 41 | } 42 | 43 | extension View { 44 | func requiresAuthentication() -> some View { 45 | modifier(RequiresAuthentication()) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/View Modifiers/WithMessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WithMessageView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/7/24. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | struct WithMessageView: ViewModifier { 12 | 13 | @State private var messageWrapper: MessageWrapper? 14 | 15 | func body(content: Content) -> some View { 16 | content 17 | .environment(\.showMessage, ShowMessageAction(action: { message, messageType, delay in 18 | self.messageWrapper = MessageWrapper(message: message, delay: delay, messageType: messageType) 19 | })) 20 | .overlay(alignment: .bottom) { 21 | messageWrapper != nil ? MessageView(messageWrapper: $messageWrapper) : nil 22 | } 23 | } 24 | } 25 | 26 | extension View { 27 | 28 | func withMessageView() -> some View { 29 | modifier(WithMessageView()) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/CartItemListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/2/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CartItemListView: View { 11 | 12 | let cartItems: [CartItem] 13 | 14 | var body: some View { 15 | ForEach(cartItems) { cartItem in 16 | CartItemView(cartItem: cartItem) 17 | }.listStyle(.plain) 18 | } 19 | } 20 | 21 | #Preview { 22 | CartItemListView(cartItems: Cart.preview.cartItems) 23 | } 24 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/CartItemQuantityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartItemQuantityView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/3/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum QuantityChangeType: Equatable { 11 | case update(Int) 12 | case delete 13 | } 14 | 15 | struct CartItemQuantityView: View { 16 | 17 | @Environment(CartStore.self) private var cartStore 18 | let cartItem: CartItem 19 | @State private var quantity: Int = 0 20 | @State private var quantityChangeType: QuantityChangeType? 21 | 22 | var body: some View { 23 | HStack { 24 | Button { 25 | if quantity == 1 { 26 | quantityChangeType = .delete 27 | } else { 28 | 29 | quantity -= 1 30 | quantityChangeType = .update(-1) 31 | } 32 | } label: { 33 | Image(systemName: cartItem.quantity == 1 ? "trash" : "minus") 34 | .frame(width: 24, height: 24) // Set a fixed frame size 35 | } 36 | Text("\(cartItem.quantity)") 37 | 38 | Button(action: { 39 | quantity += 1 40 | quantityChangeType = .update(1) 41 | }) { 42 | Image(systemName: "plus") 43 | } 44 | } 45 | .task(id: quantityChangeType) { 46 | if let quantityChangeType { 47 | print("quantityChangeType") 48 | switch quantityChangeType { 49 | case .update(let quantity): 50 | do { 51 | try await cartStore.updateItemQuantity(productId: cartItem.product.id!, quantity: quantity) 52 | } catch { 53 | // Handle the error here, e.g., show a message or log it 54 | print("Failed to update item quantity: \(error)") 55 | } 56 | case .delete: 57 | do { 58 | try await cartStore.deleteCartItem(cartItemId: cartItem.id!) 59 | } catch { 60 | // Handle the error here, e.g., show a message or log it 61 | print("Failed to delete cart item: \(error)") 62 | } 63 | } 64 | 65 | self.quantityChangeType = nil 66 | } 67 | } 68 | 69 | .onAppear(perform: { 70 | quantity = cartItem.quantity 71 | }) 72 | .frame(width: 150) 73 | .background(.gray) 74 | .foregroundStyle(.white) 75 | .buttonStyle(.borderedProminent) 76 | .tint(.gray) 77 | .cornerRadius(15.0) 78 | } 79 | } 80 | 81 | #Preview { 82 | CartItemQuantityView(cartItem: CartItem.preview) 83 | .environment(CartStore(httpClient: .development)) 84 | } 85 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/CartItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CartItemView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 11/2/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CartItemView: View { 11 | 12 | let cartItem: CartItem 13 | @State private var quantity: Int = 0 14 | 15 | var body: some View { 16 | HStack(alignment: .top) { 17 | AsyncImage(url: cartItem.product.photoUrl) { img in 18 | img.resizable() 19 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 20 | .frame(width: 100, height: 100) 21 | } placeholder: { 22 | ProgressView("Loading...") 23 | } 24 | Spacer() 25 | .frame(width: 20) 26 | VStack(alignment: .leading) { 27 | Text(cartItem.product.name) 28 | .font(.title3) 29 | Text(cartItem.product.price, format: .currency(code: "USD")) 30 | 31 | CartItemQuantityView(cartItem: cartItem) 32 | 33 | }.frame(maxWidth: .infinity, alignment: .leading) 34 | }.onAppear { 35 | quantity = cartItem.quantity 36 | } 37 | } 38 | } 39 | 40 | #Preview { 41 | CartItemView(cartItem: CartItem.preview) 42 | .environment(CartStore(httpClient: .development)) 43 | } 44 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/MessageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 9/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | enum MessageType { 11 | case error 12 | case info 13 | case success 14 | } 15 | 16 | struct MessageWrapper: Identifiable { 17 | let id: UUID = UUID() 18 | var message: String 19 | var delay: Double = 2.0 20 | var messageType: MessageType = .error 21 | } 22 | 23 | struct MessageView: View { 24 | 25 | @Binding var messageWrapper: MessageWrapper? 26 | 27 | var backgroundColor: Color { 28 | 29 | guard let messageType = messageWrapper?.messageType else { 30 | return .clear 31 | } 32 | 33 | switch messageType { 34 | case .error: 35 | return .red 36 | case .info: 37 | return .blue 38 | case .success: 39 | return .green 40 | } 41 | } 42 | 43 | var body: some View { 44 | 45 | Text(messageWrapper?.message ?? "") 46 | .frame(width: 300, alignment: .leading) 47 | .padding() 48 | .background(backgroundColor) 49 | .clipShape(RoundedRectangle(cornerRadius: /*@START_MENU_TOKEN@*/25.0/*@END_MENU_TOKEN@*/, style: /*@START_MENU_TOKEN@*/.continuous/*@END_MENU_TOKEN@*/)) 50 | .foregroundStyle(.white) 51 | .task(id: messageWrapper?.id) { 52 | try? await Task.sleep(for: .seconds(messageWrapper?.delay ?? 2.0)) 53 | guard !Task.isCancelled else { return } 54 | withAnimation { 55 | messageWrapper = nil 56 | } 57 | } 58 | } 59 | } 60 | 61 | #Preview { 62 | Group { 63 | MessageView(messageWrapper: .constant(MessageWrapper(message: "This is an error message."))) 64 | 65 | MessageView(messageWrapper: .constant(MessageWrapper(message: "This is an info message.", messageType: .info))) 66 | 67 | MessageView(messageWrapper: .constant(MessageWrapper(message: "Tbis is a success message.", messageType: .success))) 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/OrderItemView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OrderItemView.swift 3 | // hello-market-client 4 | // 5 | // Created by Mohammad Azam on 12/2/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct OrderItemView: View { 11 | 12 | let orderItem: OrderItem 13 | 14 | private var totalPrice: Double { 15 | Double(orderItem.quantity) * orderItem.product.price 16 | } 17 | 18 | var body: some View { 19 | HStack(alignment: .top) { 20 | AsyncImage(url: orderItem.product.photoUrl) { img in 21 | img.resizable() 22 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 23 | .frame(width: 100, height: 100) 24 | } placeholder: { 25 | ProgressView("Loading...") 26 | } 27 | Spacer() 28 | .frame(width: 20) 29 | VStack(alignment: .leading) { 30 | HStack { 31 | Text(orderItem.product.name) 32 | .font(.title3) 33 | Text("(\(orderItem.quantity))") 34 | } 35 | Text(totalPrice, format: .currency(code: "USD")) 36 | 37 | }.frame(maxWidth: .infinity, alignment: .leading) 38 | } 39 | } 40 | } 41 | 42 | #Preview { 43 | OrderItemView(orderItem: Order.preview.items[0]) 44 | } 45 | -------------------------------------------------------------------------------- /hello-market-client/hello-market-client/Views/PasswordField.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct PasswordField: View { 4 | @Binding var password: String 5 | @State private var isPasswordVisible: Bool = false 6 | 7 | var body: some View { 8 | HStack { 9 | if isPasswordVisible { 10 | TextField("Password", text: $password) 11 | .textContentType(.password) 12 | } else { 13 | SecureField("Password", text: $password) 14 | .textContentType(.password) 15 | } 16 | 17 | Button(action: { 18 | isPasswordVisible.toggle() 19 | }) { 20 | Image(systemName: isPasswordVisible ? "eye.slash" : "eye") 21 | .foregroundColor(.gray) 22 | } 23 | } 24 | .padding() 25 | .background(Color(.systemGray6)) 26 | .cornerRadius(8) 27 | } 28 | } 29 | 30 | #Preview { 31 | 32 | @Previewable @State var password: String = "password" 33 | PasswordField(password: $password) 34 | } 35 | -------------------------------------------------------------------------------- /hello-market-server/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | hello-market-server/uploads/ 134 | -------------------------------------------------------------------------------- /hello-market-server/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const app = express() 3 | const cors = require('cors') 4 | const authRoutes = require('./routes/auth') 5 | const productRoutes = require('./routes/product') 6 | const cartRoutes = require('./routes/cart') 7 | const userRoutes = require('./routes/user') 8 | const paymentRoutes = require('./routes/payment') 9 | const orderRoutes = require('./routes/order') 10 | 11 | const authenticate = require('./middlewares/authMiddleware') 12 | 13 | // use static resources 14 | app.use(express.static('public')) 15 | app.use('/uploads',express.static('uploads')) 16 | 17 | require('dotenv').config() 18 | 19 | app.use(cors()) 20 | app.use(express.json()) 21 | 22 | app.use('/api/auth', authRoutes); 23 | app.use('/api/products', productRoutes) 24 | app.use('/api/cart', authenticate, cartRoutes) 25 | app.use('/api/user', authenticate, userRoutes) 26 | app.use('/api/payment', authenticate, paymentRoutes) 27 | app.use('/api/orders', authenticate, orderRoutes) 28 | 29 | app.listen(8080, () => { 30 | console.log('Server is running...') 31 | }) -------------------------------------------------------------------------------- /hello-market-server/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "development": { 3 | "username": "postgres", 4 | "password": null, 5 | "database": "hellomarketdb", 6 | "host": "127.0.0.1", 7 | "dialect": "postgres" 8 | }, 9 | "test": { 10 | "username": "root", 11 | "password": null, 12 | "database": "database_test", 13 | "host": "127.0.0.1", 14 | "dialect": "mysql" 15 | }, 16 | "production": { 17 | "username": "root", 18 | "password": null, 19 | "database": "database_production", 20 | "host": "127.0.0.1", 21 | "dialect": "mysql" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hello-market-server/controllers/authenticationController.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken') 2 | const bcrypt = require('bcryptjs') 3 | const models = require('../models') 4 | const { Op } = require('sequelize'); 5 | const { validationResult } = require('express-validator'); 6 | 7 | exports.login = async (req, res) => { 8 | try { 9 | 10 | const { username, password } = req.body; 11 | console.log(username, password) 12 | 13 | // Check if username exists 14 | const existingUser = await models.User.findOne({ where: { username } }); 15 | if (!existingUser) { 16 | return res.status(401).json({ message: 'Username or password is incorrect', success: false }); 17 | } 18 | 19 | // Check the password 20 | const isPasswordValid = await bcrypt.compare(password, existingUser.password); 21 | if (!isPasswordValid) { 22 | return res.status(401).json({ message: 'Username or password is incorrect', success: false }); 23 | } 24 | 25 | // Generate JWT token 26 | const token = jwt.sign({ userId: existingUser.id }, 'SECRETKEY', { 27 | expiresIn: '1h', // Token expiration time 28 | }); 29 | 30 | return res.status(200).json({ userId: existingUser.id, username: existingUser.username, token, success: true }); 31 | } catch (error) { 32 | return res.status(500).json({ message: 'Internal server error', success: false }); 33 | } 34 | }; 35 | 36 | 37 | exports.register = async (req, res) => { 38 | 39 | try { 40 | const { username, password } = req.body; 41 | 42 | // Check if the user already exists 43 | const existingUser = await models.User.findOne({ 44 | where: { 45 | username: { [Op.iLike]: username }, 46 | }, 47 | }); 48 | 49 | if (existingUser) { 50 | return res.json({ message: 'Username taken!', success: false }); 51 | } 52 | 53 | // Create a password hash 54 | const salt = await bcrypt.genSalt(10); 55 | const hash = await bcrypt.hash(password, salt); 56 | 57 | // Create a new user 58 | const newUser = await models.User.create({ 59 | username: username, 60 | password: hash, 61 | }); 62 | 63 | res.status(201).json({ success: true }); 64 | } catch (error) { 65 | console.error(error); 66 | res.status(500).json({ message: 'Internal server error', success: false }); 67 | } 68 | }; -------------------------------------------------------------------------------- /hello-market-server/controllers/cartController.js: -------------------------------------------------------------------------------- 1 | const models = require('../models') 2 | 3 | 4 | exports.updateCartStatus = async (cartId, isActive, transaction) => { 5 | return await models.Cart.update( 6 | { is_active: isActive }, 7 | { 8 | where: { id: cartId, is_active: !isActive }, 9 | transaction, 10 | } 11 | ); 12 | }; 13 | 14 | exports.clearCartItems = async (cartId, transaction) => { 15 | return await models.CartItem.destroy({ 16 | where: { cart_id: cartId }, 17 | transaction, 18 | }); 19 | }; 20 | 21 | exports.removeCartItem = async (req, res) => { 22 | 23 | try { 24 | const { cartItemId } = req.params 25 | 26 | const deletedItem = await models.CartItem.destroy({ 27 | where: { 28 | id: cartItemId 29 | } 30 | }) 31 | 32 | if (!deletedItem) { 33 | return res.status(404).json({ message: 'Cart item not found', success: false }); 34 | } 35 | 36 | // Respond with a success message 37 | res.status(200).json({ success: true }); // 204 No Content 38 | } catch (error) { 39 | res.status(500).json({ message: 'An error occurred while removing the cart item', success: false }); 40 | } 41 | } 42 | 43 | exports.loadCart = async (req, res) => { 44 | 45 | try { 46 | const cart = await models.Cart.findOne({ 47 | where: { 48 | user_id: req.userId, 49 | is_active: true 50 | }, 51 | attributes: ['id', 'user_id', 'is_active'], 52 | include: [ 53 | { 54 | model: models.CartItem, 55 | as: 'cartItems', 56 | attributes: ['id', 'cart_id', 'product_id', 'quantity'], 57 | include: [ 58 | { 59 | model: models.Product, 60 | as: 'product', 61 | attributes: ['id', 'name', 'description', 'price', 'photo_url', 'user_id'] // Specify product fields 62 | } 63 | ] 64 | } 65 | ] 66 | }) 67 | 68 | res.status(200).json({ success: true, cart: cart }) 69 | 70 | } catch (error) { 71 | res.status(500).json({ message: error, success: false }); 72 | } 73 | 74 | } 75 | 76 | exports.addCartItem = async (req, res) => { 77 | 78 | const productId = req.body.product_id 79 | const quantity = parseInt(req.body.quantity) 80 | 81 | try { 82 | // get the cart if it is already available for this user 83 | let cart = await models.Cart.findOne({ 84 | where: { 85 | user_id: req.userId, 86 | is_active: true 87 | } 88 | }) 89 | 90 | if (!cart) { 91 | // create a new cart 92 | cart = await models.Cart.create({ 93 | user_id: req.userId, 94 | is_active: true 95 | }) 96 | } 97 | 98 | // add item to the cart 99 | const [cartItem, created] = await models.CartItem.findOrCreate({ 100 | where: { 101 | cart_id: cart.id, 102 | product_id: productId, 103 | }, 104 | defaults: { quantity } 105 | }) 106 | 107 | if (!created) { 108 | // item already exists 109 | cartItem.quantity += quantity 110 | // save it 111 | await cartItem.save() 112 | } 113 | 114 | // get cartItem with product details 115 | const cartItemWithProduct = await models.CartItem.findOne({ 116 | where: { 117 | id: cartItem.id 118 | }, 119 | include: [ 120 | { 121 | model: models.Product, 122 | as: 'product', 123 | attributes: ['id', 'name', 'description', 'price', 'photo_url', 'user_id'] 124 | } 125 | ] 126 | }) 127 | 128 | res.status(201).json({ 129 | message: 'Item added to the cart.', 130 | success: true, 131 | cartItem: cartItemWithProduct 132 | }) 133 | } catch (error) { 134 | console.error(error); 135 | res.status(500).json({ message: 'Internal server error' }); 136 | } 137 | 138 | 139 | } -------------------------------------------------------------------------------- /hello-market-server/controllers/orderController.js: -------------------------------------------------------------------------------- 1 | 2 | const models = require('../models') 3 | const cartController = require('./cartController') 4 | 5 | exports.loadOrders = async (req, res) => { 6 | 7 | const orders = await models.Order.findAll({ 8 | where: { 9 | user_id: req.userId 10 | }, 11 | attributes: ['id', 'user_id', 'total'], 12 | include: [ 13 | { 14 | model: models.OrderItem, 15 | as: 'items', 16 | attributes: ['id', 'quantity'], 17 | include: [ 18 | { 19 | model: models.Product, 20 | as: 'product', 21 | attributes: ['id', 'user_id','name', 'description', 'price', 'photo_url'] 22 | }, 23 | 24 | ] 25 | } 26 | ] 27 | }) 28 | 29 | res.json(orders) 30 | 31 | } 32 | 33 | exports.createOrder = async (req, res) => { 34 | 35 | console.log("CREATE ORDER") 36 | const userId = req.userId 37 | 38 | const { total, order_items } = req.body; 39 | 40 | // Start a transaction for atomicity 41 | const transaction = await models.Order.sequelize.transaction(); 42 | 43 | try { 44 | // Create the new order 45 | const newOrder = await models.Order.create( 46 | { 47 | user_id: userId, 48 | total: total, 49 | }, 50 | { transaction } // Ensure this operation is part of the transaction 51 | ); 52 | 53 | // Add order items 54 | const orderItemsData = order_items.map(item => ({ 55 | product_id: item.product_id, 56 | quantity: item.quantity, 57 | order_id: newOrder.id, // Associate with the newly created order 58 | })); 59 | 60 | await models.OrderItem.bulkCreate(orderItemsData, { transaction }); 61 | 62 | // get active cart_id 63 | let cart = await models.Cart.findOne({ 64 | where: { 65 | user_id: userId, 66 | is_active: true 67 | }, 68 | attributes: ['id', 'user_id', 'is_active'], 69 | }) 70 | 71 | console.log('CART') 72 | console.log(cart) 73 | 74 | // Update cart status 75 | await cartController.updateCartStatus(cart.id, false, transaction); 76 | 77 | // Clear cart items 78 | await cartController.clearCartItems(cart.id, transaction); 79 | 80 | // Commit the transaction 81 | await transaction.commit(); 82 | 83 | // get the new order and then return it 84 | 85 | // Return success response 86 | return res.status(201).json({ success: true }); 87 | } catch (error) { 88 | // Rollback the transaction in case of an error 89 | await transaction.rollback(); 90 | console.error(error); 91 | return res.status(500).json({ message: 'Internal server error', success: false }); 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /hello-market-server/controllers/paymentController.js: -------------------------------------------------------------------------------- 1 | 2 | const stripe = require('stripe')('sk_test_51Ai2s8HlHcPNi5kGS9cdIXZd4VEbZo2zQmZGU4vi6oEVRW9K5WKio6JeeRwjClNHakQhd3Rv60TxLGiMZrrcpQMk00pxsVbJSz'); 3 | 4 | exports.createPaymentIntent = async (req, res) => { 5 | 6 | // totalAmount is in dollars 7 | const { totalAmount } = req.body 8 | 9 | // Ensure totalAmount is a valid number 10 | if (typeof totalAmount !== "number" || isNaN(totalAmount)) { 11 | return res.status(400).json({ error: "Invalid totalAmount" }); 12 | } 13 | 14 | // Convert dollars to cents 15 | const totalAmountInCents = Math.round(totalAmount * 100); 16 | 17 | // Use an existing Customer ID if this is a returning customer. 18 | const customer = await stripe.customers.create(); 19 | const ephemeralKey = await stripe.ephemeralKeys.create( 20 | { customer: customer.id }, 21 | { apiVersion: '2017-06-05' } 22 | ); 23 | const paymentIntent = await stripe.paymentIntents.create({ 24 | amount: totalAmountInCents, 25 | currency: 'usd', 26 | customer: customer.id, 27 | // In the latest version of the API, specifying the `automatic_payment_methods` parameter 28 | // is optional because Stripe enables its functionality by default. 29 | automatic_payment_methods: { 30 | enabled: true, 31 | }, 32 | }); 33 | 34 | res.json({ 35 | paymentIntent: paymentIntent.client_secret, 36 | ephemeralKey: ephemeralKey.secret, 37 | customer: customer.id, 38 | publishableKey: 'pk_test_51Ai2s8HlHcPNi5kGEL4NmINijwzczRk0RJfSkEGDaAEs4ZYzZjqMXF8UEQPnYuN8Kl0usMjgQZYcd2ZRtNlNbRNR00hQK8SXLR' 39 | }); 40 | } -------------------------------------------------------------------------------- /hello-market-server/controllers/productController.js: -------------------------------------------------------------------------------- 1 | 2 | const models = require('../models') 3 | const multer = require('multer') 4 | const path = require('path') 5 | const fs = require('fs'); 6 | const { validationResult } = require('express-validator'); 7 | const { getFileNameFromUrl, deleteImageFile } = require('../utils/fileUtils'); 8 | 9 | // Configure multer for file storage 10 | const storage = multer.diskStorage({ 11 | destination: function (req, file, cb) { 12 | // Specify the destination folder for uploaded images 13 | cb(null, 'uploads/'); 14 | }, 15 | filename: function (req, file, cb) { 16 | // Generate a unique filename for each uploaded file 17 | cb(null, file.fieldname + '-' + Date.now() + path.extname(file.originalname)); 18 | } 19 | }); 20 | 21 | const uploadImage = multer({ 22 | storage: storage, 23 | limits: { fileSize: 5 * 1024 * 1024 }, // Limit file size to 5MB 24 | fileFilter: function (req, file, cb) { 25 | 26 | // Accept only images (jpeg, jpg, png) 27 | const fileTypes = /jpeg|jpg|png/; 28 | const extname = fileTypes.test(path.extname(file.originalname).toLowerCase()); 29 | const mimeType = fileTypes.test(file.mimetype); 30 | 31 | if (mimeType && extname) { 32 | return cb(null, true); 33 | } else { 34 | cb(new Error('Only images are allowed!')); 35 | } 36 | } 37 | }).single('image'); 38 | 39 | exports.upload = async (req, res) => { 40 | 41 | uploadImage(req, res, (err) => { 42 | 43 | if (err) { 44 | return res.status(400).send({ message: err.message, success: false }); 45 | } 46 | if (!req.file) { 47 | return res.status(400).send({ message: 'No file uploaded', success: false }); 48 | } 49 | 50 | // Construct the full URL for the uploaded file 51 | const baseUrl = `${req.protocol}://${req.get('host')}`; // e.g., http://localhost:3000 52 | const filePath = `/uploads/${req.file.filename}`; // Assuming your files are served from /uploads 53 | const downloadURL = `${baseUrl}${filePath}`; 54 | 55 | res.send({ message: 'File uploaded successfully', downloadURL: downloadURL, success: true }); 56 | }); 57 | } 58 | 59 | exports.getAllProducts = async (req, res) => { 60 | const products = await models.Product.findAll({}) 61 | res.json(products) 62 | } 63 | 64 | exports.deleteProduct = async (req, res) => { 65 | 66 | const errors = validationResult(req); 67 | 68 | if (!errors.isEmpty()) { 69 | const msg = errors.array().map(error => error.msg).join('') 70 | return res.status(422).json({ message: msg, success: false }); 71 | } 72 | 73 | const productId = req.params.productId 74 | 75 | try { 76 | 77 | // find the product to get the image path 78 | const product = await models.Product.findByPk(productId) 79 | if (!product) { 80 | return res.status(404).json({ message: 'Product not found', success: false }); 81 | } 82 | 83 | const fileName = getFileNameFromUrl(product.photo_url) 84 | 85 | const result = await models.Product.destroy({ 86 | where: { 87 | id: productId 88 | } 89 | }) 90 | 91 | if (result == 0) { 92 | return res.status(404).json({ message: 'Product not found', success: false }); 93 | } 94 | 95 | await deleteImageFile(fileName) 96 | 97 | return res.status(200).json({ message: `Product with ID ${productId} deleted successfully`, success: true }); 98 | 99 | } catch (error) { 100 | return res.status(500).json({ message: `Error deleting product ${error.message} `, success: false }); 101 | } 102 | } 103 | 104 | exports.getMyProducts = async (req, res) => { 105 | try { 106 | const userId = req.userId 107 | console.log(userId) 108 | const products = await models.Product.findAll({ 109 | where: { 110 | user_id: userId 111 | } 112 | }) 113 | res.json(products) 114 | } catch (error) { 115 | res.status(500).json({ message: 'Error retrieving products', success: false }); 116 | } 117 | } 118 | 119 | exports.create = async (req, res) => { 120 | 121 | const { name, description, price, photo_url } = req.body 122 | 123 | try { 124 | const newProduct = await models.Product.create({ 125 | name: name, 126 | description: description, 127 | price: price, 128 | photo_url: photo_url, 129 | user_id: req.userId 130 | }); 131 | 132 | // Return success response with the created product 133 | return res.status(201).json({ success: true, product: newProduct }); 134 | } catch (error) { 135 | return res.status(500).json({ message: "Internal server error", success: false }); 136 | } 137 | } 138 | 139 | exports.updateProduct = async (req, res) => { 140 | 141 | try { 142 | const { name, description, price, photo_url } = req.body 143 | const { productId } = req.params 144 | 145 | const product = await models.Product.findByPk(productId) 146 | if (!product) { 147 | return res.status(404).json({ message: 'Product not found', success: false }); 148 | } 149 | 150 | // update the product 151 | await product.update({ 152 | name, 153 | description, 154 | price, 155 | photo_url 156 | }) 157 | 158 | // Return the updated product along with a success message 159 | return res.status(200).json({ 160 | message: 'Product updated successfully', 161 | success: true, 162 | product // You can return the updated product details if needed 163 | }); 164 | } catch (err) { 165 | return res.status(500).json({ 166 | message: 'An error occurred while updating the product', 167 | success: false 168 | }); 169 | } 170 | 171 | } -------------------------------------------------------------------------------- /hello-market-server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | 2 | const models = require('../models') 3 | 4 | exports.loadUserInfo = async (req, res) => { 5 | 6 | try { 7 | 8 | const userInfo = await models.User.findByPk(req.userId, { 9 | attributes: ['id', 'first_name', 'last_name', 'street', 'city', 'state', 'zip_code', 'country'] 10 | }) 11 | 12 | if (!userInfo) { 13 | return res.status(404).json({ message: 'User not found.', success: false }); 14 | } 15 | 16 | return res.status(200).json({ 17 | success: true, 18 | userInfo 19 | }); 20 | 21 | } catch (error) { 22 | console.error(error) 23 | return res.status(500).json({ 24 | message: 'An error occurred while loading user info.', 25 | success: false 26 | }); 27 | } 28 | } 29 | 30 | exports.updateUserInfo = async (req, res) => { 31 | 32 | try { 33 | const userId = req.userId 34 | const { first_name, last_name, street, city, state, zip_code, country } = req.body 35 | 36 | // find the user 37 | const userInfo = await models.User.findByPk(userId, { 38 | attributes: ['id', 'first_name', 'last_name', 'street', 'city', 'state', 'zip_code', 'country'] 39 | }) 40 | 41 | if (!userInfo) { 42 | return res.status(404).json({ message: 'User not found.', success: false }); 43 | } 44 | 45 | // update the user 46 | await userInfo.update({ 47 | userId, 48 | first_name, 49 | last_name, 50 | street, 51 | city, 52 | state, 53 | zip_code, 54 | country 55 | }) 56 | 57 | return res.status(200).json({ 58 | message: 'User updated successfully', 59 | success: true, 60 | userInfo 61 | }); 62 | } catch (error) { 63 | console.log(error) 64 | return res.status(500).json({ 65 | message: 'An error occurred while updating the user', 66 | success: false 67 | }); 68 | } 69 | } -------------------------------------------------------------------------------- /hello-market-server/middlewares/authMiddleware.js: -------------------------------------------------------------------------------- 1 | 2 | const jwt = require('jsonwebtoken'); 3 | const models = require('../models') 4 | 5 | const authenticate = async (req, res, next) => { 6 | 7 | console.log("authenticate") 8 | // get the authorization header 9 | const authHeader = req.headers['authorization'] 10 | 11 | if(!authHeader) { 12 | return res.status(401).json({ message: 'No token provided' }); 13 | } 14 | 15 | const token = authHeader.split(' ')[1] 16 | 17 | if(!token) { 18 | return res.status(401).json({ message: 'Invalid token format' }); 19 | } 20 | 21 | try { 22 | 23 | const decoded = jwt.verify(token, 'SECRETKEY'); 24 | const user = await models.User.findByPk(decoded.userId) 25 | console.log(user) 26 | if(!user) { 27 | return res.status(404).json({ message: 'User not found' }); 28 | } 29 | 30 | req.userId = user.id 31 | console.log(req.userId) 32 | next() 33 | 34 | } catch (error) { 35 | console.log(error) 36 | return res.status(403).json({ message: 'Invalid or expired token' }); 37 | } 38 | 39 | } 40 | 41 | module.exports = authenticate -------------------------------------------------------------------------------- /hello-market-server/middlewares/validationErrorsMiddleware.js: -------------------------------------------------------------------------------- 1 | 2 | const { validationResult } = require('express-validator'); 3 | 4 | const validationErrorsMiddleware = (req, res, next) => { 5 | 6 | const errors = validationResult(req); 7 | 8 | if (!errors.isEmpty()) { 9 | const msg = errors.array().map(error => error.msg).join(' '); 10 | return res.status(422).json({ message: msg, success: false }); 11 | } 12 | 13 | // If no validation errors, proceed to the next middleware/route handler 14 | next(); 15 | } 16 | 17 | module.exports = validationErrorsMiddleware -------------------------------------------------------------------------------- /hello-market-server/migrations/20240905232846-create-user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** @type {import('sequelize-cli').Migration} */ 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.createTable('Users', { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: Sequelize.INTEGER 11 | }, 12 | username: { 13 | type: Sequelize.STRING 14 | }, 15 | password: { 16 | type: Sequelize.STRING 17 | }, 18 | createdAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE 21 | }, 22 | updatedAt: { 23 | allowNull: false, 24 | type: Sequelize.DATE 25 | } 26 | }); 27 | }, 28 | async down(queryInterface, Sequelize) { 29 | await queryInterface.dropTable('Users'); 30 | } 31 | }; -------------------------------------------------------------------------------- /hello-market-server/migrations/20240911011904-create-product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** @type {import('sequelize-cli').Migration} */ 3 | module.exports = { 4 | async up(queryInterface, Sequelize) { 5 | await queryInterface.createTable('Products', { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: Sequelize.INTEGER 11 | }, 12 | name: { 13 | type: Sequelize.STRING 14 | }, 15 | description: { 16 | type: Sequelize.TEXT 17 | }, 18 | price: { 19 | type: Sequelize.FLOAT 20 | }, 21 | photo_url: { 22 | type: Sequelize.STRING 23 | }, 24 | createdAt: { 25 | allowNull: false, 26 | type: Sequelize.DATE 27 | }, 28 | updatedAt: { 29 | allowNull: false, 30 | type: Sequelize.DATE 31 | } 32 | }); 33 | }, 34 | async down(queryInterface, Sequelize) { 35 | await queryInterface.dropTable('Products'); 36 | } 37 | }; -------------------------------------------------------------------------------- /hello-market-server/migrations/20240914024544-add-user-id-to-product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** @type {import('sequelize-cli').Migration} */ 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addColumn('Products', 'user_id', { 7 | type: Sequelize.INTEGER, 8 | allowNull: true, 9 | defaultValue: 1, 10 | references: { 11 | model: 'Users', 12 | key: 'id', 13 | } 14 | }); 15 | }, 16 | 17 | down: async (queryInterface, Sequelize) => { 18 | await queryInterface.removeColumn('Products', 'user_id'); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241101154232-create-cart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Carts', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | user_id: { 12 | type: Sequelize.INTEGER 13 | }, 14 | createdAt: { 15 | allowNull: false, 16 | type: Sequelize.DATE 17 | }, 18 | updatedAt: { 19 | allowNull: false, 20 | type: Sequelize.DATE 21 | } 22 | }); 23 | }, 24 | down: async (queryInterface, Sequelize) => { 25 | await queryInterface.dropTable('Carts'); 26 | } 27 | }; -------------------------------------------------------------------------------- /hello-market-server/migrations/20241101155255-create-cart-item.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('CartItems', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER, 10 | }, 11 | cart_id: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false, 14 | references: { 15 | model: 'Carts', 16 | key: 'id', 17 | }, 18 | onDelete: 'CASCADE', 19 | }, 20 | product_id: { 21 | type: Sequelize.INTEGER, 22 | allowNull: false, 23 | references: { 24 | model: 'Products', 25 | key: 'id', 26 | }, 27 | }, 28 | quantity: { 29 | type: Sequelize.INTEGER, 30 | allowNull: false, 31 | }, 32 | added_at: { 33 | type: Sequelize.DATE, 34 | defaultValue: Sequelize.NOW, 35 | }, 36 | }, { 37 | uniqueKeys: { 38 | cart_product_unique: { 39 | fields: ['cart_id', 'product_id'], 40 | }, 41 | }, 42 | }); 43 | }, 44 | down: async (queryInterface) => { 45 | await queryInterface.dropTable('Cart_Items'); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241101160721-add-is-active-to-carts.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, Sequelize) => { 3 | await queryInterface.addColumn('Carts', 'is_active', { 4 | type: Sequelize.BOOLEAN, 5 | defaultValue: true, 6 | allowNull: false, 7 | }); 8 | }, 9 | 10 | down: async (queryInterface) => { 11 | await queryInterface.removeColumn('Carts', 'is_active'); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241101172947-modify-cartitems-timestamps.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | up: async (queryInterface, Sequelize) => { 3 | // Remove the added_at column 4 | await queryInterface.removeColumn('CartItems', 'added_at'); 5 | 6 | // Add the createdAt and updatedAt columns 7 | await queryInterface.addColumn('CartItems', 'createdAt', { 8 | allowNull: false, 9 | type: Sequelize.DATE, 10 | defaultValue: Sequelize.NOW, 11 | }); 12 | 13 | await queryInterface.addColumn('CartItems', 'updatedAt', { 14 | allowNull: false, 15 | type: Sequelize.DATE, 16 | defaultValue: Sequelize.NOW, 17 | }); 18 | }, 19 | 20 | down: async (queryInterface, Sequelize) => { 21 | // Revert changes: add the added_at column back 22 | await queryInterface.addColumn('CartItems', 'added_at', { 23 | type: Sequelize.DATE, 24 | defaultValue: Sequelize.NOW, 25 | }); 26 | 27 | // Remove createdAt and updatedAt columns 28 | await queryInterface.removeColumn('CartItems', 'createdAt'); 29 | await queryInterface.removeColumn('CartItems', 'updatedAt'); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241119213137-add-columns-to-users.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addColumn('Users', 'first_name', { 6 | type: Sequelize.STRING, 7 | allowNull: true, 8 | }); 9 | await queryInterface.addColumn('Users', 'last_name', { 10 | type: Sequelize.STRING, 11 | allowNull: true, 12 | }); 13 | await queryInterface.addColumn('Users', 'street', { 14 | type: Sequelize.STRING, 15 | allowNull: true, 16 | }); 17 | await queryInterface.addColumn('Users', 'city', { 18 | type: Sequelize.STRING, 19 | allowNull: true, 20 | }); 21 | await queryInterface.addColumn('Users', 'state', { 22 | type: Sequelize.STRING, 23 | allowNull: true, 24 | }); 25 | await queryInterface.addColumn('Users', 'country', { 26 | type: Sequelize.STRING, 27 | allowNull: true, 28 | }); 29 | await queryInterface.addColumn('Users', 'zip_code', { 30 | type: Sequelize.STRING, 31 | allowNull: true, 32 | }); 33 | }, 34 | 35 | down: async (queryInterface, Sequelize) => { 36 | await queryInterface.removeColumn('Users', 'first_name'); 37 | await queryInterface.removeColumn('Users', 'last_name'); 38 | await queryInterface.removeColumn('Users', 'street'); 39 | await queryInterface.removeColumn('Users', 'city'); 40 | await queryInterface.removeColumn('Users', 'state'); 41 | await queryInterface.removeColumn('Users', 'country'); 42 | await queryInterface.removeColumn('Users', 'zip_code'); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241202002415-create-order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('Orders', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | user_id: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false, 14 | references: { 15 | model: 'Users', // Name of the target table 16 | key: 'id', // Key in the target table 17 | }, 18 | onUpdate: 'CASCADE', // Action when the referenced user is updated 19 | onDelete: 'SET NULL' // Action when the referenced user is deleted 20 | }, 21 | total: { 22 | type: Sequelize.FLOAT, 23 | allowNull: false, 24 | defaultValue: 0.0 25 | }, 26 | createdAt: { 27 | allowNull: false, 28 | type: Sequelize.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: Sequelize.DATE 33 | } 34 | }); 35 | }, 36 | down: async (queryInterface, Sequelize) => { 37 | await queryInterface.dropTable('Orders'); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /hello-market-server/migrations/20241202003211-create-order-item.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | module.exports = { 3 | up: async (queryInterface, Sequelize) => { 4 | await queryInterface.createTable('OrderItems', { 5 | id: { 6 | allowNull: false, 7 | autoIncrement: true, 8 | primaryKey: true, 9 | type: Sequelize.INTEGER 10 | }, 11 | product_id: { 12 | type: Sequelize.INTEGER, 13 | allowNull: false, 14 | references: { 15 | model: 'Products', // Name of the Products table 16 | key: 'id' // Column in the Products table 17 | }, 18 | onUpdate: 'CASCADE', // Update OrderItems if the Products table is updated 19 | onDelete: 'CASCADE' // Delete OrderItems if the referenced Product is deleted 20 | }, 21 | quantity: { 22 | type: Sequelize.INTEGER, 23 | allowNull: false, 24 | defaultValue: 1 // Default quantity is 1 25 | }, 26 | order_id: { 27 | type: Sequelize.INTEGER, 28 | allowNull: false, 29 | references: { 30 | model: 'Orders', // Name of the Orders table 31 | key: 'id' // Column in the Orders table 32 | }, 33 | onUpdate: 'CASCADE', // Update OrderItems if the Orders table is updated 34 | onDelete: 'CASCADE' // Delete OrderItems if the referenced Order is deleted 35 | }, 36 | createdAt: { 37 | allowNull: false, 38 | type: Sequelize.DATE, 39 | defaultValue: Sequelize.NOW 40 | }, 41 | updatedAt: { 42 | allowNull: false, 43 | type: Sequelize.DATE, 44 | defaultValue: Sequelize.NOW 45 | } 46 | }); 47 | }, 48 | down: async (queryInterface, Sequelize) => { 49 | await queryInterface.dropTable('OrderItems'); 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /hello-market-server/models/cart.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Cart extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | Cart.hasMany(models.CartItem, { 14 | foreignKey: 'cart_id', 15 | as: 'cartItems' 16 | }) 17 | } 18 | }; 19 | Cart.init({ 20 | user_id: DataTypes.INTEGER, 21 | is_active: DataTypes.BOOLEAN 22 | }, { 23 | sequelize, 24 | modelName: 'Cart', 25 | }); 26 | return Cart; 27 | }; -------------------------------------------------------------------------------- /hello-market-server/models/cartitem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class CartItem extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | CartItem.belongsTo(models.Product, { 14 | foreignKey: 'product_id', 15 | as: 'product' 16 | }) 17 | } 18 | }; 19 | CartItem.init({ 20 | cart_id: DataTypes.INTEGER, 21 | product_id: DataTypes.INTEGER, 22 | quantity: DataTypes.INTEGER 23 | }, { 24 | sequelize, 25 | modelName: 'CartItem', 26 | }); 27 | return CartItem; 28 | }; -------------------------------------------------------------------------------- /hello-market-server/models/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const Sequelize = require('sequelize'); 6 | const process = require('process'); 7 | const basename = path.basename(__filename); 8 | const env = process.env.NODE_ENV || 'development'; 9 | const config = require(__dirname + '/../config/config.json')[env]; 10 | const db = {}; 11 | 12 | let sequelize; 13 | if (config.use_env_variable) { 14 | sequelize = new Sequelize(process.env[config.use_env_variable], config); 15 | } else { 16 | sequelize = new Sequelize(config.database, config.username, config.password, config); 17 | } 18 | 19 | fs 20 | .readdirSync(__dirname) 21 | .filter(file => { 22 | return ( 23 | file.indexOf('.') !== 0 && 24 | file !== basename && 25 | file.slice(-3) === '.js' && 26 | file.indexOf('.test.js') === -1 27 | ); 28 | }) 29 | .forEach(file => { 30 | const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); 31 | db[model.name] = model; 32 | }); 33 | 34 | Object.keys(db).forEach(modelName => { 35 | if (db[modelName].associate) { 36 | db[modelName].associate(db); 37 | } 38 | }); 39 | 40 | db.sequelize = sequelize; 41 | db.Sequelize = Sequelize; 42 | 43 | module.exports = db; 44 | -------------------------------------------------------------------------------- /hello-market-server/models/order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Order extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | Order.hasMany(models.OrderItem, { 14 | foreignKey: 'order_id', 15 | as: 'items', 16 | onDelete: 'CASCADE' 17 | }) 18 | } 19 | }; 20 | Order.init({ 21 | user_id: DataTypes.INTEGER, 22 | total: DataTypes.FLOAT 23 | }, { 24 | sequelize, 25 | modelName: 'Order', 26 | }); 27 | return Order; 28 | }; -------------------------------------------------------------------------------- /hello-market-server/models/orderitem.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class OrderItem extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | 14 | // Define a belongsTo association with Product 15 | OrderItem.belongsTo(models.Product, { 16 | foreignKey: 'product_id', 17 | as: 'product', // Alias for accessing associated product 18 | }); 19 | } 20 | }; 21 | OrderItem.init({ 22 | product_id: DataTypes.INTEGER, 23 | quantity: DataTypes.INTEGER, 24 | order_id: DataTypes.INTEGER 25 | }, { 26 | sequelize, 27 | modelName: 'OrderItem', 28 | }); 29 | return OrderItem; 30 | }; -------------------------------------------------------------------------------- /hello-market-server/models/product.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class Product extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | } 15 | } 16 | Product.init({ 17 | name: DataTypes.STRING, 18 | description: DataTypes.TEXT, 19 | price: DataTypes.FLOAT, 20 | photo_url: DataTypes.STRING, 21 | user_id: DataTypes.INTEGER 22 | }, { 23 | sequelize, 24 | modelName: 'Product', 25 | }); 26 | return Product; 27 | }; -------------------------------------------------------------------------------- /hello-market-server/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const { 3 | Model 4 | } = require('sequelize'); 5 | module.exports = (sequelize, DataTypes) => { 6 | class User extends Model { 7 | /** 8 | * Helper method for defining associations. 9 | * This method is not a part of Sequelize lifecycle. 10 | * The `models/index` file will call this method automatically. 11 | */ 12 | static associate(models) { 13 | // define association here 14 | } 15 | } 16 | User.init({ 17 | username: DataTypes.STRING, 18 | password: DataTypes.STRING, 19 | first_name: DataTypes.STRING, 20 | last_name: DataTypes.STRING, 21 | street: DataTypes.STRING, 22 | city: DataTypes.STRING, 23 | state: DataTypes.STRING, 24 | country: DataTypes.STRING, 25 | zip_code: DataTypes.STRING, 26 | }, { 27 | sequelize, 28 | modelName: 'User', 29 | }); 30 | return User; 31 | }; -------------------------------------------------------------------------------- /hello-market-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-market-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bcryptjs": "^2.4.3", 13 | "cors": "^2.8.5", 14 | "dotenv": "^16.4.5", 15 | "express": "^4.19.2", 16 | "express-validator": "^7.2.0", 17 | "jsonwebtoken": "^9.0.2", 18 | "multer": "^1.4.5-lts.1", 19 | "pg": "^8.12.0", 20 | "sequelize": "^6.37.3", 21 | "stripe": "^17.4.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /hello-market-server/public/images/chair.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/public/images/chair.jpg -------------------------------------------------------------------------------- /hello-market-server/public/images/sofa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/public/images/sofa.jpg -------------------------------------------------------------------------------- /hello-market-server/routes/auth.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const authController = require('../controllers/authenticationController') 4 | const { registerValidator, loginValidator } = require('../utils/validators/validators'); 5 | const validationErrorsMiddleware = require('../middlewares/validationErrorsMiddleware'); 6 | 7 | router.post('/login', loginValidator, validationErrorsMiddleware, authController.login); 8 | router.post('/register', registerValidator, validationErrorsMiddleware, authController.register); 9 | 10 | module.exports = router -------------------------------------------------------------------------------- /hello-market-server/routes/cart.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const cartController = require('../controllers/cartController'); 5 | const { addCartItemValidator } = require('../utils/validators/validators'); 6 | const validationErrorsMiddleware = require('../middlewares/validationErrorsMiddleware'); 7 | const authenticate = require('../middlewares/authMiddleware'); 8 | 9 | // Add a cart item 10 | router.post( 11 | '/items', 12 | authenticate, 13 | addCartItemValidator, 14 | validationErrorsMiddleware, 15 | cartController.addCartItem 16 | ); 17 | 18 | // Load cart 19 | router.get( 20 | '/', 21 | validationErrorsMiddleware, 22 | cartController.loadCart 23 | ); 24 | 25 | // Delete a cart item 26 | router.delete( 27 | '/item/:cartItemId', 28 | validationErrorsMiddleware, 29 | cartController.removeCartItem 30 | ); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /hello-market-server/routes/order.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express') 3 | const router = express.Router() 4 | const orderController = require('../controllers/orderController'); 5 | const authenticate = require('../middlewares/authMiddleware'); 6 | const { validateCreateOrder } = require('../utils/validators/validators'); 7 | const validationErrorsMiddleware = require('../middlewares/validationErrorsMiddleware'); 8 | 9 | // Load all orders 10 | router.get('/', orderController.loadOrders) 11 | 12 | // Create a new order 13 | router.post( 14 | '/', 15 | validateCreateOrder, 16 | validationErrorsMiddleware, 17 | orderController.createOrder 18 | ); 19 | 20 | module.exports = router -------------------------------------------------------------------------------- /hello-market-server/routes/payment.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | 4 | const paymentController = require('../controllers/paymentController'); 5 | 6 | router.post('/create-payment-intent', paymentController.createPaymentIntent) 7 | 8 | module.exports = router -------------------------------------------------------------------------------- /hello-market-server/routes/product.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const router = express.Router() 3 | const productController = require('../controllers/productController') 4 | const { createProductValidator, deleteProductValidator, updateProductValidator } = require('../utils/validators/validators'); 5 | const authenticate = require('../middlewares/authMiddleware'); 6 | const validationErrorsMiddleware = require('../middlewares/validationErrorsMiddleware'); 7 | 8 | // Create a new product 9 | router.post( 10 | '/', 11 | authenticate, 12 | createProductValidator, 13 | validationErrorsMiddleware, 14 | productController.create 15 | ); 16 | 17 | // Get all products 18 | router.get('/', productController.getAllProducts); 19 | 20 | // Get products for a specific user 21 | router.get( 22 | '/user', 23 | authenticate, 24 | productController.getMyProducts 25 | ); 26 | 27 | // Upload product data 28 | router.post( 29 | '/upload', 30 | authenticate, 31 | productController.upload 32 | ); 33 | 34 | // Delete a product by ID 35 | router.delete( 36 | '/:productId', 37 | authenticate, 38 | deleteProductValidator, 39 | validationErrorsMiddleware, 40 | productController.deleteProduct 41 | ); 42 | // update product 43 | // /products/34 44 | router.put( 45 | '/:productId', 46 | authenticate, 47 | updateProductValidator, 48 | validationErrorsMiddleware, 49 | productController.updateProduct 50 | ) 51 | 52 | module.exports = router -------------------------------------------------------------------------------- /hello-market-server/routes/user.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const userController = require('../controllers/userController'); 5 | const { updateUserInfoValidator } = require('../utils/validators/validators'); 6 | const validationErrorsMiddleware = require('../middlewares/validationErrorsMiddleware'); 7 | const authenticate = require('../middlewares/authMiddleware'); 8 | 9 | // Update user information 10 | router.put( 11 | '/', 12 | updateUserInfoValidator, 13 | validationErrorsMiddleware, 14 | userController.updateUserInfo 15 | ); 16 | 17 | // Load user information 18 | router.get('/', userController.loadUserInfo); 19 | 20 | module.exports = router; 21 | -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740321897379.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740321897379.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740323920514.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740323920514.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740323928212.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740323928212.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740323941520.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740323941520.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740324041116.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740324041116.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740324062515.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740324062515.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740324220919.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740324220919.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740324417512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740324417512.png -------------------------------------------------------------------------------- /hello-market-server/uploads/image-1740324776665.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/hello-market-server/uploads/image-1740324776665.png -------------------------------------------------------------------------------- /hello-market-server/utils/fileUtils.js: -------------------------------------------------------------------------------- 1 | // fileUtils.js 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | // Function to extract file name from URL 6 | const getFileNameFromUrl = (photoUrl) => { 7 | try { 8 | const url = new URL(photoUrl); // Parse the URL 9 | const fileName = path.basename(url.pathname); // Get the file name from the path 10 | return fileName; 11 | } catch (error) { 12 | console.error('Invalid URL:', error); 13 | return null; 14 | } 15 | }; 16 | 17 | // Helper function to delete an image file 18 | const deleteImageFile = (fileName) => { 19 | return new Promise((resolve, reject) => { 20 | if (!fileName) { 21 | return resolve(); // No image to delete 22 | } 23 | 24 | const fullImagePath = path.join(__dirname, '../uploads', fileName); 25 | 26 | // Check if the file exists before trying to delete it 27 | fs.access(fullImagePath, fs.constants.F_OK, (accessErr) => { 28 | if (accessErr) { 29 | console.error('File does not exist:', fullImagePath); 30 | return resolve(); // If the file doesn't exist, we consider it successfully "deleted" 31 | } 32 | 33 | // If the file exists, delete it 34 | fs.unlink(fullImagePath, (unlinkErr) => { 35 | if (unlinkErr) { 36 | console.error('Error deleting image:', unlinkErr); 37 | return reject(unlinkErr); 38 | } else { 39 | console.log('Image deleted successfully'); 40 | return resolve(); 41 | } 42 | }); 43 | }); 44 | }); 45 | }; 46 | 47 | // Export both functions 48 | module.exports = { 49 | getFileNameFromUrl, 50 | deleteImageFile 51 | }; 52 | -------------------------------------------------------------------------------- /hello-market-server/utils/validators/validators.js: -------------------------------------------------------------------------------- 1 | const { body } = require('express-validator'); 2 | const { param } = require('express-validator'); 3 | 4 | const registerValidator = [ 5 | body('username', 'username cannot be empty.').not().isEmpty(), 6 | body('password', 'password cannot be empty.').not().isEmpty() 7 | ]; 8 | 9 | const loginValidator = [ 10 | body('username', 'username cannot be empty.').not().isEmpty(), 11 | body('password', 'password cannot be empty.').not().isEmpty() 12 | ]; 13 | 14 | const createProductValidator = [ 15 | body('name', 'name cannot be empty.').not().isEmpty(), 16 | body('description', 'description cannot be empty.').not().isEmpty(), 17 | body('price', 'price cannot be empty.').not().isEmpty(), 18 | body('photo_url') 19 | .notEmpty().withMessage('photoUrl cannot be empty.') 20 | ] 21 | 22 | const deleteProductValidator = [ 23 | param('productId') 24 | .notEmpty().withMessage('Product Id is required') 25 | .isNumeric().withMessage('Product Id must be a number') 26 | ] 27 | 28 | const updateProductValidator = [ 29 | param('productId'), 30 | body('name', 'name cannot be empty.').not().isEmpty(), 31 | body('description', 'description cannot be empty.').not().isEmpty(), 32 | body('price', 'price cannot be empty.').not().isEmpty(), 33 | body('photo_url') 34 | .notEmpty().withMessage('photoUrl cannot be empty.') 35 | ] 36 | 37 | const addCartItemValidator = [ 38 | body('product_id') 39 | .not() 40 | .isEmpty() 41 | .withMessage('Product Id cannot be empty.') 42 | .isInt() 43 | .withMessage('Product Id must be an integer.'), 44 | 45 | body('quantity') 46 | .not() 47 | .isEmpty() 48 | .withMessage('Quantity cannot be empty.') 49 | ]; 50 | 51 | const updateUserInfoValidator = [ 52 | body('first_name', 'First name cannot be empty.').notEmpty(), 53 | body('last_name', 'Last name cannot be empty.').notEmpty(), 54 | body('street', 'Street cannot be empty.').notEmpty(), 55 | body('city', 'City cannot be empty.').notEmpty(), 56 | body('state', 'State cannot be empty.').notEmpty(), 57 | body('zip_code', 'Zip code cannot be empty.').notEmpty(), 58 | body('country', 'Country cannot be empty.').notEmpty() 59 | ]; 60 | 61 | // Validation rules for createOrder 62 | const validateCreateOrder = [ 63 | body('total') 64 | .isFloat({ gt: 0 }) 65 | .withMessage('total must be a positive number'), 66 | body('order_items') 67 | .isArray({ min: 1 }) 68 | .withMessage('order_items must be a non-empty array'), 69 | body('order_items.*.product_id') 70 | .isInt({ gt: 0 }) 71 | .withMessage('Each order item must have a valid product_id'), 72 | body('order_items.*.quantity') 73 | .isInt({ gt: 0 }) 74 | .withMessage('Each order item must have a valid quantity greater than 0'), 75 | // Custom validation example 76 | body('order_items').custom((items) => { 77 | if (!Array.isArray(items) || items.length === 0) { 78 | throw new Error('order_items must be a non-empty array'); 79 | } 80 | items.forEach(item => { 81 | if (!item.product_id || !item.quantity) { 82 | throw new Error('Each order item must have a product_id and quantity'); 83 | } 84 | }); 85 | return true; 86 | }), 87 | ]; 88 | 89 | module.exports = { 90 | registerValidator, 91 | loginValidator, 92 | createProductValidator, 93 | deleteProductValidator, 94 | updateProductValidator, 95 | addCartItemValidator, 96 | updateUserInfoValidator, 97 | validateCreateOrder 98 | }; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HelloMarket", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /resources/e-commerce_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/resources/e-commerce_logo.png -------------------------------------------------------------------------------- /resources/hellomarket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/resources/hellomarket.png -------------------------------------------------------------------------------- /resources/logo.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/azamsharpschool/HelloMarket/df303f8a958a50e8c7945695cdc0723b19e46f28/resources/logo.jpeg --------------------------------------------------------------------------------