skill.md 54.7 KB

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.4.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.4.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()

  1. Validates appUuid is not empty
  2. Sets Configuration.baseURL and Configuration.host from stored environment
  3. Stores appUuid in UserDefaults for NetworkService access
  4. Initializes SQLite database (DatabaseManager.shared.initializeDatabase())
  5. Performs automatic device registration with comprehensive device info
  6. Posts Dynatrace analytics event on success/failure

Authentication Token Flow

  1. Login methods (deiLogin, verifyTicket, getCosmoteUser) receive JWT tokens from server
  2. Tokens are parsed into TokenModel (automatic JWT exp claim extraction)
  3. TokenModel stored in SQLite requestVariables table via DatabaseManager
  4. NetworkService reads tokens from database for authenticated requests
  5. Proactive refresh: tokens refreshed 5 minutes before expiration
  6. On 401 response: automatic token refresh + request retry
  7. Logout: tokens cleared from database

Environment Configuration

Environment Base URL Host
.development https://engage-uat.dei.gr engage-uat.dei.gr
.production https://engage-prod.dei.gr engage-prod.dei.gr

5. Core Components

WarplySDK (Core/WarplySDK.swift)

The sole public API surface of the framework. All client interactions go through WarplySDK.shared.

Key Responsibilities:

  • SDK configuration and initialization
  • All network API calls (campaigns, coupons, merchants, profile, cards, transactions)
  • In-memory state management (campaigns, coupons, merchants, coupon sets)
  • Event posting (dual: SwiftEventBus + EventDispatcher)
  • Campaign URL construction for WebView
  • UI presentation helpers (map, supermarket flow, dialogs)

Internal Architecture:

  • Uses NetworkService for all HTTP calls
  • Uses DatabaseManager for token storage
  • Uses SDKState (private class) for in-memory data cache
  • Uses UserDefaultsStore (private class) for preferences
  • Uses EventDispatcher for modern event dispatching

State Management Pattern:

// All state is stored in SDKState.shared (private)
// Accessed via getter/setter methods on WarplySDK:
WarplySDK.shared.setCampaignList(campaigns)    // stores sorted by _sorting
WarplySDK.shared.getCampaignList()             // returns filtered (no ccms, no telco, no questionnaire)
WarplySDK.shared.getAllCampaignList()           // returns unfiltered
WarplySDK.shared.setCouponList(coupons)        // filters active (status==1), sorts by expiration
WarplySDK.shared.getCouponList()               // returns active coupons
WarplySDK.shared.setOldCouponList(coupons)     // filters redeemed (status==0), sorts by redeemed_date desc

Campaign Processing Logic (Authenticated Users):

  1. Fetch basic campaigns from /api/mobile/v2/{appUUID}/context/
  2. Fetch personalized campaigns from /oauth/{appUUID}/context
  3. Merge both arrays
  4. Fetch coupon availability
  5. Set _coupon_availability on each campaign matching its couponset
  6. Filter: remove campaigns with _coupon_availability == 0
  7. Separate carousel items (_carousel == "true")
  8. Remove ccms offers, telco, and questionnaire campaigns
  9. Sort by _sorting field

Campaign Processing Logic (Unauthenticated Users):

  • Only fetch basic campaigns — return ALL without coupon filtering

6. Network Layer

NetworkService (Network/NetworkService.swift)

URLSession-based HTTP client with automatic auth header injection, token refresh, and request/response logging.

Key Features:

  • Dynamic baseURL from Configuration.baseURL
  • Network connectivity monitoring via NWPathMonitor
  • Automatic placeholder replacement in URLs and request bodies
  • Context response transformation (flattens {"status":"1","context":{...}} pattern)
  • Proactive token refresh before requests (5 min threshold)
  • Automatic 401 retry with token refresh
  • Comprehensive request/response logging with sensitive data masking

Endpoint System (Network/Endpoints.swift)

All API routes are defined as cases of the Endpoint enum.

Endpoint Categories:

Category Base Path Auth Type Examples
standardContext /api/mobile/v2/{appUUID}/context/ Standard (loyalty headers) getCampaigns, getAvailableCoupons, getCouponSets, getMerchantCategories, getArticles
authenticatedContext /oauth/{appUUID}/context Bearer Token getCoupons, getProfile, getMarketPassDetails, addCard, getCards, deleteCard, getTransactionHistory, getPointsHistory, validateCoupon, redeemCoupon, getCampaignsPersonalized
userManagement /user/{appUUID}/* Standard register, changePassword, resetPassword, requestOtp
authentication /oauth/{appUUID}/* Standard refreshToken, logout
partnerCosmote /partners/* Basic/Standard verifyTicket, getCosmoteUser, deiLogin
session /api/session/{sessionUuid} Standard (GET) getSingleCampaign
analytics /api/async/analytics/{appUUID}/ Standard sendEvent
deviceInfo /api/async/info/{appUUID}/ Standard sendDeviceInfo

Authentication Types:

Type Headers Added
.standard loyalty-web-id, loyalty-date, loyalty-signature (SHA256 of apiKey+timestamp), platform headers, device headers
.bearerToken All standard headers + Authorization: Bearer {access_token} (from database)
.basicAuth All standard headers + Authorization: Basic {encoded_credentials} (for Cosmote partner endpoints)

Headers Always Sent:

  • loyalty-web-id — merchant ID from Configuration
  • loyalty-date — Unix timestamp
  • loyalty-signature — SHA256 hash of "{apiKey}{timestamp}"
  • Accept-Encoding: gzip
  • Accept: application/json
  • User-Agent: gzip
  • channel: mobile
  • loyalty-bundle-id: ios:{bundleId}
  • unique-device-id: {IDFV}
  • vendor: apple, platform: ios, os_version: {version}

Placeholder Replacement System:

  • {appUUID} → App UUID from UserDefaults
  • {access_token} → Access token from database
  • {refresh_token} → Refresh token from database
  • {web_id} → Merchant ID from Configuration
  • {api_key} → Empty string (legacy, deprecated)
  • {sessionUuid} → Extracted from endpoint parameters

Context Response Transformation:

Server returns: {"status": "1", "context": {"MAPP_CAMPAIGNING": {...}}}
Transformed to: {"status": 1, "MAPP_CAMPAIGNING": {...}}

This flattening happens in transformContextResponse() for backward compatibility.

Request Parameter Structures: Most POST endpoints wrap parameters in a domain-specific key:

// Campaigns: {"campaigns": {"action": "retrieve", "language": "el", "filters": {...}}}
// Coupons:   {"coupon": {"action": "user_coupons", "details": ["merchant","redemption"], ...}}
// Profile:   {"consumer_data": {"action": "handle_user_details", "process": "get"}}
// Cards:     {"cards": {"action": "add_card", "card_number": "...", ...}}
// Merchants: {"shops": {"language": "el", "action": "retrieve_multilingual"}}
// Articles:  {"content": {"language": "el", "action": "retrieve_multilingual"}}

TokenRefreshManager (Network/TokenRefreshManager.swift)

Actor-based coordinator for JWT token refresh with retry and circuit breaker.

Retry Logic:

  • Configurable maxRetryAttempts (default: 3) and retryDelays (default: [0.0, 1.0, 5.0])
  • 401/4xx errors: no retry (permanent failure)
  • 5xx/network errors: retry with backoff
  • After all retries fail: clears tokens from database

Circuit Breaker:

  • States: closed (normal) → open (blocking) → halfOpen (testing recovery)
  • Opens after configurable failureThreshold (default: 5) consecutive failures
  • Recovery timeout: configurable (default: 300s / 5 minutes)

Request Deduplication:

  • If a refresh is already in progress, subsequent callers await the same Task
  • Prevents multiple simultaneous refresh API calls

7. Database Layer

DatabaseManager (Database/DatabaseManager.swift)

SQLite.swift-based database manager using raw SQL (not Expression builders) for all operations.

Database File: {Documents}/WarplyCache_{bundleId}.db

Tables (Schema Version 1):

Table Columns Purpose
requestVariables id INTEGER PK, client_id TEXT, client_secret TEXT, access_token TEXT, refresh_token TEXT JWT token storage (single row, UPSERT)
events _id INTEGER PK, type TEXT, time TEXT, data BLOB, priority INTEGER Offline analytics event queue
pois id INTEGER PK, lat REAL, lon REAL, radius REAL Geofencing points of interest
schema_version id INTEGER PK, version INTEGER UNIQUE, created_at TEXT Migration tracking

Schema Migration System:

  • Version tracked in schema_version table
  • currentDatabaseVersion = 1, supportedVersions = [1]
  • Migration runs in a transaction for atomicity
  • Fresh installs: createAllTables() + set version
  • Upgrades: migrateDatabase(from:to:) with per-version migration functions

Token Management Methods:

// 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 via SecRandomCopyBytes
  • Security level: kSecAttrAccessibleWhenUnlockedThisDeviceOnly — key only available when device is unlocked, never backed up

Keychain Item Structure:

  • Service: com.warply.sdk.{bundleId}
  • Account: database_encryption_key
  • Class: kSecClassGenericPassword
  • Value: 32 bytes (256-bit AES key)

9. Event System

EventDispatcher (Events/EventDispatcher.swift)

Modern Swift event system designed to eventually replace SwiftEventBus.

Architecture:

  • Thread-safe using concurrent DispatchQueue with barrier writes
  • Handlers always dispatched on DispatchQueue.main
  • Subscription-based with auto-cleanup via deinit

Event Types (Protocol-based):

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

9.5 Push Notification System

Architecture (Option A: Host App Forwards)

The framework does NOT set itself as UNUserNotificationCenterDelegate. Instead, the host app owns the delegate and forwards calls to the SDK. This avoids conflicts with Firebase, OneSignal, etc.

Push Notification Types

Type Location Purpose
WarplyApplicationState WarplySDK.swift Enum: .active, .background, .closed
WarplyPushHandler WarplySDK.swift Protocol for custom push handling (action != 0)
WarplyNotificationPayload WarplySDK.swift Parsed push payload (action, sessionUuid, message, aps, customData)
WarplyAPSItem WarplySDK.swift Parsed APS dictionary (alert, badge, sound, category)
PushNotificationReceivedEvent EventDispatcher.swift Event posted when push received
PushNotificationEngagedEvent EventDispatcher.swift Event posted when user interacts with push

Push Notification Detection

Warply push notifications are identified by the "_a" key in the payload:

{"_a": 0, "session_uuid": "abc-123", "aps": {"alert": "New offer!", "badge": 1}}
  • "_a" == 0: Default handling — framework presents CampaignViewController WebView
  • "_a" != 0: Custom action — forwarded to pushHandlerDelegate

Push Notification Methods

Method Purpose
updateDeviceToken(_:) Store token + send comprehensive device info to server
checkForLoyaltySDKNotification(_:) Detect Warply push by "_a" key, auto-handle if found
handleNotification(_:appState:) Route notification by action and app state
handleLaunchOptions(_:) Check launch options for push (cold start)
handlePendingNotification() Process stored notification from cold start
showPushCampaign(sessionUuid:) Present campaign WebView for a session UUID
resetBadge() Reset app icon badge to 0
sendDeviceInfo() Force send device info to server
sendDeviceInfoIfNeeded() Smart send — only if info changed
buildDeviceInfo() Build comprehensive device info dictionary

Push Analytics

Event Name When Sent Equivalent ObjC
NB_PushReceived When Warply push is received logUserReceivedPush:
NB_PushAck When user engages with push logUserEngagedPush:

Device Info Payload

Sent to /api/async/info/{appUUID}/ via Endpoint.sendDeviceInfo. Contains:

  • device_token, platform ("ios"), vendor ("apple"), os_version
  • unique_device_id (IDFV), ios_system_name, ios_system_version, ios_model
  • ios_locale, ios_languages, screen_resolution
  • bundle_identifier, app_version, app_build, sdk_version
  • development flag (DEBUG vs release)

Rich Push Presentation

For action == 0 pushes, the framework presents CampaignViewController internally:

  • URL: {baseURL}/api/session/{session_uuid}
  • Active state: Shows alert first, then WebView on user confirmation
  • Background state: Presents WebView immediately
  • Closed state: Stores as pending, shows via handlePendingNotification()

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_date as Date object
  • Input formats tried: "yyyy-MM-dd HH:mm:ssZZZZZ", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss.SSSSSS"

URL Cleaning

Campaign model has a static helper that cleans escaped URLs:

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:

  1. Create Task { } block
  2. Call networkService.requestRaw(endpoint) or specific convenience method
  3. Parse response inside await MainActor.run { }
  4. Check status (response["status"] as? Int == 1 or response["MAPP_*-status"] as? Int == 1)
  5. Post Dynatrace success/error event
  6. Call completion handler on main thread

Response Status Checking

Different response structures have different status locations:

// 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 (CampaignItemModelCampaign.swift)
  • Classes/Structs: PascalCase (CampaignItemModel, NetworkService)
  • Private properties: snake_case for model fields (category_title, banner_img)
  • Public accessors: underscore prefix (_category_title, _banner_img)
  • Constants: PascalCase for enum cases, camelCase for static properties
  • Endpoint enum cases: camelCase (getCampaigns, verifyTicket)

Logging Convention

Emoji-prefixed print statements throughout the codebase:

✅ Success operations
❌ Errors/failures
⚠️ Warnings
🔴 Critical errors
🟡 Token should refresh
🟢 Token valid
🔐 Token/auth operations
🔒 Encryption operations
🔓 Decryption operations
🔑 Key operations
🗄️ Database operations
🗑️ Deletion/clearing
📊 Analytics/stats
📤 Request logging
📥 Response logging
🔗 URL operations
🔄 Refresh/retry/update
🚨 Circuit breaker
🧪 Test operations
💡 Suggestions
⏱️ Timing
🔧 Configuration
🏭 Production
🚦 Request queue
⚛️ Atomic operations

Access Control

  • public: All types, methods, and properties intended for client use
  • public final class: WarplySDK, NetworkService (prevent subclassing)
  • private/internal: Implementation details, state management
  • actor: All security-critical components (TokenRefreshManager, FieldEncryption, KeychainManager)

Error Handling Convention

  • Errors are logged immediately at the point of occurrence
  • Every error posts a Dynatrace analytics event
  • Callback methods return nil on failure, async methods throw
  • handleError() private method centralizes error conversion and logging

UserDefaults Keys

  • appUuidUD — App UUID
  • merchantIdUD — Merchant ID
  • languageUD — Language
  • isDarkModeEnabledUD — Dark mode flag
  • environmentUD — "development" or "production"
  • trackersEnabled — Analytics tracking flag
  • device_token — Push notification device token

Configuration Static Properties

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.4.0'
  • Resource bundles: ResourcesBundle containing .xcassets and .otf fonts
  • Source files: SwiftWarplyFramework/SwiftWarplyFramework/**/*.{h,m,swift,xib,storyboard}
  • Excludes: build artifacts, xcodeproj files

Swift Package Manager (Package.swift):

.package(url: "https://git.warp.ly/open-source/warply_sdk_framework.git", from: "2.4.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
  • NetworkNWPathMonitor for connectivity
  • CryptoKit — AES-256-GCM encryption
  • Security — Keychain Services
  • CommonCrypto — SHA256 hashing for loyalty-signature header

16. Common Tasks & Recipes

Adding a New API Endpoint

  1. Add endpoint case in Endpoints.swift:

    case myNewEndpoint(param1: String, param2: Int)
    
  2. Add path in the path computed property:

    case .myNewEndpoint:
       return "/oauth/{appUUID}/context"  // or appropriate path
    
  3. Add method (most are POST):

    case .myNewEndpoint:
       return .POST
    
  4. Add parameters with correct wrapper key:

    case .myNewEndpoint(let param1, let param2):
       return ["consumer_data": ["action": "my_action", "param1": param1, "param2": param2]]
    
  5. Set auth type in authType:

    case .myNewEndpoint:
       return .bearerToken  // or .standard
    
  6. Set category in category:

    case .myNewEndpoint:
       return .authenticatedContext
    
  7. 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:

  1. Always maintain dual event posting (SwiftEventBus + EventDispatcher)
  2. Always post Dynatrace analytics on both success and failure paths
  3. Always provide both callback AND async/await variants for public methods
  4. Always dispatch completion callbacks on main thread via MainActor.run
  5. Use raw SQL in DatabaseManager (not SQLite.swift Expression builders)
  6. Never log sensitive data (tokens, passwords, card numbers) — use masking
  7. All security components must be actors (not classes)
  8. Models use class not struct with _fieldName accessor pattern
  9. Token storage is in database only — never in UserDefaults
  10. All response parsing must handle the context transformation pattern