feat: Implement OAuth2 password and refresh token grants for authentication, update user registration fields, and add new documentation.

This commit is contained in:
Ho Ngoc Hai
2026-01-16 10:58:09 +07:00
parent c437ea4c9f
commit e734a21a5b
11 changed files with 908 additions and 492 deletions

View File

@@ -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" hoc "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 chun đ to 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
}
}

View File

@@ -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)
}
}

View File

@@ -111,76 +111,77 @@ final class AuthManager: ObservableObject {
}
}
/// Login with email and password
/// Đăng nhp vi email và mt khu
/// Login with email and password using OAuth2 Password Grant
/// Đăng nhp vi email và mt khu s dng OAuth2 Password Grant
/// - Parameters:
/// - email: User email / Email ngưi dùng
/// - password: User password / Mt khu 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 liu 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
// Ly thông tin user t API
await fetchCurrentUser()
}
/// Register new user
/// Đăng ký ngưi dùng mi
/// - 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 / Mt khu 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 liu 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 nhp 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 mi thông tin user t API
@MainActor func refreshCurrentUser() async {
/// Fetch current user from API
/// Ly thông tin user hin ti 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 liu 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 mi thông tin user t cache hoc API
@MainActor func refreshCurrentUser() async {
await fetchCurrentUser()
}
// MARK: - Private Methods
/// Refresh access token using refresh token
/// Làm mi access token s dng refresh token
/// Refresh access token using OAuth2 refresh_token grant
/// Làm mi access token s dng 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 mi
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 {

View File

@@ -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
/// Thc hin đăng nhp
/// Perform login with IAM Service
/// Thc hin đăng nhp vi 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 nhp mock đ test
if loginEmail.lowercased() == mockEmail && loginPassword == mockPassword {
// Simulate network delay
// Gi lp delay mng
try? await Task.sleep(nanoseconds: 1_000_000_000)
// Create mock user and authenticate
// To mock user và xác thc
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
// Cp nht trng thái auth
await MainActor.run {
AuthManager.shared.setAuthenticated(user: mockUser)
}
isLoading = false
return
}
// Real API login
// Đăng nhp API tht
do {
try await AuthManager.shared.login(email: loginEmail, password: loginPassword)
} catch {
@@ -210,8 +164,8 @@ final class AuthViewModel: ObservableObject {
isLoading = false
}
/// Perform registration
/// Thc hin đăng ký
/// Perform registration with IAM Service
/// Thc hin đăng ký vi 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
/// Gi email quên mt khu
func forgotPassword() async {

View File

@@ -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 |
## <20> 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 bng tiếng Vit
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
/// Ti d liu 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.**

View File

@@ -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)

View File

@@ -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.

View File

@@ -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<T>() │ - login() │ │
│ │ │ - get(), post() │ │ │ │ - register() │ │ │
│ │ │ - put(), delete() │ │ │ │ - logout() │ │ │
│ │ └───────────────────────┘ │ │ │ - refreshToken() │ │ │
│ │ URLSession │ │ │ Keychain │ │ │
│ │ • request<T>() │ │ • @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<T: Decodable>(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)

View File

@@ -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 bng tiếng Vit
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.

View File

@@ -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<T>() │ │ • @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<T: Decodable>(
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 // Trng thái khi to
case unauthenticated // Chưa đăng nhp
case authenticated(User) // Đã đăng nhp
}
```
## 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