Manos Chorianopoulos

add AI Agent Skill File

Showing 1 changed file with 1162 additions and 0 deletions
1 +# SwiftWarplyFramework — AI Agent Skill File
2 +
3 +> Comprehensive knowledge base for AI coding agents (Claude Code, Gemini CLI, Codex, OpenCode) working on the SwiftWarplyFramework iOS SDK.
4 +
5 +---
6 +
7 +## 1. Project Overview
8 +
9 +**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.
10 +
11 +- **Language:** Swift 5.9+
12 +- **Minimum iOS:** 17.0
13 +- **Repository:** `https://git.warp.ly/open-source/warply_sdk_framework.git`
14 +- **Framework Type:** Distributable iOS framework (CocoaPods + SPM)
15 +- **Entry Point:** `WarplySDK.shared` (singleton)
16 +- **Backend:** RESTful API on `engage-uat.dei.gr` (UAT) / `engage-prod.dei.gr` (production)
17 +- **Auth Model:** JWT tokens (access + refresh) stored in local SQLite database, automatic refresh with circuit breaker
18 +
19 +### What This SDK Does
20 +- Authenticates users via DEI login (email-based) or Cosmote partner flow (GUID-based)
21 +- Manages loyalty campaigns, coupons, coupon sets, and personalized offers
22 +- Handles card management (add/get/delete credit cards)
23 +- Provides transaction and points history
24 +- Manages supermarket/market pass deals with map integration
25 +- Sends analytics events (Dynatrace integration)
26 +- Provides pre-built UI screens (MyRewards, Profile, Coupon, Campaign WebView)
27 +
28 +## 2. Architecture & Design Patterns
29 +
30 +### Core Patterns
31 +
32 +| Pattern | Where Used | Details |
33 +|---------|-----------|---------|
34 +| **Singleton** | `WarplySDK.shared`, `NetworkService.shared`, `DatabaseManager.shared`, `EventDispatcher.shared` | Main entry points for all subsystems |
35 +| **Actor Isolation** | `TokenRefreshManager`, `FieldEncryption`, `KeychainManager`, `TokenRefreshCircuitBreaker`, `RequestQueue` | Thread-safe concurrent access for security-critical components |
36 +| **Enum-based Routing** | `Endpoint` enum | All API endpoints defined as enum cases with computed properties for path, method, parameters, auth type |
37 +| **Dual API Surface** | All public SDK methods | Every API has both callback-based and `async/await` variants |
38 +| **Event Bus (Dual)** | `SwiftEventBus` + `EventDispatcher` | Events posted to both systems for backward compatibility |
39 +| **PropertyWrapper** | `@UserDefault<T>` | Type-safe UserDefaults access |
40 +| **Circuit Breaker** | `TokenRefreshCircuitBreaker` | Prevents excessive token refresh attempts |
41 +| **Builder/Factory** | `WarplyConfiguration` presets | `.development`, `.production`, `.testing`, `.highSecurity` |
42 +
43 +### Dependency Flow
44 +```
45 +WarplySDK (public API facade)
46 + ├── NetworkService (HTTP requests)
47 + │ ├── Endpoint (URL/param/auth routing)
48 + │ └── TokenRefreshManager (actor, JWT refresh)
49 + │ └── DatabaseManager (token storage)
50 + ├── DatabaseManager (SQLite.swift)
51 + │ └── FieldEncryption (actor, AES-256-GCM)
52 + │ └── KeychainManager (actor, hardware-backed keys)
53 + ├── EventDispatcher (modern event system)
54 + ├── SDKState (in-memory campaign/coupon state)
55 + └── UserDefaultsStore (non-sensitive preferences)
56 +```
57 +
58 +### Threading Model
59 +- **Main thread:** UI operations, completion callbacks (all callbacks dispatched via `MainActor.run`)
60 +- **Background:** Network requests via `async/await`, database operations
61 +- **Actor isolation:** Token refresh, encryption, keychain access — all actors prevent data races
62 +- **Configuration queue:** `DispatchQueue(label: "com.warply.sdk.configuration")` for thread-safe config updates
63 +
64 +## 3. Directory Structure
65 +
66 +```
67 +SwiftWarplyFramework/
68 +├── Package.swift # SPM package definition
69 +├── SwiftWarplyFramework.podspec # CocoaPods spec (v2.3.0)
70 +├── skill.md # This file
71 +├── SwiftWarplyFramework/
72 +│ ├── SwiftWarplyFramework/
73 +│ │ ├── Core/
74 +│ │ │ └── WarplySDK.swift # Main SDK singleton — ALL public APIs
75 +│ │ ├── Network/
76 +│ │ │ ├── Endpoints.swift # Endpoint enum (all API routes)
77 +│ │ │ ├── NetworkService.swift # HTTP client, request building, auth headers
78 +│ │ │ └── TokenRefreshManager.swift # Actor-based token refresh with retry/circuit breaker
79 +│ │ ├── Database/
80 +│ │ │ └── DatabaseManager.swift # SQLite.swift wrapper, token/event/POI storage
81 +│ │ ├── Security/
82 +│ │ │ ├── FieldEncryption.swift # AES-256-GCM field-level encryption (actor)
83 +│ │ │ └── KeychainManager.swift # iOS Keychain key management (actor)
84 +│ │ ├── Configuration/
85 +│ │ │ ├── WarplyConfiguration.swift # Main config container + presets
86 +│ │ │ ├── DatabaseConfiguration.swift
87 +│ │ │ ├── TokenConfiguration.swift
88 +│ │ │ ├── NetworkConfiguration.swift
89 +│ │ │ └── LoggingConfiguration.swift
90 +│ │ ├── Events/
91 +│ │ │ └── EventDispatcher.swift # Type-safe event system (replaces SwiftEventBus internally)
92 +│ │ ├── models/ # All data models
93 +│ │ │ ├── Campaign.swift # CampaignItemModel, LoyaltyContextualOfferModel
94 +│ │ │ ├── Coupon.swift # CouponItemModel, CouponSetItemModel, RedeemedSMHistoryModel
95 +│ │ │ ├── TokenModel.swift # JWT token model with expiration parsing
96 +│ │ │ ├── Merchant.swift # MerchantModel
97 +│ │ │ ├── ProfileModel.swift # User profile
98 +│ │ │ ├── CardModel.swift # Credit card model
99 +│ │ │ ├── TransactionModel.swift # Transaction history
100 +│ │ │ ├── PointsHistoryModel.swift # Points history
101 +│ │ │ ├── ArticleModel.swift # Content articles
102 +│ │ │ ├── Market.swift # MarketPassDetailsModel
103 +│ │ │ ├── Response.swift # VerifyTicketResponseModel, GenericResponseModel
104 +│ │ │ ├── Models.swift # LoyaltySDKDynatraceEventModel, LoyaltyGiftsForYouPackage
105 +│ │ │ ├── Events.swift # Event-related models
106 +│ │ │ ├── Gifts.swift # Gift models
107 +│ │ │ ├── OfferModel.swift # Offer models
108 +│ │ │ ├── SectionModel.swift # UI section models
109 +│ │ │ ├── CouponFilterModel.swift # Coupon filter UI models
110 +│ │ │ ├── MerchantCategoryModel.swift # Merchant categories
111 +│ │ │ └── QuestionnaireAnswerModel.swift
112 +│ │ ├── screens/ # Pre-built view controllers
113 +│ │ │ ├── MyRewardsViewController/ # Main rewards screen
114 +│ │ │ ├── ProfileViewController/ # User profile screen
115 +│ │ │ ├── CouponViewController/ # Single coupon detail
116 +│ │ │ ├── CouponsetViewController/ # Coupon set detail
117 +│ │ │ └── CampaignViewController/ # WebView for campaign URLs
118 +│ │ ├── cells/ # Reusable table/collection view cells
119 +│ │ │ ├── MyRewardsBannerOfferCollectionViewCell/
120 +│ │ │ ├── MyRewardsBannerOffersScrollTableViewCell/
121 +│ │ │ ├── MyRewardsOfferCollectionViewCell/
122 +│ │ │ ├── MyRewardsOffersScrollTableViewCell/
123 +│ │ │ ├── MyRewardsProfileInfoTableViewCell/
124 +│ │ │ ├── ProfileCouponFiltersTableViewCell/
125 +│ │ │ ├── ProfileCouponTableViewCell/
126 +│ │ │ ├── ProfileFilterCollectionViewCell/
127 +│ │ │ ├── ProfileHeaderTableViewCell/
128 +│ │ │ └── ProfileQuestionnaireTableViewCell/
129 +│ │ ├── Helpers/
130 +│ │ │ ├── WarplyReactMethods.h # React Native bridge (Obj-C header)
131 +│ │ │ └── WarplyReactMethods.m # React Native bridge (Obj-C impl)
132 +│ │ ├── fonts/ # PingLCG font family (.otf)
133 +│ │ ├── Media.xcassets/ # Image assets
134 +│ │ ├── Main.storyboard # Storyboard for campaign WebView
135 +│ │ ├── CopyableLabel.swift # UILabel subclass with copy support
136 +│ │ ├── UIColorExtensions.swift # Color utilities
137 +│ │ ├── ViewControllerExtensions.swift # VC extensions
138 +│ │ ├── XIBLoader.swift # XIB/Bundle loading helpers
139 +│ │ ├── MyEmptyClass.swift # Bundle reference helper
140 +│ │ ├── SwiftWarplyFramework.h # Framework umbrella header
141 +│ │ └── Info.plist
142 +│ └── SwiftWarplyFramework.xcodeproj/
143 +```
144 +
145 +## 4. SDK Lifecycle
146 +
147 +### Initialization Flow (Required Order)
148 +
149 +```swift
150 +// Step 1: Configure (sets environment URLs, appUuid, merchantId)
151 +WarplySDK.shared.configure(
152 + appUuid: "your-32-char-hex-uuid",
153 + merchantId: "",
154 + environment: .production, // or .development
155 + language: "el" // "el" or "en"
156 +)
157 +
158 +// Step 2: Initialize (validates config, initializes DB, registers device)
159 +WarplySDK.shared.initialize { success in
160 + // SDK ready to use
161 +}
162 +// OR async:
163 +try await WarplySDK.shared.initialize()
164 +
165 +// Step 3: Authenticate user (one of these methods)
166 +// DEI Login:
167 +WarplySDK.shared.deiLogin(email: "user@example.com") { response in ... }
168 +// OR Cosmote flow:
169 +WarplySDK.shared.verifyTicket(guid: "...", ticket: "...") { response in ... }
170 +// OR Cosmote user:
171 +WarplySDK.shared.getCosmoteUser(guid: "...") { response in ... }
172 +```
173 +
174 +### What Happens During `initialize()`
175 +1. Validates `appUuid` is not empty
176 +2. Sets `Configuration.baseURL` and `Configuration.host` from stored environment
177 +3. Stores `appUuid` in UserDefaults for NetworkService access
178 +4. Initializes SQLite database (`DatabaseManager.shared.initializeDatabase()`)
179 +5. Performs automatic device registration with comprehensive device info
180 +6. Posts Dynatrace analytics event on success/failure
181 +
182 +### Authentication Token Flow
183 +1. Login methods (`deiLogin`, `verifyTicket`, `getCosmoteUser`) receive JWT tokens from server
184 +2. Tokens are parsed into `TokenModel` (automatic JWT `exp` claim extraction)
185 +3. `TokenModel` stored in SQLite `requestVariables` table via `DatabaseManager`
186 +4. `NetworkService` reads tokens from database for authenticated requests
187 +5. Proactive refresh: tokens refreshed 5 minutes before expiration
188 +6. On 401 response: automatic token refresh + request retry
189 +7. Logout: tokens cleared from database
190 +
191 +### Environment Configuration
192 +| Environment | Base URL | Host |
193 +|-------------|----------|------|
194 +| `.development` | `https://engage-uat.dei.gr` | `engage-uat.dei.gr` |
195 +| `.production` | `https://engage-prod.dei.gr` | `engage-prod.dei.gr` |
196 +
197 +## 5. Core Components
198 +
199 +### WarplySDK (`Core/WarplySDK.swift`)
200 +
201 +The **sole public API surface** of the framework. All client interactions go through `WarplySDK.shared`.
202 +
203 +**Key Responsibilities:**
204 +- SDK configuration and initialization
205 +- All network API calls (campaigns, coupons, merchants, profile, cards, transactions)
206 +- In-memory state management (campaigns, coupons, merchants, coupon sets)
207 +- Event posting (dual: SwiftEventBus + EventDispatcher)
208 +- Campaign URL construction for WebView
209 +- UI presentation helpers (map, supermarket flow, dialogs)
210 +
211 +**Internal Architecture:**
212 +- Uses `NetworkService` for all HTTP calls
213 +- Uses `DatabaseManager` for token storage
214 +- Uses `SDKState` (private class) for in-memory data cache
215 +- Uses `UserDefaultsStore` (private class) for preferences
216 +- Uses `EventDispatcher` for modern event dispatching
217 +
218 +**State Management Pattern:**
219 +```swift
220 +// All state is stored in SDKState.shared (private)
221 +// Accessed via getter/setter methods on WarplySDK:
222 +WarplySDK.shared.setCampaignList(campaigns) // stores sorted by _sorting
223 +WarplySDK.shared.getCampaignList() // returns filtered (no ccms, no telco, no questionnaire)
224 +WarplySDK.shared.getAllCampaignList() // returns unfiltered
225 +WarplySDK.shared.setCouponList(coupons) // filters active (status==1), sorts by expiration
226 +WarplySDK.shared.getCouponList() // returns active coupons
227 +WarplySDK.shared.setOldCouponList(coupons) // filters redeemed (status==0), sorts by redeemed_date desc
228 +```
229 +
230 +**Campaign Processing Logic (Authenticated Users):**
231 +1. Fetch basic campaigns from `/api/mobile/v2/{appUUID}/context/`
232 +2. Fetch personalized campaigns from `/oauth/{appUUID}/context`
233 +3. Merge both arrays
234 +4. Fetch coupon availability
235 +5. Set `_coupon_availability` on each campaign matching its couponset
236 +6. Filter: remove campaigns with `_coupon_availability == 0`
237 +7. Separate carousel items (`_carousel == "true"`)
238 +8. Remove ccms offers, telco, and questionnaire campaigns
239 +9. Sort by `_sorting` field
240 +
241 +**Campaign Processing Logic (Unauthenticated Users):**
242 +- Only fetch basic campaigns — return ALL without coupon filtering
243 +
244 +## 6. Network Layer
245 +
246 +### NetworkService (`Network/NetworkService.swift`)
247 +
248 +URLSession-based HTTP client with automatic auth header injection, token refresh, and request/response logging.
249 +
250 +**Key Features:**
251 +- Dynamic `baseURL` from `Configuration.baseURL`
252 +- Network connectivity monitoring via `NWPathMonitor`
253 +- Automatic placeholder replacement in URLs and request bodies
254 +- Context response transformation (flattens `{"status":"1","context":{...}}` pattern)
255 +- Proactive token refresh before requests (5 min threshold)
256 +- Automatic 401 retry with token refresh
257 +- Comprehensive request/response logging with sensitive data masking
258 +
259 +### Endpoint System (`Network/Endpoints.swift`)
260 +
261 +All API routes are defined as cases of the `Endpoint` enum.
262 +
263 +**Endpoint Categories:**
264 +
265 +| Category | Base Path | Auth Type | Examples |
266 +|----------|-----------|-----------|----------|
267 +| `standardContext` | `/api/mobile/v2/{appUUID}/context/` | Standard (loyalty headers) | getCampaigns, getAvailableCoupons, getCouponSets, getMerchantCategories, getArticles |
268 +| `authenticatedContext` | `/oauth/{appUUID}/context` | Bearer Token | getCoupons, getProfile, getMarketPassDetails, addCard, getCards, deleteCard, getTransactionHistory, getPointsHistory, validateCoupon, redeemCoupon, getCampaignsPersonalized |
269 +| `userManagement` | `/user/{appUUID}/*` | Standard | register, changePassword, resetPassword, requestOtp |
270 +| `authentication` | `/oauth/{appUUID}/*` | Standard | refreshToken, logout |
271 +| `partnerCosmote` | `/partners/*` | Basic/Standard | verifyTicket, getCosmoteUser, deiLogin |
272 +| `session` | `/api/session/{sessionUuid}` | Standard (GET) | getSingleCampaign |
273 +| `analytics` | `/api/async/analytics/{appUUID}/` | Standard | sendEvent |
274 +| `deviceInfo` | `/api/async/info/{appUUID}/` | Standard | sendDeviceInfo |
275 +
276 +**Authentication Types:**
277 +
278 +| Type | Headers Added |
279 +|------|--------------|
280 +| `.standard` | `loyalty-web-id`, `loyalty-date`, `loyalty-signature` (SHA256 of apiKey+timestamp), platform headers, device headers |
281 +| `.bearerToken` | All standard headers + `Authorization: Bearer {access_token}` (from database) |
282 +| `.basicAuth` | All standard headers + `Authorization: Basic {encoded_credentials}` (for Cosmote partner endpoints) |
283 +
284 +**Headers Always Sent:**
285 +- `loyalty-web-id` — merchant ID from Configuration
286 +- `loyalty-date` — Unix timestamp
287 +- `loyalty-signature` — SHA256 hash of `"{apiKey}{timestamp}"`
288 +- `Accept-Encoding: gzip`
289 +- `Accept: application/json`
290 +- `User-Agent: gzip`
291 +- `channel: mobile`
292 +- `loyalty-bundle-id: ios:{bundleId}`
293 +- `unique-device-id: {IDFV}`
294 +- `vendor: apple`, `platform: ios`, `os_version: {version}`
295 +
296 +**Placeholder Replacement System:**
297 +- `{appUUID}` → App UUID from UserDefaults
298 +- `{access_token}` → Access token from database
299 +- `{refresh_token}` → Refresh token from database
300 +- `{web_id}` → Merchant ID from Configuration
301 +- `{api_key}` → Empty string (legacy, deprecated)
302 +- `{sessionUuid}` → Extracted from endpoint parameters
303 +
304 +**Context Response Transformation:**
305 +```
306 +Server returns: {"status": "1", "context": {"MAPP_CAMPAIGNING": {...}}}
307 +Transformed to: {"status": 1, "MAPP_CAMPAIGNING": {...}}
308 +```
309 +This flattening happens in `transformContextResponse()` for backward compatibility.
310 +
311 +**Request Parameter Structures:**
312 +Most POST endpoints wrap parameters in a domain-specific key:
313 +```swift
314 +// Campaigns: {"campaigns": {"action": "retrieve", "language": "el", "filters": {...}}}
315 +// Coupons: {"coupon": {"action": "user_coupons", "details": ["merchant","redemption"], ...}}
316 +// Profile: {"consumer_data": {"action": "handle_user_details", "process": "get"}}
317 +// Cards: {"cards": {"action": "add_card", "card_number": "...", ...}}
318 +// Merchants: {"shops": {"language": "el", "action": "retrieve_multilingual"}}
319 +// Articles: {"content": {"language": "el", "action": "retrieve_multilingual"}}
320 +```
321 +
322 +### TokenRefreshManager (`Network/TokenRefreshManager.swift`)
323 +
324 +Actor-based coordinator for JWT token refresh with retry and circuit breaker.
325 +
326 +**Retry Logic:**
327 +- Configurable `maxRetryAttempts` (default: 3) and `retryDelays` (default: [0.0, 1.0, 5.0])
328 +- 401/4xx errors: no retry (permanent failure)
329 +- 5xx/network errors: retry with backoff
330 +- After all retries fail: clears tokens from database
331 +
332 +**Circuit Breaker:**
333 +- States: `closed` (normal) → `open` (blocking) → `halfOpen` (testing recovery)
334 +- Opens after configurable `failureThreshold` (default: 5) consecutive failures
335 +- Recovery timeout: configurable (default: 300s / 5 minutes)
336 +
337 +**Request Deduplication:**
338 +- If a refresh is already in progress, subsequent callers await the same `Task`
339 +- Prevents multiple simultaneous refresh API calls
340 +
341 +## 7. Database Layer
342 +
343 +### DatabaseManager (`Database/DatabaseManager.swift`)
344 +
345 +SQLite.swift-based database manager using **raw SQL** (not Expression builders) for all operations.
346 +
347 +**Database File:** `{Documents}/WarplyCache_{bundleId}.db`
348 +
349 +**Tables (Schema Version 1):**
350 +
351 +| Table | Columns | Purpose |
352 +|-------|---------|---------|
353 +| `requestVariables` | `id` INTEGER PK, `client_id` TEXT, `client_secret` TEXT, `access_token` TEXT, `refresh_token` TEXT | JWT token storage (single row, UPSERT) |
354 +| `events` | `_id` INTEGER PK, `type` TEXT, `time` TEXT, `data` BLOB, `priority` INTEGER | Offline analytics event queue |
355 +| `pois` | `id` INTEGER PK, `lat` REAL, `lon` REAL, `radius` REAL | Geofencing points of interest |
356 +| `schema_version` | `id` INTEGER PK, `version` INTEGER UNIQUE, `created_at` TEXT | Migration tracking |
357 +
358 +**Schema Migration System:**
359 +- Version tracked in `schema_version` table
360 +- `currentDatabaseVersion = 1`, `supportedVersions = [1]`
361 +- Migration runs in a transaction for atomicity
362 +- Fresh installs: `createAllTables()` + set version
363 +- Upgrades: `migrateDatabase(from:to:)` with per-version migration functions
364 +
365 +**Token Management Methods:**
366 +```swift
367 +// Basic CRUD
368 +storeTokens(accessToken:refreshToken:clientId:clientSecret:) // UPSERT
369 +getAccessToken() async throws -> String?
370 +getRefreshToken() async throws -> String?
371 +getClientCredentials() async throws -> (clientId: String?, clientSecret: String?)
372 +clearTokens() async throws
373 +
374 +// TokenModel integration (preferred)
375 +storeTokenModel(_ tokenModel: TokenModel) async throws
376 +getTokenModel() async throws -> TokenModel?
377 +getValidTokenModel() async throws -> TokenModel? // nil if expired
378 +getTokenModelSync() throws -> TokenModel? // synchronous variant
379 +updateTokensAtomically(from:to:) async throws // race-condition safe
380 +
381 +// Encryption-aware (smart routing)
382 +storeTokenModelSmart(_ tokenModel: TokenModel) // auto-chooses encrypted/plain
383 +getTokenModelSmart() async throws -> TokenModel? // auto-chooses decrypted/plain
384 +
385 +// Performance
386 +getCachedTokenModel() async throws -> TokenModel? // 60-second memory cache
387 +```
388 +
389 +**Concurrency:** Uses `DispatchQueue(label: "com.warply.database", qos: .utility)` for thread safety.
390 +
391 +**Maintenance Methods:** `checkDatabaseIntegrity()`, `vacuumDatabase()`, `recreateDatabase()`, `getDatabaseStats()`
392 +
393 +## 8. Security Layer
394 +
395 +### FieldEncryption (`Security/FieldEncryption.swift`)
396 +
397 +Actor-based AES-256-GCM field-level encryption for sensitive token data.
398 +
399 +**Algorithm:** AES-256-GCM (via Apple CryptoKit)
400 +- Provides both encryption AND authentication (AEAD)
401 +- Output format: nonce + ciphertext + authentication tag (combined)
402 +- Keys: 256-bit (32 bytes) from iOS Keychain
403 +
404 +**Key Methods:**
405 +```swift
406 +// Low-level (provide your own key)
407 +encryptToken(_ token: String, using key: Data) throws -> Data
408 +decryptToken(_ encryptedData: Data, using key: Data) throws -> String
409 +
410 +// High-level (auto key from KeychainManager, with caching)
411 +encryptSensitiveData(_ data: String) async throws -> Data
412 +decryptSensitiveData(_ encryptedData: Data) async throws -> String
413 +
414 +// Batch operations (single key retrieval for multiple items)
415 +encryptSensitiveDataBatch(_ dataArray: [String]) async throws -> [Data]
416 +decryptSensitiveDataBatch(_ encryptedDataArray: [Data]) async throws -> [String]
417 +```
418 +
419 +**Key Caching:** Encryption key cached in memory for 300 seconds (5 min) to reduce Keychain lookups.
420 +
421 +**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").
422 +
423 +### KeychainManager (`Security/KeychainManager.swift`)
424 +
425 +Actor-based iOS Keychain wrapper for hardware-backed encryption key management.
426 +
427 +**Key Features:**
428 +- **Bundle ID isolation:** Each client app gets its own Keychain namespace via `com.warply.sdk.{bundleId}`
429 +- **Auto key generation:** `getOrCreateDatabaseKey()` creates 256-bit key on first call via `SecRandomCopyBytes`
430 +- **Security level:** `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` — key only available when device is unlocked, never backed up
431 +
432 +**Keychain Item Structure:**
433 +- Service: `com.warply.sdk.{bundleId}`
434 +- Account: `database_encryption_key`
435 +- Class: `kSecClassGenericPassword`
436 +- Value: 32 bytes (256-bit AES key)
437 +
438 +## 9. Event System
439 +
440 +### EventDispatcher (`Events/EventDispatcher.swift`)
441 +
442 +Modern Swift event system designed to eventually replace SwiftEventBus.
443 +
444 +**Architecture:**
445 +- Thread-safe using concurrent `DispatchQueue` with barrier writes
446 +- Handlers always dispatched on `DispatchQueue.main`
447 +- Subscription-based with auto-cleanup via `deinit`
448 +
449 +**Event Types (Protocol-based):**
450 +```swift
451 +public protocol WarplyEvent {
452 + var name: String { get }
453 + var timestamp: Date { get }
454 + var data: Any? { get }
455 +}
456 +```
457 +
458 +**Built-in Event Types:**
459 +
460 +| Event Struct | Event Name | Triggered When |
461 +|-------------|------------|----------------|
462 +| `CampaignsRetrievedEvent` | `"campaigns_retrieved"` | Campaigns fetched from server |
463 +| `CouponsRetrievedEvent` | `"coupons_fetched"` | Coupons fetched from server |
464 +| `MarketPassDetailsEvent` | `"market_pass_details_fetched"` | Market pass details received |
465 +| `CCMSRetrievedEvent` | `"ccms_retrieved"` | CCMS campaigns set |
466 +| `SeasonalsRetrievedEvent` | `"seasonals_retrieved"` | Seasonal list set |
467 +| `DynatraceEvent` | `"dynatrace"` | Analytics event |
468 +| `GenericWarplyEvent` | (any string) | Backward compatibility |
469 +
470 +**Dual Posting Pattern (CRITICAL):**
471 +All events in WarplySDK are posted to BOTH systems for backward compatibility:
472 +```swift
473 +// Internal helper method in WarplySDK:
474 +private func postFrameworkEvent(_ eventName: String, sender: Any? = nil) {
475 + SwiftEventBus.post(eventName, sender: sender) // Client compatibility
476 + eventDispatcher.post(eventName, sender: sender) // Modern internal
477 +}
478 +```
479 +
480 +**Client Subscription API:**
481 +```swift
482 +// String-based (backward compatible)
483 +WarplySDK.shared.subscribe(to: "campaigns_retrieved") { data in ... }
484 +
485 +// Type-safe (modern)
486 +WarplySDK.shared.subscribe(CampaignsRetrievedEvent.self) { event in ... }
487 +```
488 +
489 +**Dynatrace Analytics Pattern:**
490 +Every SDK API call posts a Dynatrace event on success AND failure:
491 +```swift
492 +let dynatraceEvent = LoyaltySDKDynatraceEventModel()
493 +dynatraceEvent._eventName = "custom_success_campaigns_loyalty" // or custom_error_*
494 +dynatraceEvent._parameters = nil
495 +self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
496 +```
497 +Event naming convention: `custom_{success|error}_{operation}_loyalty`
498 +
499 +## 10. Configuration System
500 +
501 +### WarplyConfiguration (`Configuration/WarplyConfiguration.swift`)
502 +
503 +Codable struct with nested sub-configurations for each subsystem.
504 +
505 +**Sub-configurations:**
506 +
507 +| Config Struct | Controls | Key Fields |
508 +|--------------|----------|------------|
509 +| `WarplyDatabaseConfig` | Encryption, WAL mode, cache | `encryptionEnabled`, `dataProtectionClass`, `enableWALMode`, `cacheSize`, `useKeychainForKeys` |
510 +| `WarplyTokenConfig` | Refresh retry, circuit breaker | `maxRetryAttempts`, `retryDelays`, `circuitBreakerThreshold`, `circuitBreakerResetTime`, `refreshThresholdMinutes` |
511 +| `WarplyNetworkConfig` | Timeouts, retries, caching | `requestTimeout`, `resourceTimeout`, `maxRetryAttempts`, `retryDelay`, `maxConcurrentRequests`, `enableResponseCaching`, `enableExponentialBackoff`, `allowsCellularAccess` |
512 +| `WarplyLoggingConfig` | Log levels, masking | `logLevel` (.none/.error/.warning/.info/.debug/.verbose), `enableDatabaseLogging`, `enableNetworkLogging`, `enableTokenLogging`, `maskSensitiveData`, `enableFileLogging` |
513 +
514 +**Preset Configurations:**
515 +
516 +| Preset | Encryption | Log Level | Timeout | Token Retries | Use Case |
517 +|--------|-----------|-----------|---------|---------------|----------|
518 +| `.development` | Off | `.verbose` | 15s | 2 | Local dev, full logging |
519 +| `.production` | On | `.warning` | 30s | 3 | Release builds |
520 +| `.testing` | Off | `.error` | 5s | 1 | Unit/integration tests |
521 +| `.highSecurity` | On | `.error` | 20s | 2 | Sensitive environments, WiFi only |
522 +
523 +**Usage:**
524 +```swift
525 +// Apply preset
526 +try await WarplySDK.shared.configure(WarplyConfiguration.production)
527 +
528 +// Custom config
529 +var config = WarplyConfiguration.development
530 +config.databaseConfig.encryptionEnabled = true
531 +config.tokenConfig.maxRetryAttempts = 5
532 +try await WarplySDK.shared.configure(config)
533 +
534 +// Per-component configuration
535 +try await WarplySDK.shared.configureDatabaseSecurity(dbConfig)
536 +try await WarplySDK.shared.configureTokenManagement(tokenConfig)
537 +try await WarplySDK.shared.configureLogging(loggingConfig)
538 +try await WarplySDK.shared.configureNetwork(networkConfig)
539 +```
540 +
541 +**Validation:** Each config struct has a `validate()` method that throws `ConfigurationError` with specific error types and recovery suggestions.
542 +
543 +**Serialization:** `WarplyConfiguration` is `Codable` — can be saved/loaded as JSON via `toJSONData()`, `fromJSONData()`, `saveToFile(at:)`, `loadFromFile(at:)`.
544 +
545 +## 11. Data Models
546 +
547 +All models use **dictionary-based initialization** (`init(dictionary: [String: Any])`) to parse JSON responses.
548 +
549 +### Key Model Classes
550 +
551 +| Model | File | Key Fields | Notes |
552 +|-------|------|------------|-------|
553 +| `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 |
554 +| `LoyaltyContextualOfferModel` | Campaign.swift | `_loyaltyCampaignId`, `_eligibleAssets`, `_offerName`, `_sessionId` | CCMS contextual offers |
555 +| `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 |
556 +| `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" |
557 +| `TokenModel` | TokenModel.swift | `accessToken`, `refreshToken`, `clientId`, `clientSecret`, `expirationDate`, `isExpired`, `shouldRefresh`, `isValid`, `canRefresh` | `struct`, JWT auto-parsing, 5-min proactive refresh window |
558 +| `MerchantModel` | Merchant.swift | Standard merchant fields | Location-aware merchant data |
559 +| `ProfileModel` | ProfileModel.swift | User profile fields | Consumer data |
560 +| `CardModel` | CardModel.swift | Card number, issuer, holder, expiration | Credit card data |
561 +| `TransactionModel` | TransactionModel.swift | `transactionDate`, transaction details | Sorted by date descending |
562 +| `PointsHistoryModel` | PointsHistoryModel.swift | `entryDate`, points entries | Sorted by date descending |
563 +| `MarketPassDetailsModel` | Market.swift | Supermarket pass details | Market/supermarket feature |
564 +| `RedeemedSMHistoryModel` | Coupon.swift | `_totalRedeemedValue`, `_redeemedCouponList` | Supermarket coupon redemption history |
565 +| `ArticleModel` | ArticleModel.swift | Content/article fields | Carousel content |
566 +| `MerchantCategoryModel` | MerchantCategoryModel.swift | Category fields | Merchant categorization |
567 +| `VerifyTicketResponseModel` | Response.swift | `getStatus` (1=success) | Generic API response wrapper |
568 +| `GenericResponseModel` | Response.swift | Generic response fields | Flexible response wrapper |
569 +| `LoyaltySDKDynatraceEventModel` | Models.swift | `_eventName`, `_parameters` | Dynatrace analytics event payload |
570 +| `LoyaltyGiftsForYouPackage` | Models.swift/Gifts.swift | Seasonal gift data | Seasonal promotions |
571 +
572 +### Model Property Accessor Pattern
573 +
574 +Models use a **underscore-prefixed computed property** pattern for public access:
575 +```swift
576 +// Private stored property
577 +private var category_title: String?
578 +
579 +// Public accessor (getter + setter)
580 +public var _category_title: String? {
581 + get { return self.category_title }
582 + set(newValue) { self.category_title = newValue }
583 +}
584 +```
585 +
586 +Some newer models use **read-only** accessors:
587 +```swift
588 +public var _uuid: String { get { return self.uuid ?? "" } }
589 +```
590 +
591 +### Date Handling in Models
592 +- **CampaignItemModel:** Raw strings from server, formatted via `formattedStartDate(format:)` / `formattedEndDate(format:)`
593 +- **CouponSetItemModel:** Expiration as "yyyy-MM-dd HH:mm", formatted via `formattedExpiration(format:)`, `_expiration_formatted` (dd/MM/yyyy)
594 +- **CouponItemModel:** Dates converted during init to "dd/MM/yyyy" display format, `redeemed_date` as `Date` object
595 +- Input formats tried: `"yyyy-MM-dd HH:mm:ssZZZZZ"`, `"yyyy-MM-dd HH:mm:ss"`, `"yyyy-MM-dd HH:mm:ss.SSSSSS"`
596 +
597 +### URL Cleaning
598 +Campaign model has a static helper that cleans escaped URLs:
599 +```swift
600 +private static func cleanEscapedUrl(_ url: String?) -> String? {
601 + return url?.replacingOccurrences(of: "\\/", with: "/")
602 +}
603 +```
604 +Applied to: `index_url`, `logo_url`, `banner_img`, `coupon_img`, `campaign_url`, `banner_img_mobile`
605 +
606 +## 12. UI Layer
607 +
608 +### Pre-built Screens
609 +
610 +All screens use **XIB-based** layouts (not SwiftUI) with `Bundle.frameworkBundle` for resource loading.
611 +
612 +| Screen | Storyboard ID | Purpose |
613 +|--------|--------------|---------|
614 +| `MyRewardsViewController` | — (XIB) | Main rewards dashboard with campaigns, offers, banners |
615 +| `ProfileViewController` | — (XIB) | User profile with coupons, filters, questionnaires |
616 +| `CouponViewController` | — (XIB) | Single coupon detail view |
617 +| `CouponsetViewController` | — (XIB) | Coupon set detail view |
618 +| `CampaignViewController` | `"CampaignViewController"` (Storyboard) | WebView for campaign URLs |
619 +
620 +### CampaignViewController (WebView)
621 +Used to display campaign web content. Configured via:
622 +```swift
623 +let vc = storyboard.instantiateViewController(withIdentifier: "CampaignViewController") as! CampaignViewController
624 +vc.campaignUrl = url // Campaign web URL
625 +vc.params = params // JSON string with tokens and config
626 +vc.showHeader = false // Header visibility
627 +vc.isPresented = true/false // Modal vs push navigation
628 +```
629 +
630 +### Navigation Pattern
631 +Screens support both push (navigation controller) and modal presentation:
632 +```swift
633 +if let navigationController = controller.navigationController {
634 + vc.isPresented = false
635 + navigationController.pushViewController(vc, animated: true)
636 +} else {
637 + vc.isPresented = true
638 + vc.modalPresentationStyle = .fullScreen
639 + controller.present(vc, animated: true, completion: nil)
640 +}
641 +```
642 +
643 +### Cell Architecture
644 +Table/Collection view cells are XIB-based, each in its own directory:
645 +- `MyRewardsBannerOfferCollectionViewCell` — Banner offer in collection view
646 +- `MyRewardsOfferCollectionViewCell` — Standard offer in collection view
647 +- `MyRewardsBannerOffersScrollTableViewCell` — Horizontal scrolling banner offers
648 +- `MyRewardsOffersScrollTableViewCell` — Horizontal scrolling offers
649 +- `MyRewardsProfileInfoTableViewCell` — Profile info header
650 +- `ProfileCouponTableViewCell` — Coupon row in profile
651 +- `ProfileCouponFiltersTableViewCell` — Filter chips for coupons
652 +- `ProfileFilterCollectionViewCell` — Individual filter chip
653 +- `ProfileHeaderTableViewCell` — Profile section header
654 +- `ProfileQuestionnaireTableViewCell` — Questionnaire entry
655 +
656 +### Fonts
657 +Custom fonts bundled: **PingLCG** family (Bold, Light, Regular) in `.otf` format.
658 +
659 +### Bundle Resource Loading
660 +Framework resources loaded via `Bundle.frameworkBundle` (defined in `XIBLoader.swift`/`MyEmptyClass.swift`), which resolves the correct bundle whether using CocoaPods resource bundles or SPM.
661 +
662 +## 13. API Patterns & Conventions
663 +
664 +### Dual API Surface (Callback + Async/Await)
665 +
666 +Every public API method has two variants:
667 +
668 +```swift
669 +// Callback-based (original pattern)
670 +public func getCoupons(language: String, completion: @escaping ([CouponItemModel]?) -> Void, failureCallback: @escaping (Int) -> Void)
671 +
672 +// Async/await wrapper (bridges to callback version)
673 +public func getCoupons(language: String) async throws -> [CouponItemModel] {
674 + return try await withCheckedThrowingContinuation { continuation in
675 + getCoupons(language: language, completion: { coupons in
676 + if let coupons = coupons {
677 + continuation.resume(returning: coupons)
678 + } else {
679 + continuation.resume(throwing: WarplyError.networkError)
680 + }
681 + }, failureCallback: { errorCode in
682 + continuation.resume(throwing: WarplyError.unknownError(errorCode))
683 + })
684 + }
685 +}
686 +```
687 +
688 +### Standard Method Body Pattern
689 +
690 +Every API method follows this exact pattern:
691 +1. Create `Task { }` block
692 +2. Call `networkService.requestRaw(endpoint)` or specific convenience method
693 +3. Parse response inside `await MainActor.run { }`
694 +4. Check status (`response["status"] as? Int == 1` or `response["MAPP_*-status"] as? Int == 1`)
695 +5. Post Dynatrace success/error event
696 +6. Call completion handler on main thread
697 +
698 +### Response Status Checking
699 +
700 +Different response structures have different status locations:
701 +```swift
702 +// Standard context responses (after transformation):
703 +response["status"] as? Int == 1
704 +response["MAPP_CAMPAIGNING-status"] as? Int == 1
705 +response["MAPP_COUPON-status"] as? Int == 1
706 +response["MAPP_SHOPS-status"] as? Int == 1
707 +
708 +// VerifyTicketResponseModel:
709 +tempResponse.getStatus == 1
710 +```
711 +
712 +### Language Handling Pattern
713 +
714 +Methods with optional language default to `applicationLocale`:
715 +```swift
716 +public func getCoupons(language: String? = nil, ...) {
717 + let finalLanguage = language ?? self.applicationLocale
718 + // use finalLanguage
719 +}
720 +```
721 +
722 +### Complete Public API Reference
723 +
724 +**Authentication:**
725 +- `configure(appUuid:merchantId:environment:language:)`
726 +- `initialize(callback:)` / `initialize() async throws`
727 +- `deiLogin(email:completion:failureCallback:)` / `deiLogin(email:) async throws`
728 +- `verifyTicket(guid:ticket:completion:)`
729 +- `getCosmoteUser(guid:completion:)` / `getCosmoteUser(guid:) async throws`
730 +- `logout(completion:)`
731 +
732 +**Campaigns:**
733 +- `getCampaigns(language:filters:completion:failureCallback:)` / `getCampaigns(language:filters:) async throws`
734 +- `getCampaignsPersonalized(language:filters:completion:failureCallback:)` / async variant
735 +- `getSingleCampaign(sessionUuid:completion:)` / async variant
736 +- `getSupermarketCampaign(language:completion:)` / async variant
737 +
738 +**Coupons:**
739 +- `getCoupons(language:completion:failureCallback:)` / async variant
740 +- `getCouponsUniversal(language:completion:failureCallback:)`
741 +- `getCouponSets(language:completion:failureCallback:)` / async variant
742 +- `getAvailableCoupons(completion:)` / async variant
743 +- `validateCoupon(_:completion:)` / async variant
744 +- `redeemCoupon(productId:productUuid:merchantId:completion:)` / async variant
745 +
746 +**User Management:**
747 +- `changePassword(oldPassword:newPassword:completion:)` / async variant
748 +- `resetPassword(email:completion:)` / async variant
749 +- `requestOtp(phoneNumber:completion:)` / async variant
750 +- `getProfile(completion:failureCallback:)` / async variant
751 +
752 +**Cards:**
753 +- `addCard(cardNumber:cardIssuer:cardHolder:expirationMonth:expirationYear:completion:)` / async variant
754 +- `getCards(completion:)` / async variant
755 +- `deleteCard(token:completion:)` / async variant
756 +
757 +**Transactions:**
758 +- `getTransactionHistory(productDetail:completion:)` / async variant
759 +- `getPointsHistory(completion:)` / async variant
760 +
761 +**Merchants:**
762 +- `getMerchants(language:categories:defaultShown:center:tags:uuid:distance:parentUuids:completion:failureCallback:)` / async variant
763 +- `getMerchantCategories(language:completion:failureCallback:)` / async variant
764 +
765 +**Content:**
766 +- `getArticles(language:categories:completion:failureCallback:)` / async variant
767 +
768 +**Market/Supermarket:**
769 +- `getMarketPassDetails(completion:failureCallback:)` / async variant
770 +- `getRedeemedSMHistory(language:completion:failureCallback:)` / async variant
771 +- `openSupermarketsMap(_:)` — presents map WebView
772 +- `openSuperMarketsFlow(_:)` — presents supermarket campaign
773 +
774 +**State:**
775 +- `setCampaignList(_:)` / `getCampaignList()` / `getAllCampaignList()`
776 +- `setCouponList(_:)` / `getCouponList()`
777 +- `setOldCouponList(_:)` / `getOldCouponList()`
778 +- `setCouponSetList(_:)` / `getCouponSetList()`
779 +- `setMerchantList(_:)` / `getMerchantList()`
780 +- `setCarouselList(_:)` / `getCarouselList()`
781 +- `setSupermarketCampaign(_:)` / `getSupermarketCampaign()`
782 +- `setMarketPassDetails(_:)` / `getMarketPassDetails()`
783 +- `setCCMSLoyaltyCampaigns(campaigns:)` / `getCCMSLoyaltyCampaigns()`
784 +- `setSeasonalList(_:)` / `getSeasonalList()`
785 +
786 +**Utilities:**
787 +- `constructCampaignUrl(_:)` / `constructCampaignParams(_:)` / `constructCampaignParams(campaign:isMap:)`
788 +- `constructCcmsUrl(_:)`
789 +- `showDialog(_:_:_:)`
790 +- `getMarketPassMapUrl()`
791 +- `getNetworkStatus()` — returns 1 (connected) or 0 (disconnected)
792 +- `updateRefreshToken(accessToken:refreshToken:)`
793 +- `updateDeviceToken(_:)`
794 +
795 +## 14. Coding Standards & Conventions
796 +
797 +### Naming Conventions
798 +- **Files:** PascalCase matching the primary type (`CampaignItemModel``Campaign.swift`)
799 +- **Classes/Structs:** PascalCase (`CampaignItemModel`, `NetworkService`)
800 +- **Private properties:** snake_case for model fields (`category_title`, `banner_img`)
801 +- **Public accessors:** underscore prefix (`_category_title`, `_banner_img`)
802 +- **Constants:** PascalCase for enum cases, camelCase for static properties
803 +- **Endpoint enum cases:** camelCase (`getCampaigns`, `verifyTicket`)
804 +
805 +### Logging Convention
806 +Emoji-prefixed print statements throughout the codebase:
807 +```
808 +✅ Success operations
809 +❌ Errors/failures
810 +⚠️ Warnings
811 +🔴 Critical errors
812 +🟡 Token should refresh
813 +🟢 Token valid
814 +🔐 Token/auth operations
815 +🔒 Encryption operations
816 +🔓 Decryption operations
817 +🔑 Key operations
818 +🗄️ Database operations
819 +🗑️ Deletion/clearing
820 +📊 Analytics/stats
821 +📤 Request logging
822 +📥 Response logging
823 +🔗 URL operations
824 +🔄 Refresh/retry/update
825 +🚨 Circuit breaker
826 +🧪 Test operations
827 +💡 Suggestions
828 +⏱️ Timing
829 +🔧 Configuration
830 +🏭 Production
831 +🚦 Request queue
832 +⚛️ Atomic operations
833 +```
834 +
835 +### Access Control
836 +- **`public`:** All types, methods, and properties intended for client use
837 +- **`public final class`:** WarplySDK, NetworkService (prevent subclassing)
838 +- **`private`/`internal`:** Implementation details, state management
839 +- **`actor`:** All security-critical components (TokenRefreshManager, FieldEncryption, KeychainManager)
840 +
841 +### Error Handling Convention
842 +- Errors are logged immediately at the point of occurrence
843 +- Every error posts a Dynatrace analytics event
844 +- Callback methods return `nil` on failure, async methods `throw`
845 +- `handleError()` private method centralizes error conversion and logging
846 +
847 +### UserDefaults Keys
848 +- `appUuidUD` — App UUID
849 +- `merchantIdUD` — Merchant ID
850 +- `languageUD` — Language
851 +- `isDarkModeEnabledUD` — Dark mode flag
852 +- `environmentUD` — "development" or "production"
853 +- `trackersEnabled` — Analytics tracking flag
854 +- `device_token` — Push notification device token
855 +
856 +### Configuration Static Properties
857 +```swift
858 +Configuration.baseURL // Current base URL
859 +Configuration.host // Current host
860 +Configuration.errorDomain // Error domain (same as host)
861 +Configuration.merchantId // Merchant ID
862 +Configuration.language // "el" or "en"
863 +Configuration.verifyURL // Verification URL
864 +```
865 +
866 +## 15. Dependencies & Distribution
867 +
868 +### External Dependencies
869 +
870 +| Dependency | Version | Purpose |
871 +|-----------|---------|---------|
872 +| `SQLite.swift` | `0.12.2` (exact) | SQLite database access (raw SQL, not ORM) |
873 +| `SwiftEventBus` | `5.0.0+` | Event bus for client-facing events (backward compatibility) |
874 +| `RSBarcodes_Swift` | `5.2.0+` | Barcode generation for coupon display |
875 +
876 +### Distribution Methods
877 +
878 +**CocoaPods** (`SwiftWarplyFramework.podspec`):
879 +```ruby
880 +pod 'SwiftWarplyFramework', '~> 2.3.0'
881 +```
882 +- Resource bundles: `ResourcesBundle` containing `.xcassets` and `.otf` fonts
883 +- Source files: `SwiftWarplyFramework/SwiftWarplyFramework/**/*.{h,m,swift,xib,storyboard}`
884 +- Excludes: build artifacts, xcodeproj files
885 +
886 +**Swift Package Manager** (`Package.swift`):
887 +```swift
888 +.package(url: "https://git.warp.ly/open-source/warply_sdk_framework.git", from: "2.3.0")
889 +```
890 +- Target path: `SwiftWarplyFramework/SwiftWarplyFramework`
891 +- Excludes: `Helpers/WarplyReactMethods.h`, `Helpers/WarplyReactMethods.m`, `Info.plist`
892 +- Resources: `.process()` for xcassets, fonts, storyboards, XIBs
893 +
894 +### System Frameworks Used
895 +- `Foundation` — Core types
896 +- `UIKit` — UI components
897 +- `Network``NWPathMonitor` for connectivity
898 +- `CryptoKit` — AES-256-GCM encryption
899 +- `Security` — Keychain Services
900 +- `CommonCrypto` — SHA256 hashing for loyalty-signature header
901 +
902 +## 16. Common Tasks & Recipes
903 +
904 +### Adding a New API Endpoint
905 +
906 +1. **Add endpoint case** in `Endpoints.swift`:
907 + ```swift
908 + case myNewEndpoint(param1: String, param2: Int)
909 + ```
910 +
911 +2. **Add path** in the `path` computed property:
912 + ```swift
913 + case .myNewEndpoint:
914 + return "/oauth/{appUUID}/context" // or appropriate path
915 + ```
916 +
917 +3. **Add method** (most are POST):
918 + ```swift
919 + case .myNewEndpoint:
920 + return .POST
921 + ```
922 +
923 +4. **Add parameters** with correct wrapper key:
924 + ```swift
925 + case .myNewEndpoint(let param1, let param2):
926 + return ["consumer_data": ["action": "my_action", "param1": param1, "param2": param2]]
927 + ```
928 +
929 +5. **Set auth type** in `authType`:
930 + ```swift
931 + case .myNewEndpoint:
932 + return .bearerToken // or .standard
933 + ```
934 +
935 +6. **Set category** in `category`:
936 + ```swift
937 + case .myNewEndpoint:
938 + return .authenticatedContext
939 + ```
940 +
941 +7. **Add method** in `WarplySDK.swift` (both callback + async variants):
942 + ```swift
943 + // Callback version
944 + public func myNewMethod(param1: String, param2: Int, completion: @escaping (MyModel?) -> Void, failureCallback: @escaping (Int) -> Void) {
945 + Task {
946 + do {
947 + let endpoint = Endpoint.myNewEndpoint(param1: param1, param2: param2)
948 + let response = try await networkService.requestRaw(endpoint)
949 + await MainActor.run {
950 + if response["status"] as? Int == 1 {
951 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
952 + dynatraceEvent._eventName = "custom_success_my_new_method_loyalty"
953 + dynatraceEvent._parameters = nil
954 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
955 + // Parse and return result
956 + completion(parsedResult)
957 + } else {
958 + let dynatraceEvent = LoyaltySDKDynatraceEventModel()
959 + dynatraceEvent._eventName = "custom_error_my_new_method_loyalty"
960 + dynatraceEvent._parameters = nil
961 + self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
962 + failureCallback(-1)
963 + }
964 + }
965 + } catch {
966 + await MainActor.run {
967 + self.handleError(error, context: "myNewMethod", endpoint: "myNewEndpoint", failureCallback: failureCallback)
968 + }
969 + }
970 + }
971 + }
972 +
973 + // Async variant
974 + public func myNewMethod(param1: String, param2: Int) async throws -> MyModel {
975 + return try await withCheckedThrowingContinuation { continuation in
976 + myNewMethod(param1: param1, param2: param2, completion: { result in
977 + if let result = result { continuation.resume(returning: result) }
978 + else { continuation.resume(throwing: WarplyError.networkError) }
979 + }, failureCallback: { errorCode in
980 + continuation.resume(throwing: WarplyError.unknownError(errorCode))
981 + })
982 + }
983 + }
984 + ```
985 +
986 +### Adding a New Data Model
987 +
988 +1. Create file in `models/` directory
989 +2. Use `class` (not struct) for consistency with existing models
990 +3. Implement `init(dictionary: [String: Any])` for JSON parsing
991 +4. Use `_fieldName` accessor pattern for public properties
992 +5. Handle optional fields with `?? ""` or `?? 0` defaults
993 +6. Clean URLs with `cleanEscapedUrl()` if applicable
994 +
995 +### Adding a New Database Table
996 +
997 +1. Increment `currentDatabaseVersion` in `DatabaseManager.swift`
998 +2. Add new version to `supportedVersions` array
999 +3. Create migration function `performMigrationToV{N}()`
1000 +4. Add case to `performMigration(to:)` switch
1001 +5. Create table using raw SQL (`try db.execute(...)`)
1002 +
1003 +### Adding a New Event Type
1004 +
1005 +1. Create struct conforming to `WarplyEvent` in `EventDispatcher.swift`
1006 +2. Add convenience method in `EventDispatcher` extension
1007 +3. Add `InternalFrameworkEvent` case in `WarplySDK.swift` if it's internal
1008 +4. Post using `postFrameworkEvent()` or `postInternalFrameworkEvent()`
1009 +5. **Always post to both SwiftEventBus and EventDispatcher**
1010 +
1011 +### Adding a New Configuration Option
1012 +
1013 +1. Add property to appropriate config struct (`WarplyDatabaseConfig`, etc.)
1014 +2. Add validation in `validate()` method
1015 +3. Add to `getSummary()` return dictionary
1016 +4. Add to the preset configurations (`.development`, `.production`, etc.)
1017 +5. Ensure Codable compliance (add to CodingKeys if needed)
1018 +
1019 +## 17. Error Handling
1020 +
1021 +### Error Types
1022 +
1023 +**`WarplyError`** (public, SDK-level):
1024 +| Case | Code | Description |
1025 +|------|------|-------------|
1026 +| `.networkError` | -1000 | Generic network failure |
1027 +| `.invalidResponse` | -1001 | Invalid/unparseable response |
1028 +| `.authenticationFailed` | 401 | Auth failure |
1029 +| `.dataParsingError` | -1002 | JSON parsing failure |
1030 +| `.serverError(Int)` | (code) | HTTP server error |
1031 +| `.noInternetConnection` | -1009 | No network connectivity |
1032 +| `.requestTimeout` | -1001 | Request timed out |
1033 +| `.unknownError(Int)` | (code) | Catch-all |
1034 +
1035 +**`NetworkError`** (public, network-level):
1036 +| Case | Code | Description |
1037 +|------|------|-------------|
1038 +| `.invalidURL` | -1001 | Bad URL construction |
1039 +| `.noData` | -1002 | Empty response body |
1040 +| `.decodingError(Error)` | -1003 | JSON decode failure |
1041 +| `.serverError(Int)` | (code) | HTTP 4xx/5xx |
1042 +| `.networkError(Error)` | (varies) | URLSession error |
1043 +| `.authenticationRequired` | 401 | Token needed/expired |
1044 +| `.invalidResponse` | -1004 | Non-JSON response |
1045 +
1046 +**`DatabaseError`** (internal):
1047 +- `.connectionNotAvailable` DB not initialized
1048 +- `.tableCreationFailed` Schema creation error
1049 +- `.queryFailed(String)` SQL execution error
1050 +- `.migrationFailed(String)` Version migration error
1051 +- `.corruptedDatabase` Integrity check failed
1052 +
1053 +**`TokenRefreshError`** (public):
1054 +- `.noTokensAvailable` No tokens in DB
1055 +- `.invalidRefreshToken` Bad refresh token
1056 +- `.networkError(Error)` Network failure during refresh
1057 +- `.serverError(Int)` Server error during refresh
1058 +- `.maxRetriesExceeded` All retry attempts failed
1059 +- `.refreshInProgress` Concurrent refresh attempt
1060 +- `.invalidResponse` Bad refresh response
1061 +
1062 +**`EncryptionError`** (internal):
1063 +- `.invalidKey` (4001), `.encryptionFailed` (4002), `.decryptionFailed` (4003), `.invalidData` (4004), `.keyGenerationFailed` (4005)
1064 +
1065 +**`ConfigurationError`** (public):
1066 +- `.invalidRefreshThreshold`, `.invalidRetryAttempts`, `.retryDelaysMismatch`, `.invalidTimeout`, `.invalidCacheSize`, `.invalidCircuitBreakerThreshold`, etc.
1067 +- All include `errorDescription` and `recoverySuggestion`
1068 +
1069 +### Error Flow
1070 +1. Network/DB error occurs caught in `catch` block
1071 +2. `handleError()` called converts to `WarplyError` via `convertNetworkError()`
1072 +3. Error logged with context via `logError()`
1073 +4. Dynatrace error event posted via `postErrorAnalytics()`
1074 +5. `failureCallback(errorCode)` called on main thread
1075 +
1076 +## 18. Testing
1077 +
1078 +### Mock Support
1079 +
1080 +**`MockNetworkService`** (available in `#if DEBUG`):
1081 +```swift
1082 +let mockService = MockNetworkService()
1083 +mockService.setMockResponse(["status": 1, "result": [...]], for: .getCampaigns(...))
1084 +mockService.setShouldFail(true, with: .networkError(...))
1085 +```
1086 +
1087 +### Test Configuration
1088 +Use `WarplyConfiguration.testing` preset:
1089 +- Encryption disabled for simpler test setup
1090 +- 5-second request timeout for fast tests
1091 +- Single retry attempt
1092 +- 0.1s retry delay
1093 +- Analytics and auto-registration disabled
1094 +
1095 +### SQLite Test
1096 +```swift
1097 +let result = await WarplySDK.shared.testSQLiteConnection()
1098 +// Returns true if SQLite.swift is working correctly
1099 +```
1100 +
1101 +### Database Diagnostics
1102 +```swift
1103 +// Check integrity
1104 +let isIntact = try await DatabaseManager.shared.checkDatabaseIntegrity()
1105 +
1106 +// Get stats
1107 +let stats = try await DatabaseManager.shared.getDatabaseStats()
1108 +// Returns (tokensCount: Int, eventsCount: Int, poisCount: Int)
1109 +
1110 +// Get version info
1111 +let versionInfo = try await DatabaseManager.shared.getDatabaseVersionInfo()
1112 +
1113 +// Token validation
1114 +let validationResult = try await DatabaseManager.shared.validateStoredTokens()
1115 +
1116 +// Token status
1117 +let status = try await DatabaseManager.shared.getTokenStatus()
1118 +```
1119 +
1120 +### Configuration Diagnostics
1121 +```swift
1122 +let summary = WarplySDK.shared.getConfigurationSummary()
1123 +let config = WarplySDK.shared.getCurrentConfiguration()
1124 +```
1125 +
1126 +### Encryption Diagnostics
1127 +```swift
1128 +let encryptionStats = try await DatabaseManager.shared.getEncryptionStats()
1129 +let isEncryptionWorking = await DatabaseManager.shared.validateEncryptionSetup()
1130 +let keychainDiag = await KeychainManager.shared.getDiagnosticInfo()
1131 +```
1132 +
1133 +---
1134 +
1135 +## Quick Reference Card
1136 +
1137 +| What | Where | Key |
1138 +|------|-------|-----|
1139 +| All public APIs | `Core/WarplySDK.swift` | `WarplySDK.shared` |
1140 +| API routes | `Network/Endpoints.swift` | `Endpoint` enum |
1141 +| HTTP client | `Network/NetworkService.swift` | `NetworkService.shared` |
1142 +| Token refresh | `Network/TokenRefreshManager.swift` | `TokenRefreshManager.shared` |
1143 +| Token storage | `Database/DatabaseManager.swift` | `DatabaseManager.shared` |
1144 +| Token model | `models/TokenModel.swift` | `TokenModel` struct |
1145 +| Encryption | `Security/FieldEncryption.swift` | `FieldEncryption.shared` |
1146 +| Keychain | `Security/KeychainManager.swift` | `KeychainManager.shared` |
1147 +| Events | `Events/EventDispatcher.swift` | `EventDispatcher.shared` |
1148 +| Config | `Configuration/WarplyConfiguration.swift` | `WarplyConfiguration` struct |
1149 +| Campaigns | `models/Campaign.swift` | `CampaignItemModel` class |
1150 +| Coupons | `models/Coupon.swift` | `CouponItemModel` class |
1151 +
1152 +**Critical Rules for AI Agents:**
1153 +1. **Always maintain dual event posting** (SwiftEventBus + EventDispatcher)
1154 +2. **Always post Dynatrace analytics** on both success and failure paths
1155 +3. **Always provide both callback AND async/await** variants for public methods
1156 +4. **Always dispatch completion callbacks on main thread** via `MainActor.run`
1157 +5. **Use raw SQL** in DatabaseManager (not SQLite.swift Expression builders)
1158 +6. **Never log sensitive data** (tokens, passwords, card numbers) — use masking
1159 +7. **All security components must be actors** (not classes)
1160 +8. **Models use `class` not `struct`** with `_fieldName` accessor pattern
1161 +9. **Token storage is in database only** — never in UserDefaults
1162 +10. **All response parsing must handle the context transformation** pattern