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)
// 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()
- Validates
appUuidis not empty - Sets
Configuration.baseURLandConfiguration.hostfrom stored environment - Stores
appUuidin UserDefaults for NetworkService access - Initializes SQLite database (
DatabaseManager.shared.initializeDatabase()) - Performs automatic device registration with comprehensive device info
- Posts Dynatrace analytics event on success/failure
Authentication Token Flow
- Login methods (
deiLogin,verifyTicket,getCosmoteUser) receive JWT tokens from server - Tokens are parsed into
TokenModel(automatic JWTexpclaim extraction) -
TokenModelstored in SQLiterequestVariablestable viaDatabaseManager -
NetworkServicereads tokens from database for authenticated requests - Proactive refresh: tokens refreshed 5 minutes before expiration
- On 401 response: automatic token refresh + request retry
- 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
NetworkServicefor all HTTP calls - Uses
DatabaseManagerfor token storage - Uses
SDKState(private class) for in-memory data cache - Uses
UserDefaultsStore(private class) for preferences - Uses
EventDispatcherfor modern event dispatching
State Management Pattern:
// 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):
- Fetch basic campaigns from
/api/mobile/v2/{appUUID}/context/ - Fetch personalized campaigns from
/oauth/{appUUID}/context - Merge both arrays
- Fetch coupon availability
- Set
_coupon_availabilityon each campaign matching its couponset - Filter: remove campaigns with
_coupon_availability == 0 - Separate carousel items (
_carousel == "true") - Remove ccms offers, telco, and questionnaire campaigns
- Sort by
_sortingfield
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
baseURLfromConfiguration.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: gzipAccept: application/jsonUser-Agent: gzipchannel: mobileloyalty-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:
// 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) andretryDelays(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_versiontable -
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:
// 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:
// 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 viaSecRandomCopyBytes -
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
DispatchQueuewith barrier writes - Handlers always dispatched on
DispatchQueue.main - Subscription-based with auto-cleanup via
deinit
Event Types (Protocol-based):
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:
// 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:
// 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:
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:
// 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:
// 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:
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_dateasDateobject - 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:
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:
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:
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:
// 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:
- Create
Task { }block - Call
networkService.requestRaw(endpoint)or specific convenience method - Parse response inside
await MainActor.run { } - Check status (
response["status"] as? Int == 1orresponse["MAPP_*-status"] as? Int == 1) - Post Dynatrace success/error event
- Call completion handler on main thread
Response Status Checking
Different response structures have different status locations:
// 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:
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
nilon failure, async methodsthrow -
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
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):
pod 'SwiftWarplyFramework', '~> 2.3.0'
- Resource bundles:
ResourcesBundlecontaining.xcassetsand.otffonts - Source files:
SwiftWarplyFramework/SwiftWarplyFramework/**/*.{h,m,swift,xib,storyboard} - Excludes: build artifacts, xcodeproj files
Swift Package Manager (Package.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—NWPathMonitorfor 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
-
Add endpoint case in
Endpoints.swift:case myNewEndpoint(param1: String, param2: Int) -
Add path in the
pathcomputed property:case .myNewEndpoint: return "/oauth/{appUUID}/context" // or appropriate path -
Add method (most are POST):
case .myNewEndpoint: return .POST -
Add parameters with correct wrapper key:
case .myNewEndpoint(let param1, let param2): return ["consumer_data": ["action": "my_action", "param1": param1, "param2": param2]] -
Set auth type in
authType:case .myNewEndpoint: return .bearerToken // or .standard -
Set category in
category:case .myNewEndpoint: return .authenticatedContext -
Add method in
WarplySDK.swift(both callback + async variants):// 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
let result = await WarplySDK.shared.testSQLiteConnection()
// Returns true if SQLite.swift is working correctly
Database Diagnostics
// 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
let summary = WarplySDK.shared.getConfigurationSummary()
let config = WarplySDK.shared.getCurrentConfiguration()
Encryption Diagnostics
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:
- Always maintain dual event posting (SwiftEventBus + EventDispatcher)
- Always post Dynatrace analytics on both success and failure paths
- Always provide both callback AND async/await variants for public methods
-
Always dispatch completion callbacks on main thread via
MainActor.run - Use raw SQL in DatabaseManager (not SQLite.swift Expression builders)
- Never log sensitive data (tokens, passwords, card numbers) — use masking
- All security components must be actors (not classes)
-
Models use
classnotstructwith_fieldNameaccessor pattern - Token storage is in database only — never in UserDefaults
- All response parsing must handle the context transformation pattern