diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index b83ef307..0e6d5ed2 100644 Binary files a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate and b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift index 6e16ba31..53a969a0 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Models/User.swift @@ -53,11 +53,80 @@ struct User: Codable, Identifiable, Equatable { case id case email case name - case avatarUrl = "avatar_url" - case phoneNumber = "phone_number" - case isEmailVerified = "is_email_verified" - case createdAt = "created_at" - case updatedAt = "updated_at" + case firstName + case lastName + case avatarUrl + case phoneNumber + case isEmailVerified = "emailConfirmed" + case createdAt + case updatedAt + } + + // MARK: - Custom Decoding + + /// Custom decoder to handle firstName + lastName from API + /// Custom decoder để xử lý firstName + lastName từ API + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + id = try container.decode(String.self, forKey: .id) + email = try container.decode(String.self, forKey: .email) + + // Handle name from either "name" field or "firstName" + "lastName" + // Xử lý name từ field "name" hoặc "firstName" + "lastName" + if let fullName = try? container.decode(String.self, forKey: .name), !fullName.isEmpty { + name = fullName + } else { + let firstName = try container.decodeIfPresent(String.self, forKey: .firstName) ?? "" + let lastName = try container.decodeIfPresent(String.self, forKey: .lastName) ?? "" + name = "\(firstName) \(lastName)".trimmingCharacters(in: .whitespaces) + } + + avatarUrl = try container.decodeIfPresent(String.self, forKey: .avatarUrl) + phoneNumber = try container.decodeIfPresent(String.self, forKey: .phoneNumber) + isEmailVerified = try container.decodeIfPresent(Bool.self, forKey: .isEmailVerified) ?? false + createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) + updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) + } + + // MARK: - Custom Encoding + + /// Custom encoder for Encodable conformance + /// Custom encoder cho Encodable conformance + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(email, forKey: .email) + try container.encode(name, forKey: .name) + try container.encodeIfPresent(avatarUrl, forKey: .avatarUrl) + try container.encodeIfPresent(phoneNumber, forKey: .phoneNumber) + try container.encode(isEmailVerified, forKey: .isEmailVerified) + try container.encodeIfPresent(createdAt, forKey: .createdAt) + try container.encodeIfPresent(updatedAt, forKey: .updatedAt) + } + + // MARK: - Standard Init + + /// Standard initializer for creating User instances + /// Initializer chuẩn để tạo User instances + init( + id: String, + email: String, + name: String, + avatarUrl: String? = nil, + phoneNumber: String? = nil, + isEmailVerified: Bool = false, + createdAt: Date? = nil, + updatedAt: Date? = nil + ) { + self.id = id + self.email = email + self.name = name + self.avatarUrl = avatarUrl + self.phoneNumber = phoneNumber + self.isEmailVerified = isEmailVerified + self.createdAt = createdAt + self.updatedAt = updatedAt } } diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift index 962c477b..da355fe1 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/APIService.swift @@ -320,6 +320,4 @@ final class APIService: APIServiceProtocol { throw APIError.decodingError(error) } } - try await request(endpoint: endpoint, method: .delete, body: nil as String?, headers: nil) - } } diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift index c9ed56d2..90a15b19 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Services/AuthManager.swift @@ -111,76 +111,77 @@ final class AuthManager: ObservableObject { } } - /// Login with email and password - /// Đăng nhập với email và mật khẩu + /// Login with email and password using OAuth2 Password Grant + /// Đăng nhập với email và mật khẩu sử dụng OAuth2 Password Grant /// - Parameters: /// - email: User email / Email người dùng /// - password: User password / Mật khẩu người dùng @MainActor func login(email: String, password: String) async throws { - struct LoginRequest: Encodable { - let email: String - let password: String - } + // OAuth2 Password Grant + // OAuth2 Password Grant + let formData: [String: String] = [ + "grant_type": "password", + "username": email, + "password": password, + "scope": APIConfig.oauthScope + ] - struct LoginResponse: Decodable { - let accessToken: String - let refreshToken: String - let user: User - } - - let request = LoginRequest(email: email, password: password) - let response: LoginResponse = try await APIService.shared.post( - endpoint: "/auth/login", body: request) + let tokenResponse: OAuthTokenResponse = try await APIService.shared.postForm( + endpoint: APIConfig.tokenEndpoint, + formData: formData + ) // Save tokens to Keychain // Lưu tokens vào Keychain - KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken) - KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken) - - // Cache user data - // Cache dữ liệu user - if let userData = try? JSONEncoder().encode(response.user) { - UserDefaults.standard.set(userData, forKey: StorageKeys.userData) + KeychainHelper.save(key: StorageKeys.accessToken, value: tokenResponse.accessToken) + if let refreshToken = tokenResponse.refreshToken { + KeychainHelper.save(key: StorageKeys.refreshToken, value: refreshToken) } - authState = .authenticated(response.user) + // Fetch user info from API + // Lấy thông tin user từ API + await fetchCurrentUser() } /// Register new user /// Đăng ký người dùng mới /// - Parameters: - /// - name: User name / Tên người dùng + /// - firstName: User first name / Tên người dùng + /// - lastName: User last name / Họ người dùng /// - email: User email / Email người dùng /// - password: User password / Mật khẩu người dùng - @MainActor func register(name: String, email: String, password: String) async throws { + @MainActor func register(firstName: String, lastName: String, email: String, password: String) async throws { struct RegisterRequest: Encodable { - let name: String + let firstName: String + let lastName: String let email: String let password: String } struct RegisterResponse: Decodable { - let accessToken: String - let refreshToken: String - let user: User + let success: Bool + let data: RegisterData? } - let request = RegisterRequest(name: name, email: email, password: password) - let response: RegisterResponse = try await APIService.shared.post( - endpoint: "/auth/register", body: request) - - // Save tokens to Keychain - // Lưu tokens vào Keychain - KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken) - KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken) - - // Cache user data - // Cache dữ liệu user - if let userData = try? JSONEncoder().encode(response.user) { - UserDefaults.standard.set(userData, forKey: StorageKeys.userData) + struct RegisterData: Decodable { + let userId: String + let email: String } - authState = .authenticated(response.user) + let request = RegisterRequest( + firstName: firstName, + lastName: lastName, + email: email, + password: password + ) + let _: RegisterResponse = try await APIService.shared.post( + endpoint: "/auth/register", + body: request + ) + + // Auto login after successful registration + // Tự động đăng nhập sau khi đăng ký thành công + try await login(email: email, password: password) } /// Logout current user @@ -211,11 +212,11 @@ final class AuthManager: ObservableObject { } } - /// Refresh current user from API - /// Làm mới thông tin user từ API - @MainActor func refreshCurrentUser() async { + /// Fetch current user from API + /// Lấy thông tin user hiện tại từ API + @MainActor func fetchCurrentUser() async { do { - let user: User = try await APIService.shared.get(endpoint: "/auth/me") + let user: User = try await APIService.shared.get(endpoint: "/users/me") // Cache user data // Cache dữ liệu user @@ -225,39 +226,46 @@ final class AuthManager: ObservableObject { authState = .authenticated(user) } catch { - print("Failed to refresh user: \(error)") + print("Failed to fetch user: \(error)") authState = .unauthenticated } } + /// Refresh current user from cache or API + /// Làm mới thông tin user từ cache hoặc API + @MainActor func refreshCurrentUser() async { + await fetchCurrentUser() + } + // MARK: - Private Methods - /// Refresh access token using refresh token - /// Làm mới access token sử dụng refresh token + /// Refresh access token using OAuth2 refresh_token grant + /// Làm mới access token sử dụng OAuth2 refresh_token grant /// - Returns: Whether refresh was successful / Refresh có thành công không @MainActor private func refreshTokens() async -> Bool { guard let refreshToken = refreshToken else { return false } - struct RefreshRequest: Encodable { - let refreshToken: String - } - - struct RefreshResponse: Decodable { - let accessToken: String - let refreshToken: String - } + // OAuth2 Refresh Token Grant + // OAuth2 Refresh Token Grant + let formData: [String: String] = [ + "grant_type": "refresh_token", + "refresh_token": refreshToken + ] do { - let request = RefreshRequest(refreshToken: refreshToken) - let response: RefreshResponse = try await APIService.shared.post( - endpoint: "/auth/refresh", body: request) + let response: OAuthTokenResponse = try await APIService.shared.postForm( + endpoint: APIConfig.tokenEndpoint, + formData: formData + ) // Save new tokens // Lưu tokens mới KeychainHelper.save(key: StorageKeys.accessToken, value: response.accessToken) - KeychainHelper.save(key: StorageKeys.refreshToken, value: response.refreshToken) + if let newRefreshToken = response.refreshToken { + KeychainHelper.save(key: StorageKeys.refreshToken, value: newRefreshToken) + } return true } catch { diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift index 327fd70e..825b6dbd 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/ViewModels/AuthViewModel.swift @@ -144,13 +144,8 @@ final class AuthViewModel: ObservableObject { // MARK: - Actions - // MARK: Mock Credentials (for testing) - // Thông tin mock để test - private let mockEmail = "admin@goodgo.com" - private let mockPassword = "123456" - - /// Perform login - /// Thực hiện đăng nhập + /// Perform login with IAM Service + /// Thực hiện đăng nhập với IAM Service func login() async { guard isLoginValid else { errorMessage = "Vui lòng nhập email và mật khẩu hợp lệ" @@ -160,47 +155,6 @@ final class AuthViewModel: ObservableObject { isLoading = true errorMessage = nil - // Mock login for testing - // Đăng nhập mock để test - if loginEmail.lowercased() == mockEmail && loginPassword == mockPassword { - // Simulate network delay - // Giả lập delay mạng - try? await Task.sleep(nanoseconds: 1_000_000_000) - - // Create mock user and authenticate - // Tạo mock user và xác thực - let mockUser = User( - id: "admin-001", - email: mockEmail, - name: "Admin GoodGo", - avatarUrl: nil, - phoneNumber: "+84901234567", - isEmailVerified: true, - createdAt: Date(), - updatedAt: Date() - ) - - // Save mock tokens and user - // Lưu mock tokens và user - KeychainHelper.save(key: StorageKeys.accessToken, value: "mock_access_token_\(UUID().uuidString)") - KeychainHelper.save(key: StorageKeys.refreshToken, value: "mock_refresh_token_\(UUID().uuidString)") - - if let userData = try? JSONEncoder().encode(mockUser) { - UserDefaults.standard.set(userData, forKey: StorageKeys.userData) - } - - // Update auth state - // Cập nhật trạng thái auth - await MainActor.run { - AuthManager.shared.setAuthenticated(user: mockUser) - } - - isLoading = false - return - } - - // Real API login - // Đăng nhập API thật do { try await AuthManager.shared.login(email: loginEmail, password: loginPassword) } catch { @@ -210,8 +164,8 @@ final class AuthViewModel: ObservableObject { isLoading = false } - /// Perform registration - /// Thực hiện đăng ký + /// Perform registration with IAM Service + /// Thực hiện đăng ký với IAM Service func register() async { guard isRegisterValid else { errorMessage = "Vui lòng kiểm tra lại thông tin đăng ký" @@ -222,8 +176,15 @@ final class AuthViewModel: ObservableObject { errorMessage = nil do { + // Parse name into firstName/lastName + // Tách name thành firstName/lastName + let names = registerName.split(separator: " ", maxSplits: 1) + let firstName = String(names.first ?? "") + let lastName = names.count > 1 ? String(names.last ?? "") : "" + try await AuthManager.shared.register( - name: registerName, + firstName: firstName, + lastName: lastName, email: registerEmail, password: registerPassword ) @@ -234,6 +195,7 @@ final class AuthViewModel: ObservableObject { isLoading = false } + /// Send forgot password email /// Gửi email quên mật khẩu func forgotPassword() async { diff --git a/apps/app-client-base-swift/README.md b/apps/app-client-base-swift/README.md index 1277757f..1493d92c 100644 --- a/apps/app-client-base-swift/README.md +++ b/apps/app-client-base-swift/README.md @@ -1,291 +1,56 @@ -# App Client Base Swift / Ứng Dụng Client iOS +# App Client Base Swift -> **EN**: Native iOS client application for GoodGo platform, built with Swift and SwiftUI following MVVM architecture. -> **VI**: Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI theo kiến trúc MVVM. +> **EN**: Native iOS client for GoodGo platform | **VI**: Ứng dụng iOS native cho nền tảng GoodGo -## 📱 Features / Tính Năng +## 📱 Overview / Tổng Quan -| Feature / Tính năng | Description / Mô tả | -|---------------------|---------------------| -| 🔐 Authentication | Login, Register, Forgot Password với form validation | -| 🏠 Home Dashboard | Greeting động, Featured items, Activity feed | -| 🔍 Explore | Khám phá địa điểm và dịch vụ | -| 👤 Profile | Quản lý thông tin cá nhân và cài đặt | -| 🌓 Dark Mode | Hỗ trợ chế độ tối tự động | -| 🌐 i18n | Đa ngôn ngữ (Tiếng Việt & English) | +iOS application built with Swift 5.9+ and SwiftUI following MVVM architecture. -## 🛠️ Tech Stack / Công Nghệ - -| Technology | Version | Purpose / Mục đích | -|------------|---------|-------------------| -| Swift | 5.9+ | Primary language / Ngôn ngữ chính | -| SwiftUI | iOS 15+ | Declarative UI framework | -| Xcode | 15.0+ | IDE development | -| URLSession | Native | HTTP networking | -| Keychain | Native | Secure token storage / Lưu trữ token bảo mật | -| Combine | Native | Reactive programming | - -## � Prerequisites / Yêu Cầu - -- **macOS**: 14.0+ (Sonoma) -- **Xcode**: 15.0+ -- **iOS Target**: 15.0+ -- **Apple Developer Account**: Required for device deployment / Cần thiết cho deploy lên thiết bị +Ứng dụng iOS xây dựng bằng Swift 5.9+ và SwiftUI theo kiến trúc MVVM. ## 🚀 Quick Start / Bắt Đầu Nhanh -### 1. Clone và mở project ```bash -cd apps/app-client-base-swift open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +# Press ⌘R to build and run ``` -### 2. Chọn Simulator -- Xcode menu: **Product > Destination > iPhone 15 Pro** (hoặc simulator khác) +**Mock Login:** `admin@goodgo.com` / `123456` -### 3. Build và Run -```bash -# Sử dụng shortcut -⌘R (Command + R) +## 📚 Documentation / Tài Liệu -# Hoặc từ terminal -xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ - -scheme AppClientBaseSwift \ - -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ - build -``` +| Language | Links | +|----------|-------| +| 🇬🇧 English | [README](./docs/en/README.md) • [Architecture](./docs/en/architecture.md) | +| 🇻🇳 Tiếng Việt | [README](./docs/vi/README.md) • [Kiến trúc](./docs/vi/architecture.md) | -### 4. Mock Login (để test) -``` -Email: admin@goodgo.com -Password: 123456 -``` +## 🛠️ Tech Stack -## 📂 Project Structure / Cấu Trúc Project +| Technology | Purpose | +|------------|---------| +| Swift 5.9+ | Primary language | +| SwiftUI | Declarative UI | +| URLSession | HTTP networking | +| Keychain | Secure storage | + +## 📂 Structure / Cấu Trúc ``` AppClientBaseSwift/ -├── App/ -│ └── AppClientBaseSwiftApp.swift # @main entry point -│ -├── Core/ -│ ├── Constants/ -│ │ └── Constants.swift # API, App, Storage, DesignSystem -│ └── Extensions/ -│ ├── View+Extensions.swift # SwiftUI modifiers -│ └── String+Extensions.swift # Validation, formatting -│ -├── Models/ -│ └── User.swift # User entity + extensions -│ -├── ViewModels/ # MVVM ViewModels -│ ├── AuthViewModel.swift # Login/Register/ForgotPassword -│ ├── HomeViewModel.swift # Home screen logic -│ └── ProfileViewModel.swift # Profile management -│ -├── Views/ -│ ├── Auth/ # Authentication screens -│ │ ├── AuthContainerView.swift # Auth navigation container -│ │ ├── LoginView.swift # Login UI -│ │ ├── RegisterView.swift # Registration UI -│ │ └── ForgotPasswordView.swift # Password reset UI -│ │ -│ ├── Home/ # Home components -│ │ ├── WalletCard.swift # Wallet balance card -│ │ ├── PromoCarousel.swift # Promotions carousel -│ │ ├── ServiceGrid.swift # Services grid -│ │ └── ActivityFeed.swift # Recent activities -│ │ -│ └── Screens/ # Main screens -│ ├── ContentView.swift # Root container + TabBar -│ ├── SplashView.swift # Splash animation -│ ├── WelcomeView.swift # Onboarding -│ ├── HomeView.swift # Home tab -│ ├── ExploreView.swift # Explore tab -│ └── ProfileView.swift # Profile tab -│ -├── Services/ -│ ├── APIService.swift # HTTP client với URLSession -│ └── AuthManager.swift # Auth state + Keychain -│ -└── Resources/ - ├── Assets.xcassets/ # Images & Colors - ├── en.lproj/ # English localization - └── vi.lproj/ # Vietnamese localization +├── App/ # Entry point +├── Core/ # Constants, Extensions +├── Models/ # Data models +├── ViewModels/ # MVVM ViewModels +├── Views/ # SwiftUI Views +├── Services/ # API, Auth +└── Resources/ # Assets, Localization ``` -## 🎨 Architecture / Kiến Trúc +## 🔗 Related / Liên Quan -### MVVM Pattern +- [app-client-base-net](../app-client-base-net) - .NET MAUI client +- [iam-service-net](../../services/iam-service-net) - Auth backend -``` -┌─────────────────────────────────────────────────────────────┐ -│ VIEW (SwiftUI) │ -│ HomeView, ProfileView, AuthContainerView, LoginView... │ -├─────────────────────────────────────────────────────────────┤ -│ @StateObject / @EnvironmentObject │ -│ │ │ -├────────────────────────────▼────────────────────────────────┤ -│ VIEWMODEL (ObservableObject) │ -│ HomeViewModel, AuthViewModel, ProfileViewModel │ -│ • @Published properties for reactive UI │ -│ • async/await methods for data loading │ -│ • Business logic and validation │ -├─────────────────────────────────────────────────────────────┤ -│ Protocol-based Dependency Injection │ -│ │ │ -├────────────────────────────▼────────────────────────────────┤ -│ SERVICES │ -│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ -└─────────────────────────────────────────────────────────────┘ -``` +--- -### Authentication Flow - -```mermaid -stateDiagram-v2 - [*] --> SplashScreen - SplashScreen --> CheckAuth: App Launch - CheckAuth --> Authenticated: Token Valid - CheckAuth --> Unauthenticated: No Token - Unauthenticated --> Login - Login --> Authenticated: Success - Login --> Register: Sign Up - Register --> Authenticated: Success - Authenticated --> HomeScreen - HomeScreen --> Unauthenticated: Logout -``` - -### Data Flow - -``` -User Action → View → ViewModel.method() → Service.request() → API - ↓ - @Published update - ↓ - View rerender -``` - -## 📋 Coding Conventions / Quy Ước Code - -### File Structure -```swift -// MARK: - Imports -import SwiftUI - -// MARK: - Type Definition -/// Description in English -/// Mô tả bằng tiếng Việt -struct/class/enum TypeName { - - // MARK: - Properties - - // MARK: - Init - - // MARK: - Public Methods - - // MARK: - Private Methods -} - -// MARK: - Extensions - -// MARK: - Preview Provider (DEBUG only) -``` - -### ViewModel Pattern -```swift -@MainActor -final class FeatureViewModel: ObservableObject { - // Published properties for UI binding - @Published var isLoading = false - @Published var errorMessage: String? - @Published var data: [Model] = [] - - // Dependencies via init - private let apiService: APIServiceProtocol - - init(apiService: APIServiceProtocol = APIService.shared) { - self.apiService = apiService - } - - // Async methods - func loadData() async { - isLoading = true - defer { isLoading = false } - - do { - data = try await apiService.get(endpoint: "/data") - } catch { - errorMessage = error.localizedDescription - } - } -} -``` - -### Bilingual Comments -```swift -/// Load user profile data -/// Tải dữ liệu hồ sơ người dùng -func loadProfile() async { } -``` - -## ⚙️ Configuration / Cấu Hình - -### API Configuration -```swift -// Core/Constants/Constants.swift -enum APIConfig { - static let baseURL = "https://api.goodgo.vn" - static let apiVersion = "/api/v1" - static let timeout: TimeInterval = 30.0 -} -``` - -### Environment Variables -| Key | Description / Mô tả | Default | -|-----|---------------------|---------| -| `API_BASE_URL` | Backend API URL | `https://api.goodgo.vn` | -| `API_VERSION` | API version prefix | `/api/v1` | - -## 🧪 Testing / Kiểm Thử - -### Run Unit Tests -```bash -xcodebuild test \ - -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ - -scheme AppClientBaseSwift \ - -destination 'platform=iOS Simulator,name=iPhone 15 Pro' -``` - -### Test Plan -Located at: `AppClientBaseSwift.xctestplan` - -## 🔐 Security / Bảo Mật - -| Feature | Implementation / Triển khai | -|---------|----------------------------| -| Token Storage | Keychain Services (not UserDefaults) | -| Secure Requests | HTTPS only, Bearer token auth | -| Session Management | Auto token refresh, secure logout | -| Data Protection | Sensitive data encrypted at rest | - -## 📱 Supported Devices / Thiết Bị Hỗ Trợ - -- **iPhone**: 8 and later (iOS 15+) -- **iPad**: All iPads with iOS 15+ -- **Orientations**: Portrait (primary), Landscape (supported) - -## 🔗 Related Projects / Dự Án Liên Quan - -- [app-client-base-net](../app-client-base-net) - .NET MAUI cross-platform client -- [iam-service-net](../../services/iam-service-net) - Authentication backend -- [web-client](../web-client) - Web application - -## 📚 Additional Documentation / Tài Liệu Bổ Sung - -- [ARCHITECTURE.md](./ARCHITECTURE.md) - Chi tiết kiến trúc và design decisions -- [Swift Enterprise Skills](../../.agent/skills/swift-enterprise-architect/SKILL.md) - Swift development guidelines - -## 📄 License - -Copyright © 2026 GoodGo. All rights reserved. +**Copyright © 2026 GoodGo. All rights reserved.** diff --git a/apps/app-client-base-swift/docs/README.md b/apps/app-client-base-swift/docs/README.md new file mode 100644 index 00000000..357457da --- /dev/null +++ b/apps/app-client-base-swift/docs/README.md @@ -0,0 +1,33 @@ +# Documentation / Tài Liệu + +## Languages / Ngôn Ngữ + +| Language | Documentation | +|----------|---------------| +| 🇬🇧 English | [docs/en/](./en/README.md) | +| 🇻🇳 Tiếng Việt | [docs/vi/](./vi/README.md) | + +## Structure / Cấu Trúc + +``` +docs/ +├── en/ # English documentation +│ ├── README.md # Quick start guide +│ └── architecture.md # Architecture details +│ +├── vi/ # Vietnamese documentation +│ ├── README.md # Hướng dẫn bắt đầu nhanh +│ └── architecture.md # Chi tiết kiến trúc +│ +└── README.md # This index file +``` + +## Quick Links / Liên Kết Nhanh + +### English +- [Getting Started](./en/README.md) +- [Architecture Guide](./en/architecture.md) + +### Tiếng Việt +- [Bắt Đầu Nhanh](./vi/README.md) +- [Hướng Dẫn Kiến Trúc](./vi/architecture.md) diff --git a/apps/app-client-base-swift/docs/en/README.md b/apps/app-client-base-swift/docs/en/README.md new file mode 100644 index 00000000..0537cf79 --- /dev/null +++ b/apps/app-client-base-swift/docs/en/README.md @@ -0,0 +1,207 @@ +# App Client Base Swift + +> Native iOS client application for GoodGo platform, built with Swift and SwiftUI following MVVM architecture. + +## 📱 Features + +| Feature | Description | +|---------|-------------| +| 🔐 Authentication | Login, Register, Forgot Password with form validation | +| 🏠 Home Dashboard | Dynamic greeting, Featured items, Activity feed | +| 🔍 Explore | Discover locations and services | +| 👤 Profile | User profile management and settings | +| 🌓 Dark Mode | Automatic dark mode support | +| 🌐 i18n | Multi-language support (Vietnamese & English) | + +## 🛠️ Tech Stack + +| Technology | Version | Purpose | +|------------|---------|---------| +| Swift | 5.9+ | Primary language | +| SwiftUI | iOS 15+ | Declarative UI framework | +| Xcode | 15.0+ | IDE development | +| URLSession | Native | HTTP networking | +| Keychain | Native | Secure token storage | +| Combine | Native | Reactive programming | + +## 📋 Prerequisites + +- **macOS**: 14.0+ (Sonoma) +- **Xcode**: 15.0+ +- **iOS Target**: 15.0+ +- **Apple Developer Account**: Required for device deployment + +## 🚀 Quick Start + +### 1. Clone and open project +```bash +cd apps/app-client-base-swift +open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +``` + +### 2. Select Simulator +- Xcode menu: **Product > Destination > iPhone 15 Pro** (or another simulator) + +### 3. Build and Run +```bash +# Using shortcut +⌘R (Command + R) + +# Or from terminal +xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ + build +``` + +### 4. Mock Login (for testing) +``` +Email: admin@goodgo.com +Password: 123456 +``` + +## 📂 Project Structure + +``` +AppClientBaseSwift/ +├── App/ +│ └── AppClientBaseSwiftApp.swift # @main entry point +│ +├── Core/ +│ ├── Constants/ +│ │ └── Constants.swift # API, App, Storage, DesignSystem +│ └── Extensions/ +│ ├── View+Extensions.swift # SwiftUI modifiers +│ └── String+Extensions.swift # Validation, formatting +│ +├── Models/ +│ └── User.swift # User entity + extensions +│ +├── ViewModels/ # MVVM ViewModels +│ ├── AuthViewModel.swift # Login/Register/ForgotPassword +│ ├── HomeViewModel.swift # Home screen logic +│ └── ProfileViewModel.swift # Profile management +│ +├── Views/ +│ ├── Auth/ # Authentication screens +│ ├── Home/ # Home components +│ └── Screens/ # Main screens +│ +├── Services/ +│ ├── APIService.swift # HTTP client with URLSession +│ └── AuthManager.swift # Auth state + Keychain +│ +└── Resources/ + ├── Assets.xcassets/ # Images & Colors + ├── en.lproj/ # English localization + └── vi.lproj/ # Vietnamese localization +``` + +## 🎨 Architecture + +### MVVM Pattern + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VIEW (SwiftUI) │ +│ HomeView, ProfileView, AuthContainerView, LoginView... │ +├─────────────────────────────────────────────────────────────┤ +│ @StateObject / @EnvironmentObject │ +├─────────────────────────────────────────────────────────────┤ +│ VIEWMODEL (ObservableObject) │ +│ HomeViewModel, AuthViewModel, ProfileViewModel │ +│ • @Published properties for reactive UI │ +│ • async/await methods for data loading │ +├─────────────────────────────────────────────────────────────┤ +│ Protocol-based Dependency Injection │ +├─────────────────────────────────────────────────────────────┤ +│ SERVICES │ +│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 📋 Coding Conventions + +### File Structure +```swift +// MARK: - Imports +import SwiftUI + +// MARK: - Type Definition +/// Description in English +struct/class/enum TypeName { + // MARK: - Properties + // MARK: - Init + // MARK: - Public Methods + // MARK: - Private Methods +} +``` + +### ViewModel Pattern +```swift +@MainActor +final class FeatureViewModel: ObservableObject { + @Published var isLoading = false + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + init(apiService: APIServiceProtocol = APIService.shared) { + self.apiService = apiService + } + + func loadData() async { + isLoading = true + defer { isLoading = false } + // ... + } +} +``` + +## ⚙️ Configuration + +### API Configuration +```swift +enum APIConfig { + static let baseURL = "https://api.goodgo.vn" + static let apiVersion = "/api/v1" + static let timeout: TimeInterval = 30.0 +} +``` + +## 🧪 Testing + +```bash +xcodebuild test \ + -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +## 🔐 Security + +| Feature | Implementation | +|---------|----------------| +| Token Storage | Keychain Services (not UserDefaults) | +| Secure Requests | HTTPS only, Bearer token auth | +| Session Management | Auto token refresh, secure logout | +| Data Protection | Sensitive data encrypted at rest | + +## 📱 Supported Devices + +- **iPhone**: 8 and later (iOS 15+) +- **iPad**: All iPads with iOS 15+ +- **Orientations**: Portrait (primary), Landscape (supported) + +## 🔗 Related Projects + +- [app-client-base-net](../app-client-base-net) - .NET MAUI cross-platform client +- [iam-service-net](../../services/iam-service-net) - Authentication backend + +## 📚 Additional Documentation + +- [Architecture Guide](./architecture.md) - Detailed architecture and design decisions + +## 📄 License + +Copyright © 2026 GoodGo. All rights reserved. diff --git a/apps/app-client-base-swift/ARCHITECTURE.md b/apps/app-client-base-swift/docs/en/architecture.md similarity index 66% rename from apps/app-client-base-swift/ARCHITECTURE.md rename to apps/app-client-base-swift/docs/en/architecture.md index aebfdf68..6d24d574 100644 --- a/apps/app-client-base-swift/ARCHITECTURE.md +++ b/apps/app-client-base-swift/docs/en/architecture.md @@ -1,9 +1,8 @@ -# Architecture / Kiến Trúc +# Architecture Guide -> **EN**: Detailed architecture documentation for AppClientBaseSwift iOS application. -> **VI**: Tài liệu kiến trúc chi tiết cho ứng dụng iOS AppClientBaseSwift. +> Detailed architecture documentation for AppClientBaseSwift iOS application. -## Overview / Tổng Quan +## Overview AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-ViewModel)** architecture pattern with **SwiftUI** for declarative UI. The app follows Apple's modern development best practices including: @@ -12,7 +11,7 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi - **Protocol-oriented programming** for testability - **Keychain Services** for secure storage -## Architecture Diagram / Sơ Đồ Kiến Trúc +## Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────────────┐ @@ -26,57 +25,42 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi │ │ │ LoginView │ │RegisterView │ │ForgotPasswd │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ @StateObject / @EnvironmentObject │ -│ ▼ │ +│ @StateObject / @EnvironmentObject │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ ViewModels (@MainActor) │ │ │ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ │ │ │ AuthViewModel │ │ HomeViewModel │ │ProfileViewModel│ │ │ -│ │ │ @Published │ │ @Published │ │ @Published │ │ │ -│ │ │ - isLoading │ │ - items │ │ - user │ │ │ -│ │ │ - errorMessage │ │ - greeting │ │ - isEditing │ │ │ │ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - │ Dependency Injection (Protocol-based) - ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ SERVICE LAYER │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ APIService │ │ AuthManager │ │ -│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ -│ │ │ APIServiceProtocol │ │ │ │ @Published authState │ │ │ -│ │ │ - request() │ │ │ │ - login() │ │ │ -│ │ │ - get(), post() │ │ │ │ - register() │ │ │ -│ │ │ - put(), delete() │ │ │ │ - logout() │ │ │ -│ │ └───────────────────────┘ │ │ │ - refreshToken() │ │ │ -│ │ URLSession │ │ │ Keychain │ │ │ +│ │ • request() │ │ • @Published authState │ │ +│ │ • get(), post() │ │ • login(), register() │ │ +│ │ • URLSession │ │ • Keychain storage │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ DATA LAYER │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ Models │ │ Constants │ │ -│ │ ┌───────────────────────┐ │ │ ┌───────────────────────┐ │ │ -│ │ │ User (Codable) │ │ │ │ APIConfig │ │ │ -│ │ │ HomeItem │ │ │ │ AppConstants │ │ │ -│ │ │ AuthState │ │ │ │ StorageKeys │ │ │ -│ │ └───────────────────────┘ │ │ │ DesignSystem │ │ │ +│ │ • User (Codable) │ │ • APIConfig │ │ +│ │ • HomeItem │ │ • StorageKeys │ │ +│ │ • AuthState │ │ • DesignSystem │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` -## Component Details / Chi Tiết Component +## Component Details ### 1. Presentation Layer #### Views -| Component | Responsibility / Trách nhiệm | -|-----------|------------------------------| +| Component | Responsibility | +|-----------|----------------| | `SplashView` | Animated splash screen, delayed navigation | | `ContentView` | Root TabView container, auth state routing | | `AuthContainerView` | Auth flow navigation (Login/Register/Forgot) | @@ -88,15 +72,12 @@ AppClientBaseSwift is a native iOS application built using **MVVM (Model-View-Vi ```swift @MainActor final class HomeViewModel: ObservableObject { - // Reactive properties @Published var isLoading: Bool = false @Published var items: [HomeItem] = [] @Published var errorMessage: String? - // Dependencies injected via init private let apiService: APIServiceProtocol - // Async methods using Swift Concurrency func loadData() async { ... } } ``` @@ -104,7 +85,7 @@ final class HomeViewModel: ObservableObject { ### 2. Service Layer #### APIService -HTTP client following **Single Responsibility Principle**: +HTTP client following Single Responsibility Principle: ```swift protocol APIServiceProtocol { @@ -122,7 +103,6 @@ protocol APIServiceProtocol { - Automatic JSON encoding/decoding (snake_case ↔ camelCase) - Bearer token injection - HTTP status code handling -- Error categorization #### AuthManager Singleton for authentication state: @@ -130,48 +110,20 @@ Singleton for authentication state: ```swift final class AuthManager: ObservableObject { @MainActor static let shared = AuthManager() - @Published var authState: AuthState = .unknown - - // Keychain-backed tokens - var accessToken: String? { get } - var refreshToken: String? { get } } ``` **AuthState Enum:** ```swift enum AuthState { - case unknown // Initial state / Trạng thái khởi tạo - case unauthenticated // Logged out / Chưa đăng nhập - case authenticated(User) // Logged in / Đã đăng nhập + case unknown // Initial state + case unauthenticated // Logged out + case authenticated(User) // Logged in } ``` -### 3. Data Layer - -#### Models -```swift -struct User: Codable, Identifiable, Equatable { - let id: String - let email: String - let name: String - let avatarUrl: String? - let phoneNumber: String? - let isEmailVerified: Bool - let createdAt: Date? - let updatedAt: Date? -} -``` - -#### Constants -Organized into semantic enums: -- `APIConfig`: Base URL, version, timeout -- `AppConstants`: App name, bundle ID, keychain service -- `StorageKeys`: UserDefaults/Keychain keys -- `DesignSystem`: Spacing, corner radius, font sizes - -## Data Flow / Luồng Dữ Liệu +## Data Flow ```mermaid sequenceDiagram @@ -190,47 +142,41 @@ sequenceDiagram VM-->>V: SwiftUI re-render ``` -## Authentication Flow / Luồng Xác Thực +## Authentication Flow ```mermaid stateDiagram-v2 [*] --> Unknown: App Launch - Unknown --> Authenticated: Token Found + Valid + Unknown --> Authenticated: Token Valid Unknown --> Unauthenticated: No Token - Unauthenticated --> Login: Show Login - Login --> Authenticated: Login Success + Unauthenticated --> Login + Login --> Authenticated: Success Login --> Register: Navigate - Register --> Authenticated: Register Success + Register --> Authenticated: Success - Authenticated --> HomeScreen: Show Main App + Authenticated --> HomeScreen HomeScreen --> Unauthenticated: Logout - - Authenticated --> TokenRefresh: Token Expired - TokenRefresh --> Authenticated: Refresh Success - TokenRefresh --> Unauthenticated: Refresh Failed ``` -## Design Decisions / Quyết Định Thiết Kế +## Design Decisions -### 1. Why MVVM? / Tại Sao MVVM? +### 1. Why MVVM? -| Benefit / Lợi ích | Description / Mô tả | -|-------------------|---------------------| -| Testability | ViewModel có thể test độc lập không cần UI | -| Separation of Concerns | View chỉ hiển thị, logic nằm ở ViewModel | -| SwiftUI Compatibility | `@ObservableObject` + `@Published` native | +| Benefit | Description | +|---------|-------------| +| Testability | ViewModel can be tested independently without UI | +| Separation of Concerns | View only displays, logic in ViewModel | +| SwiftUI Compatibility | `@ObservableObject` + `@Published` are native | | Reactive Updates | Combine-based automatic UI refresh | ### 2. Why Protocol-based DI? ```swift // Protocol enables mocking for tests -protocol APIServiceProtocol { - func get(endpoint: String) async throws -> T -} +protocol APIServiceProtocol { ... } -// Production implementation +// Production final class APIService: APIServiceProtocol { ... } // Test mock @@ -247,16 +193,11 @@ final class MockAPIService: APIServiceProtocol { ... } ### 4. Why @MainActor on ViewModels? -```swift -@MainActor -final class HomeViewModel: ObservableObject { ... } -``` - - Ensures all `@Published` updates happen on main thread - Prevents concurrency issues with SwiftUI - Explicit thread safety contract -## Security Architecture / Kiến Trúc Bảo Mật +## Security Architecture ``` ┌────────────────────────────────────────────────────────────┐ @@ -264,26 +205,22 @@ final class HomeViewModel: ObservableObject { ... } ├────────────────────────────────────────────────────────────┤ │ Layer 1: Transport Security (HTTPS/TLS) │ │ • All API calls use HTTPS │ -│ • Certificate pinning (TODO) │ ├────────────────────────────────────────────────────────────┤ │ Layer 2: Token Security (Keychain) │ │ • Access token stored in Keychain │ │ • Refresh token stored in Keychain │ -│ • kSecClassGenericPassword protection │ ├────────────────────────────────────────────────────────────┤ │ Layer 3: Session Security │ │ • Token expiry validation │ │ • Automatic token refresh │ -│ • Secure logout (clear all tokens) │ ├────────────────────────────────────────────────────────────┤ │ Layer 4: Input Validation │ │ • Email format validation │ │ • Password strength checking │ -│ • Form field sanitization │ └────────────────────────────────────────────────────────────┘ ``` -## Future Considerations / Hướng Phát Triển +## Future Considerations | Feature | Priority | Description | |---------|----------|-------------| @@ -291,11 +228,7 @@ final class HomeViewModel: ObservableObject { ... } | Biometric Auth | High | Face ID / Touch ID login | | Offline Mode | Medium | Local caching with SwiftData | | Push Notifications | Medium | APNs integration | -| Analytics | Low | Event tracking system | -## Related Documentation / Tài Liệu Liên Quan +## Related Documentation - [README.md](./README.md) - Quick start guide -- [Swift Enterprise Architect Skill](../../.agent/skills/swift-enterprise-architect/SKILL.md) -- [Swift Security Skill](../../.agent/skills/swift-security/SKILL.md) -- [Swift Networking Skill](../../.agent/skills/swift-networking/SKILL.md) diff --git a/apps/app-client-base-swift/docs/vi/README.md b/apps/app-client-base-swift/docs/vi/README.md new file mode 100644 index 00000000..3775a937 --- /dev/null +++ b/apps/app-client-base-swift/docs/vi/README.md @@ -0,0 +1,207 @@ +# App Client Base Swift + +> Ứng dụng iOS native cho nền tảng GoodGo, xây dựng bằng Swift và SwiftUI theo kiến trúc MVVM. + +## 📱 Tính Năng + +| Tính năng | Mô tả | +|-----------|-------| +| 🔐 Xác thực | Đăng nhập, Đăng ký, Quên mật khẩu với form validation | +| 🏠 Trang chủ | Lời chào động, Items nổi bật, Feed hoạt động | +| 🔍 Khám phá | Tìm kiếm địa điểm và dịch vụ | +| 👤 Hồ sơ | Quản lý thông tin cá nhân và cài đặt | +| 🌓 Chế độ tối | Hỗ trợ dark mode tự động | +| 🌐 Đa ngôn ngữ | Hỗ trợ Tiếng Việt & Tiếng Anh | + +## 🛠️ Công Nghệ + +| Công nghệ | Phiên bản | Mục đích | +|-----------|-----------|----------| +| Swift | 5.9+ | Ngôn ngữ chính | +| SwiftUI | iOS 15+ | UI Framework declarative | +| Xcode | 15.0+ | IDE phát triển | +| URLSession | Native | HTTP networking | +| Keychain | Native | Lưu trữ token bảo mật | +| Combine | Native | Reactive programming | + +## 📋 Yêu Cầu + +- **macOS**: 14.0+ (Sonoma) +- **Xcode**: 15.0+ +- **iOS Target**: 15.0+ +- **Tài khoản Apple Developer**: Cần thiết để deploy lên thiết bị + +## 🚀 Bắt Đầu Nhanh + +### 1. Clone và mở project +```bash +cd apps/app-client-base-swift +open AppClientBaseSwift/AppClientBaseSwift.xcodeproj +``` + +### 2. Chọn Simulator +- Menu Xcode: **Product > Destination > iPhone 15 Pro** (hoặc simulator khác) + +### 3. Build và Run +```bash +# Sử dụng shortcut +⌘R (Command + R) + +# Hoặc từ terminal +xcodebuild -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' \ + build +``` + +### 4. Mock Login (để test) +``` +Email: admin@goodgo.com +Password: 123456 +``` + +## 📂 Cấu Trúc Project + +``` +AppClientBaseSwift/ +├── App/ +│ └── AppClientBaseSwiftApp.swift # Entry point @main +│ +├── Core/ +│ ├── Constants/ +│ │ └── Constants.swift # API, App, Storage, DesignSystem +│ └── Extensions/ +│ ├── View+Extensions.swift # SwiftUI modifiers +│ └── String+Extensions.swift # Validation, formatting +│ +├── Models/ +│ └── User.swift # Entity User + extensions +│ +├── ViewModels/ # MVVM ViewModels +│ ├── AuthViewModel.swift # Login/Đăng ký/Quên mật khẩu +│ ├── HomeViewModel.swift # Logic màn hình Home +│ └── ProfileViewModel.swift # Quản lý hồ sơ +│ +├── Views/ +│ ├── Auth/ # Màn hình xác thực +│ ├── Home/ # Components trang chủ +│ └── Screens/ # Màn hình chính +│ +├── Services/ +│ ├── APIService.swift # HTTP client với URLSession +│ └── AuthManager.swift # Auth state + Keychain +│ +└── Resources/ + ├── Assets.xcassets/ # Hình ảnh & Màu sắc + ├── en.lproj/ # Bản địa hóa Tiếng Anh + └── vi.lproj/ # Bản địa hóa Tiếng Việt +``` + +## 🎨 Kiến Trúc + +### Pattern MVVM + +``` +┌─────────────────────────────────────────────────────────────┐ +│ VIEW (SwiftUI) │ +│ HomeView, ProfileView, AuthContainerView, LoginView... │ +├─────────────────────────────────────────────────────────────┤ +│ @StateObject / @EnvironmentObject │ +├─────────────────────────────────────────────────────────────┤ +│ VIEWMODEL (ObservableObject) │ +│ HomeViewModel, AuthViewModel, ProfileViewModel │ +│ • Thuộc tính @Published cho reactive UI │ +│ • Phương thức async/await để tải dữ liệu │ +├─────────────────────────────────────────────────────────────┤ +│ Dependency Injection dựa trên Protocol │ +├─────────────────────────────────────────────────────────────┤ +│ SERVICES │ +│ APIService (HTTP) • AuthManager (Auth State + Keychain) │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 📋 Quy Ước Code + +### Cấu trúc File +```swift +// MARK: - Imports +import SwiftUI + +// MARK: - Định nghĩa Type +/// Mô tả bằng tiếng Việt +struct/class/enum TypeName { + // MARK: - Properties + // MARK: - Init + // MARK: - Public Methods + // MARK: - Private Methods +} +``` + +### Pattern ViewModel +```swift +@MainActor +final class FeatureViewModel: ObservableObject { + @Published var isLoading = false + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + init(apiService: APIServiceProtocol = APIService.shared) { + self.apiService = apiService + } + + func loadData() async { + isLoading = true + defer { isLoading = false } + // ... + } +} +``` + +## ⚙️ Cấu Hình + +### Cấu hình API +```swift +enum APIConfig { + static let baseURL = "https://api.goodgo.vn" + static let apiVersion = "/api/v1" + static let timeout: TimeInterval = 30.0 +} +``` + +## 🧪 Kiểm Thử + +```bash +xcodebuild test \ + -project AppClientBaseSwift/AppClientBaseSwift.xcodeproj \ + -scheme AppClientBaseSwift \ + -destination 'platform=iOS Simulator,name=iPhone 15 Pro' +``` + +## 🔐 Bảo Mật + +| Tính năng | Triển khai | +|-----------|------------| +| Lưu trữ Token | Keychain Services (không dùng UserDefaults) | +| Request bảo mật | Chỉ HTTPS, xác thực Bearer token | +| Quản lý Session | Tự động refresh token, logout an toàn | +| Bảo vệ dữ liệu | Mã hóa dữ liệu nhạy cảm khi lưu trữ | + +## 📱 Thiết Bị Hỗ Trợ + +- **iPhone**: 8 trở lên (iOS 15+) +- **iPad**: Tất cả iPad với iOS 15+ +- **Hướng màn hình**: Portrait (chính), Landscape (hỗ trợ) + +## 🔗 Dự Án Liên Quan + +- [app-client-base-net](../app-client-base-net) - Client đa nền tảng .NET MAUI +- [iam-service-net](../../services/iam-service-net) - Backend xác thực + +## 📚 Tài Liệu Bổ Sung + +- [Hướng dẫn Kiến trúc](./architecture.md) - Chi tiết kiến trúc và quyết định thiết kế + +## 📄 Giấy Phép + +Bản quyền © 2026 GoodGo. Bảo lưu mọi quyền. diff --git a/apps/app-client-base-swift/docs/vi/architecture.md b/apps/app-client-base-swift/docs/vi/architecture.md new file mode 100644 index 00000000..1c287dd6 --- /dev/null +++ b/apps/app-client-base-swift/docs/vi/architecture.md @@ -0,0 +1,234 @@ +# Hướng Dẫn Kiến Trúc + +> Tài liệu kiến trúc chi tiết cho ứng dụng iOS AppClientBaseSwift. + +## Tổng Quan + +AppClientBaseSwift là ứng dụng iOS native được xây dựng theo mẫu kiến trúc **MVVM (Model-View-ViewModel)** với **SwiftUI** cho UI declarative. Ứng dụng tuân theo các best practices phát triển hiện đại của Apple bao gồm: + +- **Swift Concurrency** (async/await) +- **Combine** cho reactive data binding +- **Protocol-oriented programming** để tăng khả năng test +- **Keychain Services** cho lưu trữ bảo mật + +## Sơ Đồ Kiến Trúc + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP PRESENTATION │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ SwiftUI Views │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ SplashView │ │ HomeView │ │ ExploreView │ │ ProfileView │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ LoginView │ │RegisterView │ │ForgotPasswd │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ @StateObject / @EnvironmentObject │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ ViewModels (@MainActor) │ │ +│ │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ │ +│ │ │ AuthViewModel │ │ HomeViewModel │ │ProfileViewModel│ │ │ +│ │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ + Dependency Injection (dựa trên Protocol) +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP SERVICE │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ APIService │ │ AuthManager │ │ +│ │ • request() │ │ • @Published authState │ │ +│ │ • get(), post() │ │ • login(), register() │ │ +│ │ • URLSession │ │ • Lưu trữ Keychain │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────┐ +│ LỚP DATA │ +│ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ Models │ │ Constants │ │ +│ │ • User (Codable) │ │ • APIConfig │ │ +│ │ • HomeItem │ │ • StorageKeys │ │ +│ │ • AuthState │ │ • DesignSystem │ │ +│ └─────────────────────────────┘ └─────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +## Chi Tiết Component + +### 1. Lớp Presentation + +#### Views +| Component | Trách nhiệm | +|-----------|-------------| +| `SplashView` | Màn hình splash động, điều hướng trễ | +| `ContentView` | Container TabView gốc, routing theo auth state | +| `AuthContainerView` | Điều hướng luồng Auth (Login/Đăng ký/Quên MK) | +| `HomeView` | Tab Home với lời chào, promo, dịch vụ | +| `ExploreView` | Tính năng khám phá và tìm kiếm | +| `ProfileView` | Hồ sơ người dùng và cài đặt | + +#### ViewModels +```swift +@MainActor +final class HomeViewModel: ObservableObject { + @Published var isLoading: Bool = false + @Published var items: [HomeItem] = [] + @Published var errorMessage: String? + + private let apiService: APIServiceProtocol + + func loadData() async { ... } +} +``` + +### 2. Lớp Service + +#### APIService +HTTP client tuân theo nguyên tắc Single Responsibility: + +```swift +protocol APIServiceProtocol { + func request( + endpoint: String, + method: HTTPMethod, + body: Encodable?, + headers: [String: String]? + ) async throws -> T +} +``` + +**Tính năng:** +- Xử lý request/response generic +- Tự động mã hóa/giải mã JSON (snake_case ↔ camelCase) +- Tự động thêm Bearer token +- Xử lý mã trạng thái HTTP + +#### AuthManager +Singleton quản lý trạng thái xác thực: + +```swift +final class AuthManager: ObservableObject { + @MainActor static let shared = AuthManager() + @Published var authState: AuthState = .unknown +} +``` + +**Enum AuthState:** +```swift +enum AuthState { + case unknown // Trạng thái khởi tạo + case unauthenticated // Chưa đăng nhập + case authenticated(User) // Đã đăng nhập +} +``` + +## Luồng Dữ Liệu + +```mermaid +sequenceDiagram + participant V as View + participant VM as ViewModel + participant S as Service + participant API as Backend API + + V->>VM: Hành động User (tap button) + VM->>VM: Đặt isLoading = true + VM->>S: await service.request() + S->>API: HTTP Request + API-->>S: JSON Response + S-->>VM: Model đã giải mã + VM->>VM: Cập nhật @Published + VM-->>V: SwiftUI render lại +``` + +## Luồng Xác Thực + +```mermaid +stateDiagram-v2 + [*] --> Unknown: Khởi động App + Unknown --> Authenticated: Token hợp lệ + Unknown --> Unauthenticated: Không có Token + + Unauthenticated --> Login + Login --> Authenticated: Thành công + Login --> Register: Điều hướng + Register --> Authenticated: Thành công + + Authenticated --> HomeScreen + HomeScreen --> Unauthenticated: Đăng xuất +``` + +## Quyết Định Thiết Kế + +### 1. Tại sao MVVM? + +| Lợi ích | Mô tả | +|---------|-------| +| Khả năng test | ViewModel có thể test độc lập không cần UI | +| Phân tách trách nhiệm | View chỉ hiển thị, logic nằm ở ViewModel | +| Tương thích SwiftUI | `@ObservableObject` + `@Published` native | +| Cập nhật reactive | UI tự động làm mới dựa trên Combine | + +### 2. Tại sao DI dựa trên Protocol? + +```swift +// Protocol cho phép mock khi test +protocol APIServiceProtocol { ... } + +// Production +final class APIService: APIServiceProtocol { ... } + +// Test mock +final class MockAPIService: APIServiceProtocol { ... } +``` + +### 3. Tại sao Keychain thay vì UserDefaults? + +| Keychain | UserDefaults | +|----------|--------------| +| ✅ Mã hóa khi lưu trữ | ❌ Text thuần | +| ✅ Secure enclave | ❌ Có thể truy cập | +| ✅ Riêng cho app | ❌ Shared prefs | + +### 4. Tại sao @MainActor trên ViewModels? + +- Đảm bảo tất cả cập nhật `@Published` xảy ra trên main thread +- Ngăn chặn vấn đề concurrency với SwiftUI +- Contract an toàn thread rõ ràng + +## Kiến Trúc Bảo Mật + +``` +┌────────────────────────────────────────────────────────────┐ +│ CÁC LỚP BẢO MẬT │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 1: Bảo mật Transport (HTTPS/TLS) │ +│ • Tất cả API calls sử dụng HTTPS │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 2: Bảo mật Token (Keychain) │ +│ • Access token lưu trong Keychain │ +│ • Refresh token lưu trong Keychain │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 3: Bảo mật Session │ +│ • Kiểm tra hết hạn token │ +│ • Tự động refresh token │ +├────────────────────────────────────────────────────────────┤ +│ Lớp 4: Validation Input │ +│ • Kiểm tra định dạng email │ +│ • Kiểm tra độ mạnh mật khẩu │ +└────────────────────────────────────────────────────────────┘ +``` + +## Hướng Phát Triển + +| Tính năng | Ưu tiên | Mô tả | +|-----------|---------|-------| +| Certificate Pinning | Cao | Xác thực chứng chỉ TLS | +| Xác thực sinh trắc | Cao | Đăng nhập Face ID / Touch ID | +| Chế độ Offline | Trung bình | Cache local với SwiftData | +| Push Notifications | Trung bình | Tích hợp APNs | + +## Tài Liệu Liên Quan + +- [README.md](./README.md) - Hướng dẫn bắt đầu nhanh