Manos Chorianopoulos

add AI Agent Skill File

Showing 1 changed file with 1162 additions and 0 deletions
# SwiftWarplyFramework — AI Agent Skill File
> Comprehensive knowledge base for AI coding agents (Claude Code, Gemini CLI, Codex, OpenCode) working on the SwiftWarplyFramework iOS SDK.
---
## 1. Project Overview
**SwiftWarplyFramework** is a native iOS loyalty/rewards SDK (version 2.3.0) built in Swift for the DEI (Public Power Corporation of Greece) engagement platform. It provides a complete loyalty program toolkit including campaigns, coupons, merchant discovery, user profiles, card management, transaction history, and market pass (supermarket deals) features.
- **Language:** Swift 5.9+
- **Minimum iOS:** 17.0
- **Repository:** `https://git.warp.ly/open-source/warply_sdk_framework.git`
- **Framework Type:** Distributable iOS framework (CocoaPods + SPM)
- **Entry Point:** `WarplySDK.shared` (singleton)
- **Backend:** RESTful API on `engage-uat.dei.gr` (UAT) / `engage-prod.dei.gr` (production)
- **Auth Model:** JWT tokens (access + refresh) stored in local SQLite database, automatic refresh with circuit breaker
### What This SDK Does
- Authenticates users via DEI login (email-based) or Cosmote partner flow (GUID-based)
- Manages loyalty campaigns, coupons, coupon sets, and personalized offers
- Handles card management (add/get/delete credit cards)
- Provides transaction and points history
- Manages supermarket/market pass deals with map integration
- Sends analytics events (Dynatrace integration)
- Provides pre-built UI screens (MyRewards, Profile, Coupon, Campaign WebView)
## 2. Architecture & Design Patterns
### Core Patterns
| Pattern | Where Used | Details |
|---------|-----------|---------|
| **Singleton** | `WarplySDK.shared`, `NetworkService.shared`, `DatabaseManager.shared`, `EventDispatcher.shared` | Main entry points for all subsystems |
| **Actor Isolation** | `TokenRefreshManager`, `FieldEncryption`, `KeychainManager`, `TokenRefreshCircuitBreaker`, `RequestQueue` | Thread-safe concurrent access for security-critical components |
| **Enum-based Routing** | `Endpoint` enum | All API endpoints defined as enum cases with computed properties for path, method, parameters, auth type |
| **Dual API Surface** | All public SDK methods | Every API has both callback-based and `async/await` variants |
| **Event Bus (Dual)** | `SwiftEventBus` + `EventDispatcher` | Events posted to both systems for backward compatibility |
| **PropertyWrapper** | `@UserDefault<T>` | Type-safe UserDefaults access |
| **Circuit Breaker** | `TokenRefreshCircuitBreaker` | Prevents excessive token refresh attempts |
| **Builder/Factory** | `WarplyConfiguration` presets | `.development`, `.production`, `.testing`, `.highSecurity` |
### Dependency Flow
```
WarplySDK (public API facade)
├── NetworkService (HTTP requests)
│ ├── Endpoint (URL/param/auth routing)
│ └── TokenRefreshManager (actor, JWT refresh)
│ └── DatabaseManager (token storage)
├── DatabaseManager (SQLite.swift)
│ └── FieldEncryption (actor, AES-256-GCM)
│ └── KeychainManager (actor, hardware-backed keys)
├── EventDispatcher (modern event system)
├── SDKState (in-memory campaign/coupon state)
└── UserDefaultsStore (non-sensitive preferences)
```
### Threading Model
- **Main thread:** UI operations, completion callbacks (all callbacks dispatched via `MainActor.run`)
- **Background:** Network requests via `async/await`, database operations
- **Actor isolation:** Token refresh, encryption, keychain access — all actors prevent data races
- **Configuration queue:** `DispatchQueue(label: "com.warply.sdk.configuration")` for thread-safe config updates
## 3. Directory Structure
```
SwiftWarplyFramework/
├── Package.swift # SPM package definition
├── SwiftWarplyFramework.podspec # CocoaPods spec (v2.3.0)
├── skill.md # This file
├── SwiftWarplyFramework/
│ ├── SwiftWarplyFramework/
│ │ ├── Core/
│ │ │ └── WarplySDK.swift # Main SDK singleton — ALL public APIs
│ │ ├── Network/
│ │ │ ├── Endpoints.swift # Endpoint enum (all API routes)
│ │ │ ├── NetworkService.swift # HTTP client, request building, auth headers
│ │ │ └── TokenRefreshManager.swift # Actor-based token refresh with retry/circuit breaker
│ │ ├── Database/
│ │ │ └── DatabaseManager.swift # SQLite.swift wrapper, token/event/POI storage
│ │ ├── Security/
│ │ │ ├── FieldEncryption.swift # AES-256-GCM field-level encryption (actor)
│ │ │ └── KeychainManager.swift # iOS Keychain key management (actor)
│ │ ├── Configuration/
│ │ │ ├── WarplyConfiguration.swift # Main config container + presets
│ │ │ ├── DatabaseConfiguration.swift
│ │ │ ├── TokenConfiguration.swift
│ │ │ ├── NetworkConfiguration.swift
│ │ │ └── LoggingConfiguration.swift
│ │ ├── Events/
│ │ │ └── EventDispatcher.swift # Type-safe event system (replaces SwiftEventBus internally)
│ │ ├── models/ # All data models
│ │ │ ├── Campaign.swift # CampaignItemModel, LoyaltyContextualOfferModel
│ │ │ ├── Coupon.swift # CouponItemModel, CouponSetItemModel, RedeemedSMHistoryModel
│ │ │ ├── TokenModel.swift # JWT token model with expiration parsing
│ │ │ ├── Merchant.swift # MerchantModel
│ │ │ ├── ProfileModel.swift # User profile
│ │ │ ├── CardModel.swift # Credit card model
│ │ │ ├── TransactionModel.swift # Transaction history
│ │ │ ├── PointsHistoryModel.swift # Points history
│ │ │ ├── ArticleModel.swift # Content articles
│ │ │ ├── Market.swift # MarketPassDetailsModel
│ │ │ ├── Response.swift # VerifyTicketResponseModel, GenericResponseModel
│ │ │ ├── Models.swift # LoyaltySDKDynatraceEventModel, LoyaltyGiftsForYouPackage
│ │ │ ├── Events.swift # Event-related models
│ │ │ ├── Gifts.swift # Gift models
│ │ │ ├── OfferModel.swift # Offer models
│ │ │ ├── SectionModel.swift # UI section models
│ │ │ ├── CouponFilterModel.swift # Coupon filter UI models
│ │ │ ├── MerchantCategoryModel.swift # Merchant categories
│ │ │ └── QuestionnaireAnswerModel.swift
│ │ ├── screens/ # Pre-built view controllers
│ │ │ ├── MyRewardsViewController/ # Main rewards screen
│ │ │ ├── ProfileViewController/ # User profile screen
│ │ │ ├── CouponViewController/ # Single coupon detail
│ │ │ ├── CouponsetViewController/ # Coupon set detail
│ │ │ └── CampaignViewController/ # WebView for campaign URLs
│ │ ├── cells/ # Reusable table/collection view cells
│ │ │ ├── MyRewardsBannerOfferCollectionViewCell/
│ │ │ ├── MyRewardsBannerOffersScrollTableViewCell/
│ │ │ ├── MyRewardsOfferCollectionViewCell/
│ │ │ ├── MyRewardsOffersScrollTableViewCell/
│ │ │ ├── MyRewardsProfileInfoTableViewCell/
│ │ │ ├── ProfileCouponFiltersTableViewCell/
│ │ │ ├── ProfileCouponTableViewCell/
│ │ │ ├── ProfileFilterCollectionViewCell/
│ │ │ ├── ProfileHeaderTableViewCell/
│ │ │ └── ProfileQuestionnaireTableViewCell/
│ │ ├── Helpers/
│ │ │ ├── WarplyReactMethods.h # React Native bridge (Obj-C header)
│ │ │ └── WarplyReactMethods.m # React Native bridge (Obj-C impl)
│ │ ├── fonts/ # PingLCG font family (.otf)
│ │ ├── Media.xcassets/ # Image assets
│ │ ├── Main.storyboard # Storyboard for campaign WebView
│ │ ├── CopyableLabel.swift # UILabel subclass with copy support
│ │ ├── UIColorExtensions.swift # Color utilities
│ │ ├── ViewControllerExtensions.swift # VC extensions
│ │ ├── XIBLoader.swift # XIB/Bundle loading helpers
│ │ ├── MyEmptyClass.swift # Bundle reference helper
│ │ ├── SwiftWarplyFramework.h # Framework umbrella header
│ │ └── Info.plist
│ └── SwiftWarplyFramework.xcodeproj/
```
## 4. SDK Lifecycle
### Initialization Flow (Required Order)
```swift
// Step 1: Configure (sets environment URLs, appUuid, merchantId)
WarplySDK.shared.configure(
appUuid: "your-32-char-hex-uuid",
merchantId: "",
environment: .production, // or .development
language: "el" // "el" or "en"
)
// Step 2: Initialize (validates config, initializes DB, registers device)
WarplySDK.shared.initialize { success in
// SDK ready to use
}
// OR async:
try await WarplySDK.shared.initialize()
// Step 3: Authenticate user (one of these methods)
// DEI Login:
WarplySDK.shared.deiLogin(email: "user@example.com") { response in ... }
// OR Cosmote flow:
WarplySDK.shared.verifyTicket(guid: "...", ticket: "...") { response in ... }
// OR Cosmote user:
WarplySDK.shared.getCosmoteUser(guid: "...") { response in ... }
```
### What Happens During `initialize()`
1. Validates `appUuid` is not empty
2. Sets `Configuration.baseURL` and `Configuration.host` from stored environment
3. Stores `appUuid` in UserDefaults for NetworkService access
4. Initializes SQLite database (`DatabaseManager.shared.initializeDatabase()`)
5. Performs automatic device registration with comprehensive device info
6. Posts Dynatrace analytics event on success/failure
### Authentication Token Flow
1. Login methods (`deiLogin`, `verifyTicket`, `getCosmoteUser`) receive JWT tokens from server
2. Tokens are parsed into `TokenModel` (automatic JWT `exp` claim extraction)
3. `TokenModel` stored in SQLite `requestVariables` table via `DatabaseManager`
4. `NetworkService` reads tokens from database for authenticated requests
5. Proactive refresh: tokens refreshed 5 minutes before expiration
6. On 401 response: automatic token refresh + request retry
7. Logout: tokens cleared from database
### Environment Configuration
| Environment | Base URL | Host |
|-------------|----------|------|
| `.development` | `https://engage-uat.dei.gr` | `engage-uat.dei.gr` |
| `.production` | `https://engage-prod.dei.gr` | `engage-prod.dei.gr` |
## 5. Core Components
### WarplySDK (`Core/WarplySDK.swift`)
The **sole public API surface** of the framework. All client interactions go through `WarplySDK.shared`.
**Key Responsibilities:**
- SDK configuration and initialization
- All network API calls (campaigns, coupons, merchants, profile, cards, transactions)
- In-memory state management (campaigns, coupons, merchants, coupon sets)
- Event posting (dual: SwiftEventBus + EventDispatcher)
- Campaign URL construction for WebView
- UI presentation helpers (map, supermarket flow, dialogs)
**Internal Architecture:**
- Uses `NetworkService` for all HTTP calls
- Uses `DatabaseManager` for token storage
- Uses `SDKState` (private class) for in-memory data cache
- Uses `UserDefaultsStore` (private class) for preferences
- Uses `EventDispatcher` for modern event dispatching
**State Management Pattern:**
```swift
// All state is stored in SDKState.shared (private)
// Accessed via getter/setter methods on WarplySDK:
WarplySDK.shared.setCampaignList(campaigns) // stores sorted by _sorting
WarplySDK.shared.getCampaignList() // returns filtered (no ccms, no telco, no questionnaire)
WarplySDK.shared.getAllCampaignList() // returns unfiltered
WarplySDK.shared.setCouponList(coupons) // filters active (status==1), sorts by expiration
WarplySDK.shared.getCouponList() // returns active coupons
WarplySDK.shared.setOldCouponList(coupons) // filters redeemed (status==0), sorts by redeemed_date desc
```
**Campaign Processing Logic (Authenticated Users):**
1. Fetch basic campaigns from `/api/mobile/v2/{appUUID}/context/`
2. Fetch personalized campaigns from `/oauth/{appUUID}/context`
3. Merge both arrays
4. Fetch coupon availability
5. Set `_coupon_availability` on each campaign matching its couponset
6. Filter: remove campaigns with `_coupon_availability == 0`
7. Separate carousel items (`_carousel == "true"`)
8. Remove ccms offers, telco, and questionnaire campaigns
9. Sort by `_sorting` field
**Campaign Processing Logic (Unauthenticated Users):**
- Only fetch basic campaigns — return ALL without coupon filtering
## 6. Network Layer
### NetworkService (`Network/NetworkService.swift`)
URLSession-based HTTP client with automatic auth header injection, token refresh, and request/response logging.
**Key Features:**
- Dynamic `baseURL` from `Configuration.baseURL`
- Network connectivity monitoring via `NWPathMonitor`
- Automatic placeholder replacement in URLs and request bodies
- Context response transformation (flattens `{"status":"1","context":{...}}` pattern)
- Proactive token refresh before requests (5 min threshold)
- Automatic 401 retry with token refresh
- Comprehensive request/response logging with sensitive data masking
### Endpoint System (`Network/Endpoints.swift`)
All API routes are defined as cases of the `Endpoint` enum.
**Endpoint Categories:**
| Category | Base Path | Auth Type | Examples |
|----------|-----------|-----------|----------|
| `standardContext` | `/api/mobile/v2/{appUUID}/context/` | Standard (loyalty headers) | getCampaigns, getAvailableCoupons, getCouponSets, getMerchantCategories, getArticles |
| `authenticatedContext` | `/oauth/{appUUID}/context` | Bearer Token | getCoupons, getProfile, getMarketPassDetails, addCard, getCards, deleteCard, getTransactionHistory, getPointsHistory, validateCoupon, redeemCoupon, getCampaignsPersonalized |
| `userManagement` | `/user/{appUUID}/*` | Standard | register, changePassword, resetPassword, requestOtp |
| `authentication` | `/oauth/{appUUID}/*` | Standard | refreshToken, logout |
| `partnerCosmote` | `/partners/*` | Basic/Standard | verifyTicket, getCosmoteUser, deiLogin |
| `session` | `/api/session/{sessionUuid}` | Standard (GET) | getSingleCampaign |
| `analytics` | `/api/async/analytics/{appUUID}/` | Standard | sendEvent |
| `deviceInfo` | `/api/async/info/{appUUID}/` | Standard | sendDeviceInfo |
**Authentication Types:**
| Type | Headers Added |
|------|--------------|
| `.standard` | `loyalty-web-id`, `loyalty-date`, `loyalty-signature` (SHA256 of apiKey+timestamp), platform headers, device headers |
| `.bearerToken` | All standard headers + `Authorization: Bearer {access_token}` (from database) |
| `.basicAuth` | All standard headers + `Authorization: Basic {encoded_credentials}` (for Cosmote partner endpoints) |
**Headers Always Sent:**
- `loyalty-web-id` — merchant ID from Configuration
- `loyalty-date` — Unix timestamp
- `loyalty-signature` — SHA256 hash of `"{apiKey}{timestamp}"`
- `Accept-Encoding: gzip`
- `Accept: application/json`
- `User-Agent: gzip`
- `channel: mobile`
- `loyalty-bundle-id: ios:{bundleId}`
- `unique-device-id: {IDFV}`
- `vendor: apple`, `platform: ios`, `os_version: {version}`
**Placeholder Replacement System:**
- `{appUUID}` → App UUID from UserDefaults
- `{access_token}` → Access token from database
- `{refresh_token}` → Refresh token from database
- `{web_id}` → Merchant ID from Configuration
- `{api_key}` → Empty string (legacy, deprecated)
- `{sessionUuid}` → Extracted from endpoint parameters
**Context Response Transformation:**
```
Server returns: {"status": "1", "context": {"MAPP_CAMPAIGNING": {...}}}
Transformed to: {"status": 1, "MAPP_CAMPAIGNING": {...}}
```
This flattening happens in `transformContextResponse()` for backward compatibility.
**Request Parameter Structures:**
Most POST endpoints wrap parameters in a domain-specific key:
```swift
// Campaigns: {"campaigns": {"action": "retrieve", "language": "el", "filters": {...}}}
// Coupons: {"coupon": {"action": "user_coupons", "details": ["merchant","redemption"], ...}}
// Profile: {"consumer_data": {"action": "handle_user_details", "process": "get"}}
// Cards: {"cards": {"action": "add_card", "card_number": "...", ...}}
// Merchants: {"shops": {"language": "el", "action": "retrieve_multilingual"}}
// Articles: {"content": {"language": "el", "action": "retrieve_multilingual"}}
```
### TokenRefreshManager (`Network/TokenRefreshManager.swift`)
Actor-based coordinator for JWT token refresh with retry and circuit breaker.
**Retry Logic:**
- Configurable `maxRetryAttempts` (default: 3) and `retryDelays` (default: [0.0, 1.0, 5.0])
- 401/4xx errors: no retry (permanent failure)
- 5xx/network errors: retry with backoff
- After all retries fail: clears tokens from database
**Circuit Breaker:**
- States: `closed` (normal) → `open` (blocking) → `halfOpen` (testing recovery)
- Opens after configurable `failureThreshold` (default: 5) consecutive failures
- Recovery timeout: configurable (default: 300s / 5 minutes)
**Request Deduplication:**
- If a refresh is already in progress, subsequent callers await the same `Task`
- Prevents multiple simultaneous refresh API calls
## 7. Database Layer
### DatabaseManager (`Database/DatabaseManager.swift`)
SQLite.swift-based database manager using **raw SQL** (not Expression builders) for all operations.
**Database File:** `{Documents}/WarplyCache_{bundleId}.db`
**Tables (Schema Version 1):**
| Table | Columns | Purpose |
|-------|---------|---------|
| `requestVariables` | `id` INTEGER PK, `client_id` TEXT, `client_secret` TEXT, `access_token` TEXT, `refresh_token` TEXT | JWT token storage (single row, UPSERT) |
| `events` | `_id` INTEGER PK, `type` TEXT, `time` TEXT, `data` BLOB, `priority` INTEGER | Offline analytics event queue |
| `pois` | `id` INTEGER PK, `lat` REAL, `lon` REAL, `radius` REAL | Geofencing points of interest |
| `schema_version` | `id` INTEGER PK, `version` INTEGER UNIQUE, `created_at` TEXT | Migration tracking |
**Schema Migration System:**
- Version tracked in `schema_version` table
- `currentDatabaseVersion = 1`, `supportedVersions = [1]`
- Migration runs in a transaction for atomicity
- Fresh installs: `createAllTables()` + set version
- Upgrades: `migrateDatabase(from:to:)` with per-version migration functions
**Token Management Methods:**
```swift
// Basic CRUD
storeTokens(accessToken:refreshToken:clientId:clientSecret:) // UPSERT
getAccessToken() async throws -> String?
getRefreshToken() async throws -> String?
getClientCredentials() async throws -> (clientId: String?, clientSecret: String?)
clearTokens() async throws
// TokenModel integration (preferred)
storeTokenModel(_ tokenModel: TokenModel) async throws
getTokenModel() async throws -> TokenModel?
getValidTokenModel() async throws -> TokenModel? // nil if expired
getTokenModelSync() throws -> TokenModel? // synchronous variant
updateTokensAtomically(from:to:) async throws // race-condition safe
// Encryption-aware (smart routing)
storeTokenModelSmart(_ tokenModel: TokenModel) // auto-chooses encrypted/plain
getTokenModelSmart() async throws -> TokenModel? // auto-chooses decrypted/plain
// Performance
getCachedTokenModel() async throws -> TokenModel? // 60-second memory cache
```
**Concurrency:** Uses `DispatchQueue(label: "com.warply.database", qos: .utility)` for thread safety.
**Maintenance Methods:** `checkDatabaseIntegrity()`, `vacuumDatabase()`, `recreateDatabase()`, `getDatabaseStats()`
## 8. Security Layer
### FieldEncryption (`Security/FieldEncryption.swift`)
Actor-based AES-256-GCM field-level encryption for sensitive token data.
**Algorithm:** AES-256-GCM (via Apple CryptoKit)
- Provides both encryption AND authentication (AEAD)
- Output format: nonce + ciphertext + authentication tag (combined)
- Keys: 256-bit (32 bytes) from iOS Keychain
**Key Methods:**
```swift
// Low-level (provide your own key)
encryptToken(_ token: String, using key: Data) throws -> Data
decryptToken(_ encryptedData: Data, using key: Data) throws -> String
// High-level (auto key from KeychainManager, with caching)
encryptSensitiveData(_ data: String) async throws -> Data
decryptSensitiveData(_ encryptedData: Data) async throws -> String
// Batch operations (single key retrieval for multiple items)
encryptSensitiveDataBatch(_ dataArray: [String]) async throws -> [Data]
decryptSensitiveDataBatch(_ encryptedDataArray: [Data]) async throws -> [String]
```
**Key Caching:** Encryption key cached in memory for 300 seconds (5 min) to reduce Keychain lookups.
**Encrypted Token Storage:** When encryption is enabled, tokens are stored as Base64-encoded encrypted data in the database. The system detects encrypted vs. plain text by checking if data is valid Base64 but NOT a JWT (JWTs start with "eyJ").
### KeychainManager (`Security/KeychainManager.swift`)
Actor-based iOS Keychain wrapper for hardware-backed encryption key management.
**Key Features:**
- **Bundle ID isolation:** Each client app gets its own Keychain namespace via `com.warply.sdk.{bundleId}`
- **Auto key generation:** `getOrCreateDatabaseKey()` creates 256-bit key on first call via `SecRandomCopyBytes`
- **Security level:** `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` — key only available when device is unlocked, never backed up
**Keychain Item Structure:**
- Service: `com.warply.sdk.{bundleId}`
- Account: `database_encryption_key`
- Class: `kSecClassGenericPassword`
- Value: 32 bytes (256-bit AES key)
## 9. Event System
### EventDispatcher (`Events/EventDispatcher.swift`)
Modern Swift event system designed to eventually replace SwiftEventBus.
**Architecture:**
- Thread-safe using concurrent `DispatchQueue` with barrier writes
- Handlers always dispatched on `DispatchQueue.main`
- Subscription-based with auto-cleanup via `deinit`
**Event Types (Protocol-based):**
```swift
public protocol WarplyEvent {
var name: String { get }
var timestamp: Date { get }
var data: Any? { get }
}
```
**Built-in Event Types:**
| Event Struct | Event Name | Triggered When |
|-------------|------------|----------------|
| `CampaignsRetrievedEvent` | `"campaigns_retrieved"` | Campaigns fetched from server |
| `CouponsRetrievedEvent` | `"coupons_fetched"` | Coupons fetched from server |
| `MarketPassDetailsEvent` | `"market_pass_details_fetched"` | Market pass details received |
| `CCMSRetrievedEvent` | `"ccms_retrieved"` | CCMS campaigns set |
| `SeasonalsRetrievedEvent` | `"seasonals_retrieved"` | Seasonal list set |
| `DynatraceEvent` | `"dynatrace"` | Analytics event |
| `GenericWarplyEvent` | (any string) | Backward compatibility |
**Dual Posting Pattern (CRITICAL):**
All events in WarplySDK are posted to BOTH systems for backward compatibility:
```swift
// Internal helper method in WarplySDK:
private func postFrameworkEvent(_ eventName: String, sender: Any? = nil) {
SwiftEventBus.post(eventName, sender: sender) // Client compatibility
eventDispatcher.post(eventName, sender: sender) // Modern internal
}
```
**Client Subscription API:**
```swift
// String-based (backward compatible)
WarplySDK.shared.subscribe(to: "campaigns_retrieved") { data in ... }
// Type-safe (modern)
WarplySDK.shared.subscribe(CampaignsRetrievedEvent.self) { event in ... }
```
**Dynatrace Analytics Pattern:**
Every SDK API call posts a Dynatrace event on success AND failure:
```swift
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_campaigns_loyalty" // or custom_error_*
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
```
Event naming convention: `custom_{success|error}_{operation}_loyalty`
## 10. Configuration System
### WarplyConfiguration (`Configuration/WarplyConfiguration.swift`)
Codable struct with nested sub-configurations for each subsystem.
**Sub-configurations:**
| Config Struct | Controls | Key Fields |
|--------------|----------|------------|
| `WarplyDatabaseConfig` | Encryption, WAL mode, cache | `encryptionEnabled`, `dataProtectionClass`, `enableWALMode`, `cacheSize`, `useKeychainForKeys` |
| `WarplyTokenConfig` | Refresh retry, circuit breaker | `maxRetryAttempts`, `retryDelays`, `circuitBreakerThreshold`, `circuitBreakerResetTime`, `refreshThresholdMinutes` |
| `WarplyNetworkConfig` | Timeouts, retries, caching | `requestTimeout`, `resourceTimeout`, `maxRetryAttempts`, `retryDelay`, `maxConcurrentRequests`, `enableResponseCaching`, `enableExponentialBackoff`, `allowsCellularAccess` |
| `WarplyLoggingConfig` | Log levels, masking | `logLevel` (.none/.error/.warning/.info/.debug/.verbose), `enableDatabaseLogging`, `enableNetworkLogging`, `enableTokenLogging`, `maskSensitiveData`, `enableFileLogging` |
**Preset Configurations:**
| Preset | Encryption | Log Level | Timeout | Token Retries | Use Case |
|--------|-----------|-----------|---------|---------------|----------|
| `.development` | Off | `.verbose` | 15s | 2 | Local dev, full logging |
| `.production` | On | `.warning` | 30s | 3 | Release builds |
| `.testing` | Off | `.error` | 5s | 1 | Unit/integration tests |
| `.highSecurity` | On | `.error` | 20s | 2 | Sensitive environments, WiFi only |
**Usage:**
```swift
// Apply preset
try await WarplySDK.shared.configure(WarplyConfiguration.production)
// Custom config
var config = WarplyConfiguration.development
config.databaseConfig.encryptionEnabled = true
config.tokenConfig.maxRetryAttempts = 5
try await WarplySDK.shared.configure(config)
// Per-component configuration
try await WarplySDK.shared.configureDatabaseSecurity(dbConfig)
try await WarplySDK.shared.configureTokenManagement(tokenConfig)
try await WarplySDK.shared.configureLogging(loggingConfig)
try await WarplySDK.shared.configureNetwork(networkConfig)
```
**Validation:** Each config struct has a `validate()` method that throws `ConfigurationError` with specific error types and recovery suggestions.
**Serialization:** `WarplyConfiguration` is `Codable` — can be saved/loaded as JSON via `toJSONData()`, `fromJSONData()`, `saveToFile(at:)`, `loadFromFile(at:)`.
## 11. Data Models
All models use **dictionary-based initialization** (`init(dictionary: [String: Any])`) to parse JSON responses.
### Key Model Classes
| Model | File | Key Fields | Notes |
|-------|------|------------|-------|
| `CampaignItemModel` | Campaign.swift | `session_uuid`, `index_url`, `title`, `subtitle`, `offer_category`, `logo_url`, `couponset`, `_sorting`, `_type`, `_carousel`, `_coupon_availability`, `_banner_img`, `_filter`, `_start_date`, `_end_date` | `class` (not struct), mutable properties via `_fieldName` pattern |
| `LoyaltyContextualOfferModel` | Campaign.swift | `_loyaltyCampaignId`, `_eligibleAssets`, `_offerName`, `_sessionId` | CCMS contextual offers |
| `CouponItemModel` | Coupon.swift | `couponset_uuid`, `name`, `coupon`, `barcode`, `status` (1=active, 0=redeemed), `expiration`, `redeemed_date`, `couponset_data`, `merchant_details` | Status: 1=active, 0=redeemed |
| `CouponSetItemModel` | Coupon.swift | `_uuid`, `_name`, `_description`, `_img_preview`, `_expiration`, `_discount`, `_discount_type`, `_final_price`, `_couponset_type`, `_merchant_uuid`, `_terms`, `_shop_availability` | Expiration stored as raw date string "yyyy-MM-dd HH:mm" |
| `TokenModel` | TokenModel.swift | `accessToken`, `refreshToken`, `clientId`, `clientSecret`, `expirationDate`, `isExpired`, `shouldRefresh`, `isValid`, `canRefresh` | `struct`, JWT auto-parsing, 5-min proactive refresh window |
| `MerchantModel` | Merchant.swift | Standard merchant fields | Location-aware merchant data |
| `ProfileModel` | ProfileModel.swift | User profile fields | Consumer data |
| `CardModel` | CardModel.swift | Card number, issuer, holder, expiration | Credit card data |
| `TransactionModel` | TransactionModel.swift | `transactionDate`, transaction details | Sorted by date descending |
| `PointsHistoryModel` | PointsHistoryModel.swift | `entryDate`, points entries | Sorted by date descending |
| `MarketPassDetailsModel` | Market.swift | Supermarket pass details | Market/supermarket feature |
| `RedeemedSMHistoryModel` | Coupon.swift | `_totalRedeemedValue`, `_redeemedCouponList` | Supermarket coupon redemption history |
| `ArticleModel` | ArticleModel.swift | Content/article fields | Carousel content |
| `MerchantCategoryModel` | MerchantCategoryModel.swift | Category fields | Merchant categorization |
| `VerifyTicketResponseModel` | Response.swift | `getStatus` (1=success) | Generic API response wrapper |
| `GenericResponseModel` | Response.swift | Generic response fields | Flexible response wrapper |
| `LoyaltySDKDynatraceEventModel` | Models.swift | `_eventName`, `_parameters` | Dynatrace analytics event payload |
| `LoyaltyGiftsForYouPackage` | Models.swift/Gifts.swift | Seasonal gift data | Seasonal promotions |
### Model Property Accessor Pattern
Models use a **underscore-prefixed computed property** pattern for public access:
```swift
// Private stored property
private var category_title: String?
// Public accessor (getter + setter)
public var _category_title: String? {
get { return self.category_title }
set(newValue) { self.category_title = newValue }
}
```
Some newer models use **read-only** accessors:
```swift
public var _uuid: String { get { return self.uuid ?? "" } }
```
### Date Handling in Models
- **CampaignItemModel:** Raw strings from server, formatted via `formattedStartDate(format:)` / `formattedEndDate(format:)`
- **CouponSetItemModel:** Expiration as "yyyy-MM-dd HH:mm", formatted via `formattedExpiration(format:)`, `_expiration_formatted` (dd/MM/yyyy)
- **CouponItemModel:** Dates converted during init to "dd/MM/yyyy" display format, `redeemed_date` as `Date` object
- Input formats tried: `"yyyy-MM-dd HH:mm:ssZZZZZ"`, `"yyyy-MM-dd HH:mm:ss"`, `"yyyy-MM-dd HH:mm:ss.SSSSSS"`
### URL Cleaning
Campaign model has a static helper that cleans escaped URLs:
```swift
private static func cleanEscapedUrl(_ url: String?) -> String? {
return url?.replacingOccurrences(of: "\\/", with: "/")
}
```
Applied to: `index_url`, `logo_url`, `banner_img`, `coupon_img`, `campaign_url`, `banner_img_mobile`
## 12. UI Layer
### Pre-built Screens
All screens use **XIB-based** layouts (not SwiftUI) with `Bundle.frameworkBundle` for resource loading.
| Screen | Storyboard ID | Purpose |
|--------|--------------|---------|
| `MyRewardsViewController` | — (XIB) | Main rewards dashboard with campaigns, offers, banners |
| `ProfileViewController` | — (XIB) | User profile with coupons, filters, questionnaires |
| `CouponViewController` | — (XIB) | Single coupon detail view |
| `CouponsetViewController` | — (XIB) | Coupon set detail view |
| `CampaignViewController` | `"CampaignViewController"` (Storyboard) | WebView for campaign URLs |
### CampaignViewController (WebView)
Used to display campaign web content. Configured via:
```swift
let vc = storyboard.instantiateViewController(withIdentifier: "CampaignViewController") as! CampaignViewController
vc.campaignUrl = url // Campaign web URL
vc.params = params // JSON string with tokens and config
vc.showHeader = false // Header visibility
vc.isPresented = true/false // Modal vs push navigation
```
### Navigation Pattern
Screens support both push (navigation controller) and modal presentation:
```swift
if let navigationController = controller.navigationController {
vc.isPresented = false
navigationController.pushViewController(vc, animated: true)
} else {
vc.isPresented = true
vc.modalPresentationStyle = .fullScreen
controller.present(vc, animated: true, completion: nil)
}
```
### Cell Architecture
Table/Collection view cells are XIB-based, each in its own directory:
- `MyRewardsBannerOfferCollectionViewCell` — Banner offer in collection view
- `MyRewardsOfferCollectionViewCell` — Standard offer in collection view
- `MyRewardsBannerOffersScrollTableViewCell` — Horizontal scrolling banner offers
- `MyRewardsOffersScrollTableViewCell` — Horizontal scrolling offers
- `MyRewardsProfileInfoTableViewCell` — Profile info header
- `ProfileCouponTableViewCell` — Coupon row in profile
- `ProfileCouponFiltersTableViewCell` — Filter chips for coupons
- `ProfileFilterCollectionViewCell` — Individual filter chip
- `ProfileHeaderTableViewCell` — Profile section header
- `ProfileQuestionnaireTableViewCell` — Questionnaire entry
### Fonts
Custom fonts bundled: **PingLCG** family (Bold, Light, Regular) in `.otf` format.
### Bundle Resource Loading
Framework resources loaded via `Bundle.frameworkBundle` (defined in `XIBLoader.swift`/`MyEmptyClass.swift`), which resolves the correct bundle whether using CocoaPods resource bundles or SPM.
## 13. API Patterns & Conventions
### Dual API Surface (Callback + Async/Await)
Every public API method has two variants:
```swift
// Callback-based (original pattern)
public func getCoupons(language: String, completion: @escaping ([CouponItemModel]?) -> Void, failureCallback: @escaping (Int) -> Void)
// Async/await wrapper (bridges to callback version)
public func getCoupons(language: String) async throws -> [CouponItemModel] {
return try await withCheckedThrowingContinuation { continuation in
getCoupons(language: language, completion: { coupons in
if let coupons = coupons {
continuation.resume(returning: coupons)
} else {
continuation.resume(throwing: WarplyError.networkError)
}
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
```
### Standard Method Body Pattern
Every API method follows this exact pattern:
1. Create `Task { }` block
2. Call `networkService.requestRaw(endpoint)` or specific convenience method
3. Parse response inside `await MainActor.run { }`
4. Check status (`response["status"] as? Int == 1` or `response["MAPP_*-status"] as? Int == 1`)
5. Post Dynatrace success/error event
6. Call completion handler on main thread
### Response Status Checking
Different response structures have different status locations:
```swift
// Standard context responses (after transformation):
response["status"] as? Int == 1
response["MAPP_CAMPAIGNING-status"] as? Int == 1
response["MAPP_COUPON-status"] as? Int == 1
response["MAPP_SHOPS-status"] as? Int == 1
// VerifyTicketResponseModel:
tempResponse.getStatus == 1
```
### Language Handling Pattern
Methods with optional language default to `applicationLocale`:
```swift
public func getCoupons(language: String? = nil, ...) {
let finalLanguage = language ?? self.applicationLocale
// use finalLanguage
}
```
### Complete Public API Reference
**Authentication:**
- `configure(appUuid:merchantId:environment:language:)`
- `initialize(callback:)` / `initialize() async throws`
- `deiLogin(email:completion:failureCallback:)` / `deiLogin(email:) async throws`
- `verifyTicket(guid:ticket:completion:)`
- `getCosmoteUser(guid:completion:)` / `getCosmoteUser(guid:) async throws`
- `logout(completion:)`
**Campaigns:**
- `getCampaigns(language:filters:completion:failureCallback:)` / `getCampaigns(language:filters:) async throws`
- `getCampaignsPersonalized(language:filters:completion:failureCallback:)` / async variant
- `getSingleCampaign(sessionUuid:completion:)` / async variant
- `getSupermarketCampaign(language:completion:)` / async variant
**Coupons:**
- `getCoupons(language:completion:failureCallback:)` / async variant
- `getCouponsUniversal(language:completion:failureCallback:)`
- `getCouponSets(language:completion:failureCallback:)` / async variant
- `getAvailableCoupons(completion:)` / async variant
- `validateCoupon(_:completion:)` / async variant
- `redeemCoupon(productId:productUuid:merchantId:completion:)` / async variant
**User Management:**
- `changePassword(oldPassword:newPassword:completion:)` / async variant
- `resetPassword(email:completion:)` / async variant
- `requestOtp(phoneNumber:completion:)` / async variant
- `getProfile(completion:failureCallback:)` / async variant
**Cards:**
- `addCard(cardNumber:cardIssuer:cardHolder:expirationMonth:expirationYear:completion:)` / async variant
- `getCards(completion:)` / async variant
- `deleteCard(token:completion:)` / async variant
**Transactions:**
- `getTransactionHistory(productDetail:completion:)` / async variant
- `getPointsHistory(completion:)` / async variant
**Merchants:**
- `getMerchants(language:categories:defaultShown:center:tags:uuid:distance:parentUuids:completion:failureCallback:)` / async variant
- `getMerchantCategories(language:completion:failureCallback:)` / async variant
**Content:**
- `getArticles(language:categories:completion:failureCallback:)` / async variant
**Market/Supermarket:**
- `getMarketPassDetails(completion:failureCallback:)` / async variant
- `getRedeemedSMHistory(language:completion:failureCallback:)` / async variant
- `openSupermarketsMap(_:)` — presents map WebView
- `openSuperMarketsFlow(_:)` — presents supermarket campaign
**State:**
- `setCampaignList(_:)` / `getCampaignList()` / `getAllCampaignList()`
- `setCouponList(_:)` / `getCouponList()`
- `setOldCouponList(_:)` / `getOldCouponList()`
- `setCouponSetList(_:)` / `getCouponSetList()`
- `setMerchantList(_:)` / `getMerchantList()`
- `setCarouselList(_:)` / `getCarouselList()`
- `setSupermarketCampaign(_:)` / `getSupermarketCampaign()`
- `setMarketPassDetails(_:)` / `getMarketPassDetails()`
- `setCCMSLoyaltyCampaigns(campaigns:)` / `getCCMSLoyaltyCampaigns()`
- `setSeasonalList(_:)` / `getSeasonalList()`
**Utilities:**
- `constructCampaignUrl(_:)` / `constructCampaignParams(_:)` / `constructCampaignParams(campaign:isMap:)`
- `constructCcmsUrl(_:)`
- `showDialog(_:_:_:)`
- `getMarketPassMapUrl()`
- `getNetworkStatus()` — returns 1 (connected) or 0 (disconnected)
- `updateRefreshToken(accessToken:refreshToken:)`
- `updateDeviceToken(_:)`
## 14. Coding Standards & Conventions
### Naming Conventions
- **Files:** PascalCase matching the primary type (`CampaignItemModel``Campaign.swift`)
- **Classes/Structs:** PascalCase (`CampaignItemModel`, `NetworkService`)
- **Private properties:** snake_case for model fields (`category_title`, `banner_img`)
- **Public accessors:** underscore prefix (`_category_title`, `_banner_img`)
- **Constants:** PascalCase for enum cases, camelCase for static properties
- **Endpoint enum cases:** camelCase (`getCampaigns`, `verifyTicket`)
### Logging Convention
Emoji-prefixed print statements throughout the codebase:
```
✅ Success operations
❌ Errors/failures
⚠️ Warnings
🔴 Critical errors
🟡 Token should refresh
🟢 Token valid
🔐 Token/auth operations
🔒 Encryption operations
🔓 Decryption operations
🔑 Key operations
🗄️ Database operations
🗑️ Deletion/clearing
📊 Analytics/stats
📤 Request logging
📥 Response logging
🔗 URL operations
🔄 Refresh/retry/update
🚨 Circuit breaker
🧪 Test operations
💡 Suggestions
⏱️ Timing
🔧 Configuration
🏭 Production
🚦 Request queue
⚛️ Atomic operations
```
### Access Control
- **`public`:** All types, methods, and properties intended for client use
- **`public final class`:** WarplySDK, NetworkService (prevent subclassing)
- **`private`/`internal`:** Implementation details, state management
- **`actor`:** All security-critical components (TokenRefreshManager, FieldEncryption, KeychainManager)
### Error Handling Convention
- Errors are logged immediately at the point of occurrence
- Every error posts a Dynatrace analytics event
- Callback methods return `nil` on failure, async methods `throw`
- `handleError()` private method centralizes error conversion and logging
### UserDefaults Keys
- `appUuidUD` — App UUID
- `merchantIdUD` — Merchant ID
- `languageUD` — Language
- `isDarkModeEnabledUD` — Dark mode flag
- `environmentUD` — "development" or "production"
- `trackersEnabled` — Analytics tracking flag
- `device_token` — Push notification device token
### Configuration Static Properties
```swift
Configuration.baseURL // Current base URL
Configuration.host // Current host
Configuration.errorDomain // Error domain (same as host)
Configuration.merchantId // Merchant ID
Configuration.language // "el" or "en"
Configuration.verifyURL // Verification URL
```
## 15. Dependencies & Distribution
### External Dependencies
| Dependency | Version | Purpose |
|-----------|---------|---------|
| `SQLite.swift` | `0.12.2` (exact) | SQLite database access (raw SQL, not ORM) |
| `SwiftEventBus` | `5.0.0+` | Event bus for client-facing events (backward compatibility) |
| `RSBarcodes_Swift` | `5.2.0+` | Barcode generation for coupon display |
### Distribution Methods
**CocoaPods** (`SwiftWarplyFramework.podspec`):
```ruby
pod 'SwiftWarplyFramework', '~> 2.3.0'
```
- Resource bundles: `ResourcesBundle` containing `.xcassets` and `.otf` fonts
- Source files: `SwiftWarplyFramework/SwiftWarplyFramework/**/*.{h,m,swift,xib,storyboard}`
- Excludes: build artifacts, xcodeproj files
**Swift Package Manager** (`Package.swift`):
```swift
.package(url: "https://git.warp.ly/open-source/warply_sdk_framework.git", from: "2.3.0")
```
- Target path: `SwiftWarplyFramework/SwiftWarplyFramework`
- Excludes: `Helpers/WarplyReactMethods.h`, `Helpers/WarplyReactMethods.m`, `Info.plist`
- Resources: `.process()` for xcassets, fonts, storyboards, XIBs
### System Frameworks Used
- `Foundation` — Core types
- `UIKit` — UI components
- `Network``NWPathMonitor` for connectivity
- `CryptoKit` — AES-256-GCM encryption
- `Security` — Keychain Services
- `CommonCrypto` — SHA256 hashing for loyalty-signature header
## 16. Common Tasks & Recipes
### Adding a New API Endpoint
1. **Add endpoint case** in `Endpoints.swift`:
```swift
case myNewEndpoint(param1: String, param2: Int)
```
2. **Add path** in the `path` computed property:
```swift
case .myNewEndpoint:
return "/oauth/{appUUID}/context" // or appropriate path
```
3. **Add method** (most are POST):
```swift
case .myNewEndpoint:
return .POST
```
4. **Add parameters** with correct wrapper key:
```swift
case .myNewEndpoint(let param1, let param2):
return ["consumer_data": ["action": "my_action", "param1": param1, "param2": param2]]
```
5. **Set auth type** in `authType`:
```swift
case .myNewEndpoint:
return .bearerToken // or .standard
```
6. **Set category** in `category`:
```swift
case .myNewEndpoint:
return .authenticatedContext
```
7. **Add method** in `WarplySDK.swift` (both callback + async variants):
```swift
// Callback version
public func myNewMethod(param1: String, param2: Int, completion: @escaping (MyModel?) -> Void, failureCallback: @escaping (Int) -> Void) {
Task {
do {
let endpoint = Endpoint.myNewEndpoint(param1: param1, param2: param2)
let response = try await networkService.requestRaw(endpoint)
await MainActor.run {
if response["status"] as? Int == 1 {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_my_new_method_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
// Parse and return result
completion(parsedResult)
} else {
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_error_my_new_method_loyalty"
dynatraceEvent._parameters = nil
self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
failureCallback(-1)
}
}
} catch {
await MainActor.run {
self.handleError(error, context: "myNewMethod", endpoint: "myNewEndpoint", failureCallback: failureCallback)
}
}
}
}
// Async variant
public func myNewMethod(param1: String, param2: Int) async throws -> MyModel {
return try await withCheckedThrowingContinuation { continuation in
myNewMethod(param1: param1, param2: param2, completion: { result in
if let result = result { continuation.resume(returning: result) }
else { continuation.resume(throwing: WarplyError.networkError) }
}, failureCallback: { errorCode in
continuation.resume(throwing: WarplyError.unknownError(errorCode))
})
}
}
```
### Adding a New Data Model
1. Create file in `models/` directory
2. Use `class` (not struct) for consistency with existing models
3. Implement `init(dictionary: [String: Any])` for JSON parsing
4. Use `_fieldName` accessor pattern for public properties
5. Handle optional fields with `?? ""` or `?? 0` defaults
6. Clean URLs with `cleanEscapedUrl()` if applicable
### Adding a New Database Table
1. Increment `currentDatabaseVersion` in `DatabaseManager.swift`
2. Add new version to `supportedVersions` array
3. Create migration function `performMigrationToV{N}()`
4. Add case to `performMigration(to:)` switch
5. Create table using raw SQL (`try db.execute(...)`)
### Adding a New Event Type
1. Create struct conforming to `WarplyEvent` in `EventDispatcher.swift`
2. Add convenience method in `EventDispatcher` extension
3. Add `InternalFrameworkEvent` case in `WarplySDK.swift` if it's internal
4. Post using `postFrameworkEvent()` or `postInternalFrameworkEvent()`
5. **Always post to both SwiftEventBus and EventDispatcher**
### Adding a New Configuration Option
1. Add property to appropriate config struct (`WarplyDatabaseConfig`, etc.)
2. Add validation in `validate()` method
3. Add to `getSummary()` return dictionary
4. Add to the preset configurations (`.development`, `.production`, etc.)
5. Ensure Codable compliance (add to CodingKeys if needed)
## 17. Error Handling
### Error Types
**`WarplyError`** (public, SDK-level):
| Case | Code | Description |
|------|------|-------------|
| `.networkError` | -1000 | Generic network failure |
| `.invalidResponse` | -1001 | Invalid/unparseable response |
| `.authenticationFailed` | 401 | Auth failure |
| `.dataParsingError` | -1002 | JSON parsing failure |
| `.serverError(Int)` | (code) | HTTP server error |
| `.noInternetConnection` | -1009 | No network connectivity |
| `.requestTimeout` | -1001 | Request timed out |
| `.unknownError(Int)` | (code) | Catch-all |
**`NetworkError`** (public, network-level):
| Case | Code | Description |
|------|------|-------------|
| `.invalidURL` | -1001 | Bad URL construction |
| `.noData` | -1002 | Empty response body |
| `.decodingError(Error)` | -1003 | JSON decode failure |
| `.serverError(Int)` | (code) | HTTP 4xx/5xx |
| `.networkError(Error)` | (varies) | URLSession error |
| `.authenticationRequired` | 401 | Token needed/expired |
| `.invalidResponse` | -1004 | Non-JSON response |
**`DatabaseError`** (internal):
- `.connectionNotAvailable` DB not initialized
- `.tableCreationFailed` Schema creation error
- `.queryFailed(String)` SQL execution error
- `.migrationFailed(String)` Version migration error
- `.corruptedDatabase` Integrity check failed
**`TokenRefreshError`** (public):
- `.noTokensAvailable` No tokens in DB
- `.invalidRefreshToken` Bad refresh token
- `.networkError(Error)` Network failure during refresh
- `.serverError(Int)` Server error during refresh
- `.maxRetriesExceeded` All retry attempts failed
- `.refreshInProgress` Concurrent refresh attempt
- `.invalidResponse` Bad refresh response
**`EncryptionError`** (internal):
- `.invalidKey` (4001), `.encryptionFailed` (4002), `.decryptionFailed` (4003), `.invalidData` (4004), `.keyGenerationFailed` (4005)
**`ConfigurationError`** (public):
- `.invalidRefreshThreshold`, `.invalidRetryAttempts`, `.retryDelaysMismatch`, `.invalidTimeout`, `.invalidCacheSize`, `.invalidCircuitBreakerThreshold`, etc.
- All include `errorDescription` and `recoverySuggestion`
### Error Flow
1. Network/DB error occurs caught in `catch` block
2. `handleError()` called converts to `WarplyError` via `convertNetworkError()`
3. Error logged with context via `logError()`
4. Dynatrace error event posted via `postErrorAnalytics()`
5. `failureCallback(errorCode)` called on main thread
## 18. Testing
### Mock Support
**`MockNetworkService`** (available in `#if DEBUG`):
```swift
let mockService = MockNetworkService()
mockService.setMockResponse(["status": 1, "result": [...]], for: .getCampaigns(...))
mockService.setShouldFail(true, with: .networkError(...))
```
### Test Configuration
Use `WarplyConfiguration.testing` preset:
- Encryption disabled for simpler test setup
- 5-second request timeout for fast tests
- Single retry attempt
- 0.1s retry delay
- Analytics and auto-registration disabled
### SQLite Test
```swift
let result = await WarplySDK.shared.testSQLiteConnection()
// Returns true if SQLite.swift is working correctly
```
### Database Diagnostics
```swift
// Check integrity
let isIntact = try await DatabaseManager.shared.checkDatabaseIntegrity()
// Get stats
let stats = try await DatabaseManager.shared.getDatabaseStats()
// Returns (tokensCount: Int, eventsCount: Int, poisCount: Int)
// Get version info
let versionInfo = try await DatabaseManager.shared.getDatabaseVersionInfo()
// Token validation
let validationResult = try await DatabaseManager.shared.validateStoredTokens()
// Token status
let status = try await DatabaseManager.shared.getTokenStatus()
```
### Configuration Diagnostics
```swift
let summary = WarplySDK.shared.getConfigurationSummary()
let config = WarplySDK.shared.getCurrentConfiguration()
```
### Encryption Diagnostics
```swift
let encryptionStats = try await DatabaseManager.shared.getEncryptionStats()
let isEncryptionWorking = await DatabaseManager.shared.validateEncryptionSetup()
let keychainDiag = await KeychainManager.shared.getDiagnosticInfo()
```
---
## Quick Reference Card
| What | Where | Key |
|------|-------|-----|
| All public APIs | `Core/WarplySDK.swift` | `WarplySDK.shared` |
| API routes | `Network/Endpoints.swift` | `Endpoint` enum |
| HTTP client | `Network/NetworkService.swift` | `NetworkService.shared` |
| Token refresh | `Network/TokenRefreshManager.swift` | `TokenRefreshManager.shared` |
| Token storage | `Database/DatabaseManager.swift` | `DatabaseManager.shared` |
| Token model | `models/TokenModel.swift` | `TokenModel` struct |
| Encryption | `Security/FieldEncryption.swift` | `FieldEncryption.shared` |
| Keychain | `Security/KeychainManager.swift` | `KeychainManager.shared` |
| Events | `Events/EventDispatcher.swift` | `EventDispatcher.shared` |
| Config | `Configuration/WarplyConfiguration.swift` | `WarplyConfiguration` struct |
| Campaigns | `models/Campaign.swift` | `CampaignItemModel` class |
| Coupons | `models/Coupon.swift` | `CouponItemModel` class |
**Critical Rules for AI Agents:**
1. **Always maintain dual event posting** (SwiftEventBus + EventDispatcher)
2. **Always post Dynatrace analytics** on both success and failure paths
3. **Always provide both callback AND async/await** variants for public methods
4. **Always dispatch completion callbacks on main thread** via `MainActor.run`
5. **Use raw SQL** in DatabaseManager (not SQLite.swift Expression builders)
6. **Never log sensitive data** (tokens, passwords, card numbers) — use masking
7. **All security components must be actors** (not classes)
8. **Models use `class` not `struct`** with `_fieldName` accessor pattern
9. **Token storage is in database only** — never in UserDefaults
10. **All response parsing must handle the context transformation** pattern