# MyCloudVault iOS Integration Guide

Complete guide for building an iOS client app for MyCloudVault backup system.

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Project Setup](#project-setup)
3. [Architecture Overview](#architecture-overview)
4. [Implementation Steps](#implementation-steps)
5. [Code Examples](#code-examples)
6. [Testing](#testing)
7. [Best Practices](#best-practices)

---

## Prerequisites

### Development Environment

- **Xcode**: 15.0 or later
- **Minimum iOS**: iOS 15.0
- **Target iOS**: iOS 17.0
- **Language**: Swift 5.9+
- **Package Manager**: Swift Package Manager

### Required Capabilities

Add to your app's capabilities:

1. **Background Modes**
   - Background fetch
   - Background processing

2. **App Groups** (for sharing data with extensions)

3. **Keychain Sharing** (for secure credential storage)

### Info.plist Permissions

```xml
<key>NSPhotoLibraryUsageDescription</key>
<string>MyCloudVault needs access to backup your photos</string>

<key>NSCameraUsageDescription</key>
<string>MyCloudVault needs camera access to backup photos</string>

<key>NSDocumentsFolderUsageDescription</key>
<string>MyCloudVault needs access to backup your documents</string>

<key>UIBackgroundModes</key>
<array>
    <string>fetch</string>
    <string>processing</string>
</array>
```

### Dependencies (Swift Package Manager)

Add these packages in Xcode:

```swift
// File > Add Package Dependencies

// Alamofire for networking
https://github.com/Alamofire/Alamofire.git (5.8.0+)

// CryptoKit is built-in (iOS 13+)
// No external crypto library needed
```

---

## Project Setup

### 1. Create Project Structure

```
MyCloudVault/
├── App/
│   ├── MyCloudVaultApp.swift
│   └── AppDelegate.swift
├── API/
│   ├── MyCloudVaultAPI.swift
│   ├── APIClient.swift
│   ├── APIRouter.swift
│   └── Models/
│       ├── Device.swift
│       ├── Backup.swift
│       └── APIResponse.swift
├── Data/
│   ├── Repository/
│   │   ├── BackupRepository.swift
│   │   └── DeviceRepository.swift
│   ├── Local/
│   │   ├── CoreDataStack.swift
│   │   └── KeychainManager.swift
│   └── Models/
│       └── BackupEntity.swift
├── Domain/
│   ├── UseCases/
│   │   ├── StartBackupUseCase.swift
│   │   ├── UploadBackupUseCase.swift
│   │   └── VerifyBackupUseCase.swift
│   └── Models/
│       └── BackupConfiguration.swift
├── Presentation/
│   ├── Pairing/
│   │   ├── PairingView.swift
│   │   └── PairingViewModel.swift
│   ├── Backup/
│   │   ├── BackupView.swift
│   │   └── BackupViewModel.swift
│   └── Settings/
│       ├── SettingsView.swift
│       └── SettingsViewModel.swift
├── Services/
│   ├── BackupService.swift
│   └── BackgroundTaskManager.swift
└── Utilities/
    ├── EncryptionManager.swift
    ├── FileManager+Extensions.swift
    └── NetworkMonitor.swift
```

---

## Architecture Overview

### MVVM + Clean Architecture

```
View (SwiftUI)
    ↓
ViewModel (ObservableObject)
    ↓
UseCase
    ↓
Repository
    ↓
[API Client] ← → [Local Storage]
```

### Key Components

1. **API Client**: Alamofire-based networking
2. **Repository**: Data layer abstraction
3. **UseCase**: Business logic
4. **ViewModel**: UI state management
5. **Service**: Background operations
6. **Keychain**: Secure credential storage

---

## Implementation Steps

### Step 1: API Client Setup

**APIRouter.swift**

```swift
import Foundation
import Alamofire

enum APIRouter: URLRequestConvertible {
    case healthCheck
    case pairDevice(PairDeviceRequest)
    case getDevice(String)
    case startBackup(StartBackupRequest)
    case uploadChunk(deviceId: String, backupId: String, chunkIndex: Int, totalChunks: Int, data: Data)
    case uploadEncrypted(deviceId: String, backupId: String, metadata: EncryptionMetadata, data: Data)
    case verifyBackup(VerifyBackupRequest)
    case listBackups(deviceId: String, limit: Int, offset: Int, status: String?)
    case downloadBackup(backupId: String, deviceId: String)
    case deleteBackup(backupId: String, deviceId: String)
    case generateEncryptionKey(GenerateKeyRequest)
    
    var baseURL: URL {
        let urlString = UserDefaults.standard.string(forKey: "server_url") ?? "http://192.168.1.100:8080"
        return URL(string: urlString)!
    }
    
    var method: HTTPMethod {
        switch self {
        case .healthCheck, .getDevice, .listBackups, .downloadBackup:
            return .get
        case .deleteBackup:
            return .delete
        default:
            return .post
        }
    }
    
    var path: String {
        switch self {
        case .healthCheck:
            return "/api/health"
        case .pairDevice:
            return "/api/devices/pair"
        case .getDevice(let deviceId):
            return "/api/devices/\(deviceId)"
        case .startBackup:
            return "/api/backup/start"
        case .uploadChunk:
            return "/api/backup/upload-chunk"
        case .uploadEncrypted:
            return "/api/backup/upload-encrypted"
        case .verifyBackup:
            return "/api/backup/verify"
        case .listBackups:
            return "/api/backup/list"
        case .downloadBackup(let backupId, _):
            return "/api/backup/download/\(backupId)"
        case .deleteBackup(let backupId, _):
            return "/api/backup/delete/\(backupId)"
        case .generateEncryptionKey:
            return "/api/encryption/generate-key"
        }
    }
    
    func asURLRequest() throws -> URLRequest {
        let url = baseURL.appendingPathComponent(path)
        var request = URLRequest(url: url)
        request.method = method
        
        // Add authentication headers
        if let deviceId = KeychainManager.shared.getDeviceId() {
            request.setValue(deviceId, forHTTPHeaderField: "X-Device-ID")
        }
        if let apiKey = KeychainManager.shared.getAPIKey() {
            request.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
        }
        
        // Add parameters
        switch self {
        case .pairDevice(let req):
            request = try JSONEncoding.default.encode(request, with: req.dictionary)
        case .startBackup(let req):
            request = try JSONEncoding.default.encode(request, with: req.dictionary)
        case .verifyBackup(let req):
            request = try JSONEncoding.default.encode(request, with: req.dictionary)
        case .listBackups(let deviceId, let limit, let offset, let status):
            var params: [String: Any] = [
                "deviceId": deviceId,
                "limit": limit,
                "offset": offset
            ]
            if let status = status {
                params["status"] = status
            }
            request = try URLEncoding.default.encode(request, with: params)
        case .downloadBackup(_, let deviceId), .deleteBackup(_, let deviceId):
            request = try URLEncoding.default.encode(request, with: ["deviceId": deviceId])
        case .generateEncryptionKey(let req):
            request = try JSONEncoding.default.encode(request, with: req.dictionary)
        default:
            break
        }
        
        return request
    }
}
```

**APIClient.swift**

```swift
import Foundation
import Alamofire
import Combine

class APIClient {
    static let shared = APIClient()
    
    private let session: Session
    
    private init() {
        let configuration = URLSessionConfiguration.default
        configuration.timeoutIntervalForRequest = 30
        configuration.timeoutIntervalForResource = 60
        
        self.session = Session(configuration: configuration)
    }
    
    func request<T: Decodable>(_ router: APIRouter) -> AnyPublisher<T, Error> {
        return session.request(router)
            .validate()
            .publishDecodable(type: APIResponse<T>.self)
            .value()
            .tryMap { response in
                guard response.success, let data = response.data else {
                    throw APIError.serverError(response.error ?? "Unknown error")
                }
                return data
            }
            .eraseToAnyPublisher()
    }
    
    func upload(multipartFormData: @escaping (MultipartFormData) -> Void, to router: APIRouter) -> AnyPublisher<Data, Error> {
        return Future { promise in
            self.session.upload(multipartFormData: multipartFormData, with: router)
                .validate()
                .responseData { response in
                    switch response.result {
                    case .success(let data):
                        promise(.success(data))
                    case .failure(let error):
                        promise(.failure(error))
                    }
                }
        }
        .eraseToAnyPublisher()
    }
    
    func download(_ router: APIRouter, to destination: URL) -> AnyPublisher<URL, Error> {
        return Future { promise in
            let destination: DownloadRequest.Destination = { _, _ in
                return (destination, [.removePreviousFile, .createIntermediateDirectories])
            }
            
            self.session.download(router, to: destination)
                .validate()
                .responseURL { response in
                    switch response.result {
                    case .success(let url):
                        promise(.success(url))
                    case .failure(let error):
                        promise(.failure(error))
                    }
                }
        }
        .eraseToAnyPublisher()
    }
}

enum APIError: Error {
    case serverError(String)
    case networkError(Error)
    case decodingError
}
```

### Step 2: Data Models

**Device.swift**

```swift
import Foundation

struct Device: Codable {
    let id: Int
    let deviceId: String
    let deviceName: String
    let deviceModel: String?
    let osVersion: String?
    let status: String
    let lastSeen: String
}

struct PairDeviceRequest: Codable {
    let deviceId: String
    let deviceName: String
    let deviceModel: String
    let osVersion: String
    let pairingCode: String
    
    var dictionary: [String: Any] {
        return [
            "deviceId": deviceId,
            "deviceName": deviceName,
            "deviceModel": deviceModel,
            "osVersion": osVersion,
            "pairingCode": pairingCode
        ]
    }
}

struct PairDeviceResponse: Codable {
    let device: Device
    let apiKey: String
}
```

**Backup.swift**

```swift
import Foundation

struct Backup: Codable, Identifiable {
    let id: Int
    let backupId: String
    let deviceId: String
    let backupType: String
    let status: String
    let size: Int64
    let uploadedSize: Int64
    let fileCount: Int
    let encryptionEnabled: Bool
    let verificationStatus: String?
    let startedAt: String
    let completedAt: String?
}

struct StartBackupRequest: Codable {
    let deviceId: String
    let backupType: String
    let totalSize: Int64
    let fileCount: Int
    let metadata: [String: Any]?
    
    var dictionary: [String: Any] {
        var dict: [String: Any] = [
            "deviceId": deviceId,
            "backupType": backupType,
            "totalSize": totalSize,
            "fileCount": fileCount
        ]
        if let metadata = metadata {
            dict["metadata"] = metadata
        }
        return dict
    }
}

struct BackupList: Codable {
    let backups: [Backup]
    let total: Int
    let limit: Int
    let offset: Int
}
```

### Step 3: Keychain Manager

**KeychainManager.swift**

```swift
import Foundation
import Security

class KeychainManager {
    static let shared = KeychainManager()
    
    private let service = "com.yourcompany.mycloudvault"
    
    private init() {}
    
    func saveDeviceId(_ deviceId: String) {
        save(key: "device_id", value: deviceId)
    }
    
    func getDeviceId() -> String? {
        return get(key: "device_id")
    }
    
    func saveAPIKey(_ apiKey: String) {
        save(key: "api_key", value: apiKey)
    }
    
    func getAPIKey() -> String? {
        return get(key: "api_key")
    }
    
    func clearCredentials() {
        delete(key: "device_id")
        delete(key: "api_key")
    }
    
    private func save(key: String, value: String) {
        let data = value.data(using: .utf8)!
        
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecValueData as String: data
        ]
        
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }
    
    private func get(key: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key,
            kSecReturnData as String: true
        ]
        
        var result: AnyObject?
        let status = SecItemCopyMatching(query as CFDictionary, &result)
        
        guard status == errSecSuccess,
              let data = result as? Data,
              let value = String(data: data, encoding: .utf8) else {
            return nil
        }
        
        return value
    }
    
    private func delete(key: String) {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrService as String: service,
            kSecAttrAccount as String: key
        ]
        
        SecItemDelete(query as CFDictionary)
    }
}
```

### Step 4: Encryption Manager

**EncryptionManager.swift**

```swift
import Foundation
import CryptoKit

class EncryptionManager {
    static let shared = EncryptionManager()
    
    private init() {}
    
    func generateKey() -> SymmetricKey {
        return SymmetricKey(size: .bits256)
    }
    
    func encrypt(data: Data, key: SymmetricKey) throws -> EncryptionResult {
        let sealedBox = try AES.GCM.seal(data, using: key)
        
        guard let ciphertext = sealedBox.ciphertext,
              let tag = sealedBox.tag else {
            throw EncryptionError.encryptionFailed
        }
        
        return EncryptionResult(
            ciphertext: ciphertext,
            nonce: sealedBox.nonce,
            tag: tag
        )
    }
    
    func decrypt(ciphertext: Data, nonce: AES.GCM.Nonce, tag: Data, key: SymmetricKey) throws -> Data {
        let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
        return try AES.GCM.open(sealedBox, using: key)
    }
    
    func keyToBase64(_ key: SymmetricKey) -> String {
        return key.withUnsafeBytes { Data($0).base64EncodedString() }
    }
    
    func base64ToKey(_ base64: String) throws -> SymmetricKey {
        guard let data = Data(base64Encoded: base64) else {
            throw EncryptionError.invalidKey
        }
        return SymmetricKey(data: data)
    }
}

struct EncryptionResult {
    let ciphertext: Data
    let nonce: AES.GCM.Nonce
    let tag: Data
}

enum EncryptionError: Error {
    case encryptionFailed
    case decryptionFailed
    case invalidKey
}
```

### Step 5: Backup Service

**BackupService.swift**

```swift
import Foundation
import Combine
import UIKit

class BackupService: ObservableObject {
    static let shared = BackupService()
    
    @Published var isBackingUp = false
    @Published var progress: Double = 0.0
    @Published var currentFile: String = ""
    
    private var cancellables = Set<AnyCancellable>()
    
    private init() {}
    
    func startBackup(type: String, encryptionEnabled: Bool) {
        isBackingUp = true
        progress = 0.0
        
        Task {
            do {
                try await performBackup(type: type, encryptionEnabled: encryptionEnabled)
            } catch {
                print("Backup failed: \(error)")
            }
            
            await MainActor.run {
                isBackingUp = false
            }
        }
    }
    
    private func performBackup(type: String, encryptionEnabled: Bool) async throws {
        // 1. Collect files
        let files = try await collectFilesToBackup()
        let totalSize = files.reduce(0) { $0 + (try? FileManager.default.attributesOfItem(atPath: $1.path)[.size] as? Int64 ?? 0) ?? 0 }
        
        // 2. Start backup session
        guard let deviceId = KeychainManager.shared.getDeviceId() else {
            throw BackupError.notPaired
        }
        
        let startRequest = StartBackupRequest(
            deviceId: deviceId,
            backupType: type,
            totalSize: totalSize,
            fileCount: files.count,
            metadata: nil
        )
        
        let backup: Backup = try await withCheckedThrowingContinuation { continuation in
            APIClient.shared.request(APIRouter.startBackup(startRequest))
                .sink(
                    receiveCompletion: { completion in
                        if case .failure(let error) = completion {
                            continuation.resume(throwing: error)
                        }
                    },
                    receiveValue: { backup in
                        continuation.resume(returning: backup)
                    }
                )
                .store(in: &cancellables)
        }
        
        // 3. Upload files
        if encryptionEnabled {
            try await uploadEncrypted(deviceId: deviceId, backupId: backup.backupId, files: files)
        } else {
            try await uploadChunked(deviceId: deviceId, backupId: backup.backupId, files: files)
        }
        
        // 4. Verify backup
        try await verifyBackup(deviceId: deviceId, backupId: backup.backupId)
    }
    
    private func collectFilesToBackup() async throws -> [URL] {
        // Implement file collection logic
        // This would scan Photos library, Documents, etc.
        return []
    }
    
    private func uploadChunked(deviceId: String, backupId: String, files: [URL]) async throws {
        let chunkSize = 5 * 1024 * 1024 // 5MB
        var uploadedSize: Int64 = 0
        let totalSize = files.reduce(0) { $0 + (try? FileManager.default.attributesOfItem(atPath: $1.path)[.size] as? Int64 ?? 0) ?? 0 }
        
        for file in files {
            let data = try Data(contentsOf: file)
            let chunks = stride(from: 0, to: data.count, by: chunkSize).map {
                data[$0..<min($0 + chunkSize, data.count)]
            }
            
            for (index, chunk) in chunks.enumerated() {
                try await uploadChunk(
                    deviceId: deviceId,
                    backupId: backupId,
                    chunkIndex: index,
                    totalChunks: chunks.count,
                    data: chunk
                )
                
                uploadedSize += Int64(chunk.count)
                await MainActor.run {
                    self.progress = Double(uploadedSize) / Double(totalSize)
                }
            }
        }
    }
    
    private func uploadChunk(deviceId: String, backupId: String, chunkIndex: Int, totalChunks: Int, data: Data) async throws {
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            APIClient.shared.upload(
                multipartFormData: { formData in
                    formData.append(deviceId.data(using: .utf8)!, withName: "deviceId")
                    formData.append(backupId.data(using: .utf8)!, withName: "backupId")
                    formData.append("\(chunkIndex)".data(using: .utf8)!, withName: "chunkIndex")
                    formData.append("\(totalChunks)".data(using: .utf8)!, withName: "totalChunks")
                    formData.append(data, withName: "chunk", fileName: "chunk.dat", mimeType: "application/octet-stream")
                },
                to: APIRouter.uploadChunk(deviceId: deviceId, backupId: backupId, chunkIndex: chunkIndex, totalChunks: totalChunks, data: data)
            )
            .sink(
                receiveCompletion: { completion in
                    if case .failure(let error) = completion {
                        continuation.resume(throwing: error)
                    } else {
                        continuation.resume()
                    }
                },
                receiveValue: { _ in }
            )
            .store(in: &cancellables)
        }
    }
    
    private func uploadEncrypted(deviceId: String, backupId: String, files: [URL]) async throws {
        // Generate encryption key
        let key = EncryptionManager.shared.generateKey()
        
        // Create archive
        let archiveURL = try createArchive(files: files)
        let archiveData = try Data(contentsOf: archiveURL)
        
        // Encrypt
        let encrypted = try EncryptionManager.shared.encrypt(data: archiveData, key: key)
        
        // Upload
        let metadata = EncryptionMetadata(
            algorithm: "aes-256-gcm",
            iv: encrypted.nonce.withUnsafeBytes { Data($0).base64EncodedString() },
            authTag: encrypted.tag.base64EncodedString(),
            salt: ""
        )
        
        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
            APIClient.shared.upload(
                multipartFormData: { formData in
                    formData.append(deviceId.data(using: .utf8)!, withName: "deviceId")
                    formData.append(backupId.data(using: .utf8)!, withName: "backupId")
                    
                    let metadataJSON = try! JSONEncoder().encode(metadata)
                    formData.append(metadataJSON, withName: "encryptionMetadata")
                    
                    formData.append(encrypted.ciphertext, withName: "encryptedData", fileName: "backup.enc", mimeType: "application/octet-stream")
                },
                to: APIRouter.uploadEncrypted(deviceId: deviceId, backupId: backupId, metadata: metadata, data: encrypted.ciphertext)
            )
            .sink(
                receiveCompletion: { completion in
                    if case .failure(let error) = completion {
                        continuation.resume(throwing: error)
                    } else {
                        continuation.resume()
                    }
                },
                receiveValue: { _ in }
            )
            .store(in: &cancellables)
        }
        
        // Clean up
        try? FileManager.default.removeItem(at: archiveURL)
    }
    
    private func createArchive(files: [URL]) throws -> URL {
        // Implement ZIP archive creation
        let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("backup.zip")
        // Use Compression framework or third-party library
        return tempURL
    }
    
    private func verifyBackup(deviceId: String, backupId: String) async throws {
        let request = VerifyBackupRequest(deviceId: deviceId, backupId: backupId)
        
        let _: VerificationResult = try await withCheckedThrowingContinuation { continuation in
            APIClient.shared.request(APIRouter.verifyBackup(request))
                .sink(
                    receiveCompletion: { completion in
                        if case .failure(let error) = completion {
                            continuation.resume(throwing: error)
                        }
                    },
                    receiveValue: { result in
                        continuation.resume(returning: result)
                    }
                )
                .store(in: &cancellables)
        }
    }
}

enum BackupError: Error {
    case notPaired
    case uploadFailed
    case verificationFailed
}
```

### Step 6: Pairing View (SwiftUI)

**PairingView.swift**

```swift
import SwiftUI

struct PairingView: View {
    @StateObject private var viewModel = PairingViewModel()
    
    var body: some View {
        NavigationView {
            Form {
                Section(header: Text("Server Configuration")) {
                    TextField("Server URL", text: $viewModel.serverURL)
                        .textContentType(.URL)
                        .autocapitalization(.none)
                        .keyboardType(.URL)
                }
                
                Section(header: Text("Pairing Code")) {
                    TextField("Enter 6-digit code", text: $viewModel.pairingCode)
                        .textContentType(.oneTimeCode)
                        .keyboardType(.numberPad)
                }
                
                Section {
                    Button(action: {
                        viewModel.pairDevice()
                    }) {
                        if viewModel.isPairing {
                            ProgressView()
                        } else {
                            Text("Pair Device")
                        }
                    }
                    .disabled(viewModel.isPairing || viewModel.serverURL.isEmpty || viewModel.pairingCode.isEmpty)
                }
                
                if let error = viewModel.errorMessage {
                    Section {
                        Text(error)
                            .foregroundColor(.red)
                    }
                }
            }
            .navigationTitle("Pair Device")
        }
    }
}

class PairingViewModel: ObservableObject {
    @Published var serverURL = ""
    @Published var pairingCode = ""
    @Published var isPairing = false
    @Published var errorMessage: String?
    @Published var isPaired = false
    
    private var cancellables = Set<AnyCancellable>()
    
    func pairDevice() {
        isPairing = true
        errorMessage = nil
        
        // Save server URL
        UserDefaults.standard.set(serverURL, forKey: "server_url")
        
        // Generate device info
        let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
        let deviceName = UIDevice.current.name
        let deviceModel = UIDevice.current.model
        let osVersion = UIDevice.current.systemVersion
        
        let request = PairDeviceRequest(
            deviceId: "ios-\(deviceId)",
            deviceName: deviceName,
            deviceModel: deviceModel,
            osVersion: osVersion,
            pairingCode: pairingCode
        )
        
        APIClient.shared.request(APIRouter.pairDevice(request))
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    self?.isPairing = false
                    if case .failure(let error) = completion {
                        self?.errorMessage = "Pairing failed: \(error.localizedDescription)"
                    }
                },
                receiveValue: { [weak self] (response: PairDeviceResponse) in
                    KeychainManager.shared.saveDeviceId(response.device.deviceId)
                    KeychainManager.shared.saveAPIKey(response.apiKey)
                    self?.isPaired = true
                }
            )
            .store(in: &cancellables)
    }
}
```

---

## Testing

### Unit Tests

```swift
import XCTest
@testable import MyCloudVault

class EncryptionManagerTests: XCTestCase {
    func testEncryptDecrypt() throws {
        let manager = EncryptionManager.shared
        let key = manager.generateKey()
        let data = "Hello, MyCloudVault!".data(using: .utf8)!
        
        let encrypted = try manager.encrypt(data: data, key: key)
        let decrypted = try manager.decrypt(
            ciphertext: encrypted.ciphertext,
            nonce: encrypted.nonce,
            tag: encrypted.tag,
            key: key
        )
        
        XCTAssertEqual(data, decrypted)
    }
}
```

---

## Best Practices

### Security

1. **Use Keychain for credentials**
2. **Enable App Transport Security (ATS)**
3. **Implement certificate pinning**
4. **Use CryptoKit for encryption**
5. **Clear sensitive data from memory**

### Performance

1. **Use Background Tasks framework**
2. **Implement exponential backoff**
3. **Compress before encryption**
4. **Use async/await for concurrency**
5. **Batch operations**

### User Experience

1. **Show progress indicators**
2. **Handle errors gracefully**
3. **Provide offline mode**
4. **Allow backup cancellation**
5. **Display storage usage**

---

## In-App Purchases & Monetization

### Overview

CloudVault uses a **freemium model** where:
- **Server software**: Free and open-source (self-hosted)
- **Mobile apps**: Paid with in-app purchases for premium features

### StoreKit 2 Setup

Add to your project:

```swift
import StoreKit

// Product IDs
enum ProductID: String, CaseIterable {
    case premiumMonthly = "com.cloudvault.premium.monthly"
    case premiumYearly = "com.cloudvault.premium.yearly"
    case lifetime = "com.cloudvault.lifetime"
}
```

### Subscription Tiers

#### Free Tier
- 1 device
- 10 backups maximum
- 5 GB storage limit
- Manual backups only
- Standard support

#### Premium Monthly ($4.99/month)
- Unlimited devices
- Unlimited backups
- Unlimited storage
- Automated scheduling
- Priority support
- Advanced compression

#### Premium Yearly ($49.99/year) - Save 17%
- All Premium Monthly features
- Annual billing
- Best value

#### Lifetime ($99.99 one-time)
- All Premium features forever
- One-time payment
- No recurring fees

### Store Manager Implementation

```swift
@MainActor
class StoreManager: ObservableObject {
    @Published private(set) var products: [Product] = []
    @Published private(set) var purchasedProductIDs = Set<String>()
    @Published var isPremium = false
    
    private var updateListenerTask: Task<Void, Error>?
    
    init() {
        updateListenerTask = listenForTransactions()
        
        Task {
            await loadProducts()
            await updatePurchasedProducts()
        }
    }
    
    deinit {
        updateListenerTask?.cancel()
    }
    
    func listenForTransactions() -> Task<Void, Error> {
        return Task.detached {
            for await result in Transaction.updates {
                do {
                    let transaction = try self.checkVerified(result)
                    await self.updatePurchasedProducts()
                    await transaction.finish()
                } catch {
                    print("Transaction verification failed")
                }
            }
        }
    }
    
    @MainActor
    func loadProducts() async {
        do {
            let productIDs = ProductID.allCases.map { $0.rawValue }
            products = try await Product.products(for: productIDs)
        } catch {
            print("Failed to load products: \(error)")
        }
    }
    
    func purchase(_ product: Product) async throws -> Transaction? {
        let result = try await product.purchase()
        
        switch result {
        case .success(let verification):
            let transaction = try checkVerified(verification)
            await updatePurchasedProducts()
            await transaction.finish()
            return transaction
            
        case .userCancelled, .pending:
            return nil
            
        @unknown default:
            return nil
        }
    }
    
    func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
        switch result {
        case .unverified:
            throw StoreError.failedVerification
        case .verified(let safe):
            return safe
        }
    }
    
    @MainActor
    func updatePurchasedProducts() async {
        for await result in Transaction.currentEntitlements {
            do {
                let transaction = try checkVerified(result)
                
                if transaction.revocationDate == nil {
                    purchasedProductIDs.insert(transaction.productID)
                } else {
                    purchasedProductIDs.remove(transaction.productID)
                }
            } catch {
                print("Failed to verify transaction")
            }
        }
        
        isPremium = !purchasedProductIDs.isEmpty
    }
    
    func restorePurchases() async {
        try? await AppStore.sync()
        await updatePurchasedProducts()
    }
}

enum StoreError: Error {
    case failedVerification
}
```

### Feature Gating

```swift
class FeatureManager: ObservableObject {
    @Published var isPremium = false
    
    private let maxFreeDevices = 1
    private let maxFreeBackups = 10
    private let maxFreeStorageGB = 5
    
    func canAddDevice() -> Bool {
        let deviceCount = getDeviceCount()
        return isPremium || deviceCount < maxFreeDevices
    }
    
    func canCreateBackup() -> Bool {
        let backupCount = getBackupCount()
        return isPremium || backupCount < maxFreeBackups
    }
    
    func canScheduleBackups() -> Bool {
        return isPremium
    }
    
    func hasStorageAvailable(sizeBytes: Int64) -> Bool {
        if isPremium { return true }
        
        let usedStorageGB = getTotalStorageUsed() / (1024 * 1024 * 1024)
        let newStorageGB = sizeBytes / (1024 * 1024 * 1024)
        return (usedStorageGB + newStorageGB) <= maxFreeStorageGB
    }
    
    private func getDeviceCount() -> Int {
        // Fetch from UserDefaults or API
        return UserDefaults.standard.integer(forKey: "device_count")
    }
    
    private func getBackupCount() -> Int {
        return UserDefaults.standard.integer(forKey: "backup_count")
    }
    
    private func getTotalStorageUsed() -> Int64 {
        return UserDefaults.standard.object(forKey: "storage_used") as? Int64 ?? 0
    }
}
```

### Purchase UI

```swift
struct PurchaseView: View {
    @StateObject private var storeManager = StoreManager()
    @State private var isPurchasing = false
    
    var body: some View {
        List {
            Section("Premium Features") {
                FeatureRow(icon: "infinity", title: "Unlimited Devices")
                FeatureRow(icon: "icloud", title: "Unlimited Storage")
                FeatureRow(icon: "calendar", title: "Automated Scheduling")
                FeatureRow(icon: "bolt.fill", title: "Advanced Compression")
            }
            
            Section("Choose Your Plan") {
                ForEach(storeManager.products, id: \.id) { product in
                    ProductRow(product: product) {
                        Task {
                            isPurchasing = true
                            try? await storeManager.purchase(product)
                            isPurchasing = false
                        }
                    }
                }
            }
            
            Section {
                Button("Restore Purchases") {
                    Task {
                        await storeManager.restorePurchases()
                    }
                }
            }
        }
        .navigationTitle("Go Premium")
        .disabled(isPurchasing)
    }
}

struct ProductRow: View {
    let product: Product
    let onPurchase: () -> Void
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(product.displayName)
                    .font(.headline)
                Text(product.description)
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            
            Spacer()
            
            Button(product.displayPrice) {
                onPurchase()
            }
            .buttonStyle(.borderedProminent)
        }
    }
}
```

---

## QR Code Device Pairing

### Overview

CloudVault uses QR code scanning for instant device pairing, reducing setup time from 5 minutes to 10 seconds.

### Implementation

```swift
import AVFoundation
import SwiftUI

struct QRCodeScannerView: View {
    @StateObject private var viewModel = QRCodeScannerViewModel()
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        ZStack {
            CameraPreview(session: viewModel.session)
                .ignoresSafeArea()
            
            VStack {
                Text("Scan QR Code")
                    .font(.title)
                    .foregroundColor(.white)
                    .padding()
                
                Spacer()
                
                RoundedRectangle(cornerRadius: 20)
                    .stroke(Color.white, lineWidth: 3)
                    .frame(width: 250, height: 250)
                
                Spacer()
                
                Button("Cancel") {
                    dismiss()
                }
                .foregroundColor(.white)
                .padding()
            }
        }
        .onAppear {
            viewModel.startScanning()
        }
        .onDisappear {
            viewModel.stopScanning()
        }
        .alert("Error", isPresented: $viewModel.showError) {
            Button("OK") { }
        } message: {
            Text(viewModel.errorMessage ?? "Unknown error")
        }
    }
}

class QRCodeScannerViewModel: NSObject, ObservableObject, AVCaptureMetadataOutputObjectsDelegate {
    @Published var showError = false
    @Published var errorMessage: String?
    @Published var isPaired = false
    
    let session = AVCaptureSession()
    private let output = AVCaptureMetadataOutput()
    
    func startScanning() {
        guard let device = AVCaptureDevice.default(for: .video) else {
            showError("Camera not available")
            return
        }
        
        do {
            let input = try AVCaptureDeviceInput(device: device)
            
            if session.canAddInput(input) {
                session.addInput(input)
            }
            
            if session.canAddOutput(output) {
                session.addOutput(output)
                output.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
                output.metadataObjectTypes = [.qr]
            }
            
            DispatchQueue.global(qos: .userInitiated).async { [weak self] in
                self?.session.startRunning()
            }
        } catch {
            showError("Failed to start camera: \(error.localizedDescription)")
        }
    }
    
    func stopScanning() {
        session.stopRunning()
    }
    
    func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
        guard let metadataObject = metadataObjects.first,
              let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject,
              let stringValue = readableObject.stringValue else {
            return
        }
        
        handleQRCode(stringValue)
    }
    
    private func handleQRCode(_ qrCode: String) {
        stopScanning()
        
        guard let data = qrCode.data(using: .utf8),
              let json = try? JSONDecoder().decode(QRCodeData.self, from: data) else {
            showError("Invalid QR code")
            return
        }
        
        // Check expiration
        if Date().timeIntervalSince1970 * 1000 > Double(json.expiresAt) {
            showError("QR code expired. Please generate a new one.")
            return
        }
        
        // Pair device
        Task {
            await pairDevice(serverUrl: json.serverUrl, token: json.token)
        }
    }
    
    private func pairDevice(serverUrl: String, token: String) async {
        let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? UUID().uuidString
        let deviceName = UIDevice.current.name
        
        let request = PairDeviceRequest(
            token: token,
            deviceId: deviceId,
            deviceName: deviceName,
            platform: "ios",
            osVersion: UIDevice.current.systemVersion
        )
        
        do {
            let response = try await APIClient.shared.pairDevice(request)
            
            // Save credentials
            UserDefaults.standard.set(serverUrl, forKey: "server_url")
            KeychainManager.shared.saveDeviceId(response.deviceId)
            
            await MainActor.run {
                isPaired = true
            }
        } catch {
            await MainActor.run {
                showError("Pairing failed: \(error.localizedDescription)")
            }
        }
    }
    
    private func showError(_ message: String) {
        errorMessage = message
        showError = true
    }
}

struct QRCodeData: Codable {
    let serverUrl: String
    let token: String
    let expiresAt: Int64
}

struct CameraPreview: UIViewRepresentable {
    let session: AVCaptureSession
    
    func makeUIView(context: Context) -> UIView {
        let view = UIView(frame: .zero)
        let previewLayer = AVCaptureVideoPreviewLayer(session: session)
        previewLayer.videoGravity = .resizeAspectFill
        view.layer.addSublayer(previewLayer)
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        if let previewLayer = uiView.layer.sublayers?.first as? AVCaptureVideoPreviewLayer {
            previewLayer.frame = uiView.bounds
        }
    }
}
```

---

## Compression Support

### Overview

CloudVault implements automatic compression to reduce storage usage by up to 70%, saving significant costs.

### Implementation

```swift
import Compression

class CompressionManager {
    enum CompressionError: Error {
        case compressionFailed
        case decompressionFailed
    }
    
    func compressFile(at inputURL: URL, to outputURL: URL) throws {
        let inputData = try Data(contentsOf: inputURL)
        let compressedData = try compress(data: inputData)
        try compressedData.write(to: outputURL)
    }
    
    func decompressFile(at inputURL: URL, to outputURL: URL) throws {
        let compressedData = try Data(contentsOf: inputURL)
        let decompressedData = try decompress(data: compressedData)
        try decompressedData.write(to: outputURL)
    }
    
    func compress(data: Data) throws -> Data {
        let sourceBuffer = [UInt8](data)
        let destinationBufferSize = data.count
        var destinationBuffer = [UInt8](repeating: 0, count: destinationBufferSize)
        
        let compressedSize = compression_encode_buffer(
            &destinationBuffer,
            destinationBufferSize,
            sourceBuffer,
            sourceBuffer.count,
            nil,
            COMPRESSION_ZLIB
        )
        
        guard compressedSize > 0 else {
            throw CompressionError.compressionFailed
        }
        
        return Data(destinationBuffer[0..<compressedSize])
    }
    
    func decompress(data: Data) throws -> Data {
        let sourceBuffer = [UInt8](data)
        let destinationBufferSize = data.count * 4 // Estimate
        var destinationBuffer = [UInt8](repeating: 0, count: destinationBufferSize)
        
        let decompressedSize = compression_decode_buffer(
            &destinationBuffer,
            destinationBufferSize,
            sourceBuffer,
            sourceBuffer.count,
            nil,
            COMPRESSION_ZLIB
        )
        
        guard decompressedSize > 0 else {
            throw CompressionError.decompressionFailed
        }
        
        return Data(destinationBuffer[0..<decompressedSize])
    }
    
    func getCompressionRatio(originalSize: Int64, compressedSize: Int64) -> Float {
        guard originalSize > 0 else { return 0 }
        return Float(1 - (Double(compressedSize) / Double(originalSize))) * 100
    }
}
```

### Integration with Upload

```swift
func uploadBackupWithCompression(fileURL: URL, deviceId: String) async throws {
    let compressedURL = FileManager.default.temporaryDirectory
        .appendingPathComponent("\(fileURL.lastPathComponent).gz")
    
    defer {
        try? FileManager.default.removeItem(at: compressedURL)
    }
    
    do {
        try compressionManager.compressFile(at: fileURL, to: compressedURL)
        
        let originalSize = try FileManager.default.attributesOfItem(atPath: fileURL.path)[.size] as? Int64 ?? 0
        let compressedSize = try FileManager.default.attributesOfItem(atPath: compressedURL.path)[.size] as? Int64 ?? 0
        let ratio = compressionManager.getCompressionRatio(originalSize: originalSize, compressedSize: compressedSize)
        
        print("Compressed: \(originalSize)B -> \(compressedSize)B (\(ratio)% savings)")
        
        try await uploadFile(compressedURL, deviceId: deviceId, compressed: true)
    } catch {
        // Upload original if compression fails
        try await uploadFile(fileURL, deviceId: deviceId, compressed: false)
    }
}
```

---

## Backup Scheduling

### Overview

CloudVault supports automated backup scheduling using BackgroundTasks framework.

### Background Tasks Setup

Add to `Info.plist`:

```xml
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.cloudvault.backup.refresh</string>
</array>
```

### Implementation

```swift
import BackgroundTasks

class BackupScheduler {
    static let shared = BackupScheduler()
    private let taskIdentifier = "com.cloudvault.backup.refresh"
    
    func registerBackgroundTasks() {
        BGTaskScheduler.shared.register(
            forTaskWithIdentifier: taskIdentifier,
            using: nil
        ) { task in
            self.handleBackupTask(task: task as! BGAppRefreshTask)
        }
    }
    
    func scheduleBackup(at date: Date, wifiOnly: Bool = true, minBattery: Int = 20) {
        let request = BGAppRefreshTaskRequest(identifier: taskIdentifier)
        request.earliestBeginDate = date
        
        do {
            try BGTaskScheduler.shared.submit(request)
            print("Backup scheduled for \(date)")
        } catch {
            print("Failed to schedule backup: \(error)")
        }
    }
    
    private func handleBackupTask(task: BGAppRefreshTask) {
        // Schedule next backup
        let nextDate = Calendar.current.date(byAdding: .day, value: 1, to: Date())!
        scheduleBackup(at: nextDate)
        
        task.expirationHandler = {
            // Clean up
        }
        
        Task {
            do {
                // Check conditions
                guard await checkBackupConditions() else {
                    task.setTaskCompleted(success: false)
                    return
                }
                
                // Perform backup
                try await performBackup()
                task.setTaskCompleted(success: true)
            } catch {
                task.setTaskCompleted(success: false)
            }
        }
    }
    
    private func checkBackupConditions() async -> Bool {
        // Check WiFi
        if !isWiFiConnected() {
            return false
        }
        
        // Check battery
        UIDevice.current.isBatteryMonitoringEnabled = true
        let batteryLevel = UIDevice.current.batteryLevel
        if batteryLevel < 0.2 { // 20%
            return false
        }
        
        return true
    }
    
    private func isWiFiConnected() -> Bool {
        // Implement network check
        return true
    }
    
    private func performBackup() async throws {
        // Implement backup logic
    }
}
```

### Schedule Management UI

```swift
struct ScheduleView: View {
    @State private var selectedTime = Date()
    @State private var wifiOnly = true
    @State private var minBattery = 20
    
    var body: some View {
        Form {
            Section("Schedule") {
                DatePicker("Backup Time", selection: $selectedTime, displayedComponents: .hourAndMinute)
            }
            
            Section("Conditions") {
                Toggle("WiFi Only", isOn: $wifiOnly)
                
                Stepper("Minimum Battery: \(minBattery)%", value: $minBattery, in: 0...100, step: 10)
            }
            
            Section {
                Button("Save Schedule") {
                    BackupScheduler.shared.scheduleBackup(
                        at: selectedTime,
                        wifiOnly: wifiOnly,
                        minBattery: minBattery
                    )
                }
            }
        }
        .navigationTitle("Backup Schedule")
    }
}
```

---

## Upload Progress Tracking

### Overview

Real-time upload progress tracking with per-chunk progress bars for multi-threaded uploads.

### Progress Tracker

```swift
class UploadProgressTracker: ObservableObject {
    @Published var progress: UploadProgress?
    private var timer: Timer?
    
    func startTracking(backupId: String) {
        timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
            Task {
                await self?.fetchProgress(backupId: backupId)
            }
        }
    }
    
    func stopTracking() {
        timer?.invalidate()
        timer = nil
    }
    
    private func fetchProgress(backupId: String) async {
        do {
            let response = try await APIClient.shared.getUploadProgress(backupId: backupId)
            
            await MainActor.run {
                self.progress = response.progress
                
                if response.progress.status == "completed" {
                    stopTracking()
                }
            }
        } catch {
            print("Failed to fetch progress: \(error)")
        }
    }
}

struct UploadProgress: Codable {
    let fileName: String
    let totalChunks: Int
    let uploadedChunks: Int
    let totalSize: Int64?
    let uploadedSize: Int64
    let status: String
}
```

### UI Implementation

```swift
struct UploadProgressView: View {
    @StateObject private var tracker = UploadProgressTracker()
    let backupId: String
    
    var body: some View {
        VStack(spacing: 16) {
            if let progress = tracker.progress {
                Text(progress.fileName)
                    .font(.headline)
                
                ProgressView(value: Double(progress.uploadedChunks), total: Double(progress.totalChunks))
                    .progressViewStyle(.linear)
                
                HStack {
                    Text("\(progress.uploadedChunks) / \(progress.totalChunks) chunks")
                        .font(.caption)
                        .foregroundColor(.secondary)
                    
                    Spacer()
                    
                    Text("\(Int((Double(progress.uploadedChunks) / Double(progress.totalChunks)) * 100))%")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
            } else {
                ProgressView("Loading...")
            }
        }
        .padding()
        .onAppear {
            tracker.startTracking(backupId: backupId)
        }
        .onDisappear {
            tracker.stopTracking()
        }
    }
}
```

---

## Next Steps

1. Implement Photos library access
2. Add restore functionality
3. Create settings screen
4. Implement background tasks
5. Add backup history view
6. Implement incremental backups
7. Add backup verification UI
8. Create backup logs

---

## Resources

- [Apple CryptoKit](https://developer.apple.com/documentation/cryptokit)
- [Alamofire Documentation](https://github.com/Alamofire/Alamofire)
- [Background Tasks](https://developer.apple.com/documentation/backgroundtasks)
- [Keychain Services](https://developer.apple.com/documentation/security/keychain_services)
