feat: Implement OAuth2 password and refresh token grants for authentication, update user registration fields, and add new documentation.
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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ả 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.**
|
||||
|
||||
33
apps/app-client-base-swift/docs/README.md
Normal file
33
apps/app-client-base-swift/docs/README.md
Normal 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)
|
||||
207
apps/app-client-base-swift/docs/en/README.md
Normal file
207
apps/app-client-base-swift/docs/en/README.md
Normal 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.
|
||||
@@ -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)
|
||||
207
apps/app-client-base-swift/docs/vi/README.md
Normal file
207
apps/app-client-base-swift/docs/vi/README.md
Normal 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ả 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.
|
||||
234
apps/app-client-base-swift/docs/vi/architecture.md
Normal file
234
apps/app-client-base-swift/docs/vi/architecture.md
Normal 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 // 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
|
||||
Reference in New Issue
Block a user