Manos Chorianopoulos

Dynamic BaseURL Management, Language Configuration Support, complete HTTP header management

......@@ -129,7 +129,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
WarplySDK.shared.configure(
appUuid: "YOUR_APP_UUID", // Replace with your App Uuid
merchantId: "YOUR_MERCHANT_ID", // Replace with your Merchant ID
environment: .production // Use .development for testing
environment: .production, // Use .development for testing
language: "el" // Language: "en" or "el" (default: "el")
)
return true
......@@ -213,7 +214,7 @@ class ViewController: UIViewController {
func testWarplySDK() {
// Simple completion handler approach
WarplySDK.shared.getCampaigns(language: "en") { campaigns in
WarplySDK.shared.getCampaigns(language: "el") { campaigns in
DispatchQueue.main.async {
if let campaigns = campaigns {
print("🎉 Success! Retrieved \(campaigns.count) campaigns")
......@@ -234,7 +235,7 @@ For modern Swift development:
func testWarplySDKAsync() {
Task {
do {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "el")
print("🎉 Success! Retrieved \(campaigns.count) campaigns")
// Update UI on main thread
......@@ -688,11 +689,32 @@ WarplySDK.shared.configure(
)
```
### Language Configuration
```swift
// Configure language during SDK setup (recommended)
WarplySDK.shared.configure(
appUuid: "your-app-uuid",
merchantId: "your-merchant-id",
environment: .production,
language: "el" // "en" for English, "el" for Greek (default)
)
// Change language at runtime
WarplySDK.shared.applicationLocale = "el" // or "el"
// Language affects:
// - API responses and content localization
// - Campaign and coupon descriptions
// - Error messages and UI text
// - Date and number formatting
```
### SDK Properties
```swift
// Language setting (default: "el")
WarplySDK.shared.applicationLocale = "en" // or "el"
WarplySDK.shared.applicationLocale = "el" // or "en"
// Dark mode support
WarplySDK.shared.isDarkModeEnabled = true
......@@ -775,7 +797,7 @@ WarplySDK.shared.updateRefreshToken(
```swift
// Basic campaign retrieval
WarplySDK.shared.getCampaigns(language: "en") { campaigns in
WarplySDK.shared.getCampaigns(language: "el") { campaigns in
guard let campaigns = campaigns else { return }
for campaign in campaigns {
......@@ -794,7 +816,7 @@ let filters: [String: Any] = [
]
]
WarplySDK.shared.getCampaigns(language: "en", filters: filters) { campaigns in
WarplySDK.shared.getCampaigns(language: "el", filters: filters) { campaigns in
// Handle filtered campaigns
}
```
......@@ -820,7 +842,7 @@ Task {
### Get Supermarket Campaign
```swift
WarplySDK.shared.getSupermarketCampaign(language: "en") { campaign in
WarplySDK.shared.getSupermarketCampaign(language: "el") { campaign in
if let campaign = campaign {
print("Supermarket campaign available: \(campaign.title ?? "")")
// Show supermarket offers
......@@ -869,7 +891,7 @@ let carouselCampaigns = WarplySDK.shared.getCarouselList()
```swift
// Completion handler approach
WarplySDK.shared.getCoupons(language: "en", completion: { coupons in
WarplySDK.shared.getCoupons(language: "el", completion: { coupons in
guard let coupons = coupons else { return }
for coupon in coupons {
......@@ -885,7 +907,7 @@ WarplySDK.shared.getCoupons(language: "en", completion: { coupons in
// Async/await approach
Task {
do {
let coupons = try await WarplySDK.shared.getCoupons(language: "en")
let coupons = try await WarplySDK.shared.getCoupons(language: "el")
// Filter active coupons
let activeCoupons = coupons.filter { $0.status == 1 }
......@@ -984,7 +1006,7 @@ Task {
```swift
Task {
do {
let history = try await WarplySDK.shared.getRedeemedSMHistory(language: "en")
let history = try await WarplySDK.shared.getRedeemedSMHistory(language: "el")
print("Total redeemed value: \(history._totalRedeemedValue)")
print("Redeemed coupons: \(history._redeemedCouponList.count)")
......@@ -1153,7 +1175,7 @@ public enum WarplyError: Error {
// Async/await error handling
Task {
do {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "el")
// Success
} catch let error as WarplyError {
switch error {
......@@ -1174,7 +1196,7 @@ Task {
}
// Completion handler error handling
WarplySDK.shared.getCoupons(language: "en", completion: { coupons in
WarplySDK.shared.getCoupons(language: "el", completion: { coupons in
if let coupons = coupons {
// Success
} else {
......@@ -1240,7 +1262,7 @@ let subscription = WarplySDK.shared.subscribe(CampaignsRetrievedEvent.self) { ev
**Before:**
```swift
WarplySDK.shared.getCampaigns(language: "en") { campaigns in
WarplySDK.shared.getCampaigns(language: "el") { campaigns in
if let campaigns = campaigns {
// Handle success
} else {
......@@ -1253,7 +1275,7 @@ WarplySDK.shared.getCampaigns(language: "en") { campaigns in
```swift
Task {
do {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "el")
// Handle success
} catch {
// Handle error with proper error types
......@@ -1296,7 +1318,7 @@ override func viewDidLoad() {
// ✅ Good: Comprehensive error handling
Task {
do {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "el")
updateUI(with: campaigns)
} catch let error as WarplyError {
handleWarplyError(error)
......@@ -1307,7 +1329,7 @@ Task {
// ❌ Bad: Ignoring errors
Task {
let campaigns = try? await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try? await WarplySDK.shared.getCampaigns(language: "el")
// Silently fails
}
```
......@@ -1347,7 +1369,7 @@ class BadViewController: UIViewController {
```swift
// ✅ Good: Main thread UI updates
Task {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "el")
await MainActor.run {
self.updateCampaignsUI(campaigns)
......@@ -1355,7 +1377,7 @@ Task {
}
// ✅ Also good: Using completion handlers
WarplySDK.shared.getCampaigns(language: "en") { campaigns in
WarplySDK.shared.getCampaigns(language: "el") { campaigns in
DispatchQueue.main.async {
self.updateCampaignsUI(campaigns)
}
......@@ -1376,7 +1398,7 @@ func loadCampaigns() {
// Then, fetch fresh data
Task {
do {
let freshCampaigns = try await WarplySDK.shared.getCampaigns(language: "en")
let freshCampaigns = try await WarplySDK.shared.getCampaigns(language: "el")
updateUI(with: freshCampaigns)
} catch {
// Handle error, keep cached data
......@@ -1427,7 +1449,7 @@ if networkStatus != 1 {
}
// Check language parameter
WarplySDK.shared.getCampaigns(language: "en") // Try different language
WarplySDK.shared.getCampaigns(language: "el") // Try different language
```
#### 3. Events Not Firing
......@@ -1457,7 +1479,7 @@ SwiftEventBus.onMainThread(self, name: "campaigns_retrieved") { result in
**Solutions:**
```swift
// Use weak references in closures
WarplySDK.shared.getCampaigns(language: "en") { [weak self] campaigns in
WarplySDK.shared.getCampaigns(language: "el") { [weak self] campaigns in
self?.updateUI(campaigns)
}
......@@ -1486,8 +1508,8 @@ print("Network Status: \(WarplySDK.shared.getNetworkStatus())")
```swift
// Batch API calls when possible
Task {
async let campaigns = WarplySDK.shared.getCampaigns(language: "en")
async let coupons = WarplySDK.shared.getCoupons(language: "en")
async let campaigns = WarplySDK.shared.getCampaigns(language: "el")
async let coupons = WarplySDK.shared.getCoupons(language: "el")
async let marketPass = WarplySDK.shared.getMarketPassDetails()
do {
......@@ -1548,4 +1570,32 @@ Start with the Quick Start guide and gradually adopt the advanced features as ne
---
## 📋 **Changelog**
### **Version 2.2.10** - *June 23, 2025*
#### **🆕 New Features**
- **Language Configuration Support**: Added configurable language parameter to SDK initialization
- New `language` parameter in `configure()` method (defaults to "el")
- Runtime language switching via `applicationLocale` property
- Automatic configuration sync when language changes
#### **🔧 Network Improvements**
- **Comprehensive Header System**: Implemented complete HTTP header management based on original Objective-C implementation
- Core loyalty headers: `loyalty-web-id`, `loyalty-date`, `loyalty-signature`
- Device identification headers: `unique-device-id`, `vendor`, `platform`, `os_version`
- App identification headers: `loyalty-bundle-id`, `manufacturer`, `ios_device_model`
- Authentication headers with proper Bearer token handling
- Special endpoint headers for registration, and logout flows
- **Dynamic BaseURL Management**: Enhanced baseURL handling for improved configuration flexibility
- Dynamic baseURL reading from Configuration on every request
- Environment-aware URL switching (development/production)
- Real-time configuration updates without restart
- Fallback safety mechanisms with default stage URL
- **SHA256 Signature Generation**: Added secure signature generation for API authentication
- **Device Info Utilities**: Enhanced device information collection for headers
- **Platform-Specific Headers**: iOS-specific headers for better backend compatibility
---
**Happy Coding! 🚀**
......
......@@ -135,14 +135,16 @@ public final class WarplySDK {
// MARK: - Configuration
/// Configure the SDK with app uuid and merchant ID
public func configure(appUuid: String, merchantId: String, environment: Configuration.Environment = .production) {
public func configure(appUuid: String, merchantId: String, environment: Configuration.Environment = .production, language: String = "el") {
Configuration.baseURL = environment.baseURL
Configuration.host = environment.host
Configuration.errorDomain = environment.host
Configuration.merchantId = merchantId
Configuration.language = language
storage.appUuid = appUuid
storage.merchantId = merchantId
storage.applicationLocale = language
}
/// Set environment (development/production)
......@@ -196,7 +198,6 @@ public final class WarplySDK {
set {
let tempLang = (newValue == "EN" || newValue == "en") ? "en" : "el"
storage.applicationLocale = tempLang
// Language setting now handled by pure Swift configuration
Configuration.language = tempLang
}
}
......
......@@ -8,6 +8,46 @@
import Foundation
import Network
import UIKit
import CommonCrypto
// MARK: - String Extensions for SHA256
extension String {
func sha256() -> String {
let data = Data(self.utf8)
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes { bytes in
_ = CC_SHA256(bytes.bindMemory(to: UInt8.self).baseAddress, CC_LONG(data.count), &hash)
}
return hash.map { String(format: "%02x", $0) }.joined()
}
}
// MARK: - Device Info Utilities
extension UIDevice {
var modelName: String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value))!)
}
return identifier
}
var bundleIdentifier: String {
return Bundle.main.bundleIdentifier ?? ""
}
var appVersion: String {
return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
}
}
// MARK: - Network Service Protocol
......@@ -25,7 +65,10 @@ public final class NetworkService: NetworkServiceProtocol {
// MARK: - Properties
private let session: URLSession
private let baseURL: String
private var baseURL: String {
// Dynamic baseURL that always reads from Configuration
return Configuration.baseURL.isEmpty ? "https://engage-stage.warp.ly" : Configuration.baseURL
}
private var accessToken: String?
private var refreshToken: String?
private let networkMonitor: NWPathMonitor
......@@ -34,9 +77,7 @@ public final class NetworkService: NetworkServiceProtocol {
// MARK: - Initialization
public init(baseURL: String = Configuration.baseURL) {
self.baseURL = baseURL
public init() {
// Configure URLSession
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30.0
......@@ -168,21 +209,16 @@ public final class NetworkService: NetworkServiceProtocol {
var request = URLRequest(url: url)
request.httpMethod = endpoint.method.rawValue
request.timeoutInterval = 30.0
// Add headers
// Add comprehensive headers based on original Objective-C implementation
addWarplyHeaders(to: &request, endpoint: endpoint)
// Add endpoint-specific headers
for (key, value) in endpoint.headers {
request.setValue(value, forHTTPHeaderField: key)
}
// Add authentication if required
if endpoint.requiresAuthentication, let accessToken = accessToken {
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
// Add app-specific headers
request.setValue(Configuration.merchantId, forHTTPHeaderField: "X-Merchant-ID")
request.setValue(Configuration.language, forHTTPHeaderField: "X-Language")
// Add parameters
if let parameters = endpoint.parameters {
switch endpoint.method {
......@@ -210,6 +246,99 @@ public final class NetworkService: NetworkServiceProtocol {
return request
}
/// Add comprehensive Warply headers based on original Objective-C implementation
private func addWarplyHeaders(to request: inout URLRequest, endpoint: Endpoint) {
// Core headers (always sent)
let timestamp = Int(Date().timeIntervalSince1970)
// Loyalty headers - core authentication
request.setValue(Configuration.merchantId, forHTTPHeaderField: "loyalty-web-id")
request.setValue("\(timestamp)", forHTTPHeaderField: "loyalty-date")
// Generate loyalty signature (apiKey + timestamp SHA256)
// TODO: Get apiKey from secure storage or configuration
let apiKey = getApiKey()
if !apiKey.isEmpty {
let signatureString = "\(apiKey)\(timestamp)"
let signature = signatureString.sha256()
request.setValue(signature, forHTTPHeaderField: "loyalty-signature")
}
// Standard HTTP headers
request.setValue("gzip", forHTTPHeaderField: "Accept-Encoding")
request.setValue("application/json", forHTTPHeaderField: "Accept")
request.setValue("gzip", forHTTPHeaderField: "User-Agent")
// App identification headers
let bundleId = UIDevice.current.bundleIdentifier
if !bundleId.isEmpty {
request.setValue("ios:\(bundleId)", forHTTPHeaderField: "loyalty-bundle-id")
}
// Device identification
if let deviceId = UIDevice.current.identifierForVendor?.uuidString {
request.setValue(deviceId, forHTTPHeaderField: "unique-device-id")
}
// Platform headers
request.setValue("apple", forHTTPHeaderField: "vendor")
request.setValue("ios", forHTTPHeaderField: "platform")
request.setValue(UIDevice.current.systemVersion, forHTTPHeaderField: "os_version")
request.setValue("mobile", forHTTPHeaderField: "channel")
// Device info headers (if trackers enabled)
if UserDefaults.standard.bool(forKey: "trackersEnabled") {
request.setValue("Apple", forHTTPHeaderField: "manufacturer")
request.setValue(UIDevice.current.modelName, forHTTPHeaderField: "ios_device_model")
let appVersion = UIDevice.current.appVersion
if !appVersion.isEmpty {
request.setValue(appVersion, forHTTPHeaderField: "app_version")
}
}
// Authentication headers
if endpoint.requiresAuthentication {
if let accessToken = accessToken {
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
}
}
// Special headers for specific endpoints
addSpecialHeaders(to: &request, endpoint: endpoint)
}
/// Add special headers for specific endpoint types
private func addSpecialHeaders(to request: inout URLRequest, endpoint: Endpoint) {
// Handle Cosmote-specific endpoints
if endpoint.path.contains("/partners/cosmote/") || endpoint.path.contains("/partners/oauth/") {
// Basic auth for Cosmote endpoints (from original implementation)
let basicAuth = "MVBQNFhCQzhFYTJBaUdCNkJWZGFGUERlTTNLQ3kzMjU6YzViMzAyZDY5N2FiNGY3NzhiNThhMTg0YzBkZWRmNGU="
request.setValue("Basic \(basicAuth)", forHTTPHeaderField: "Authorization")
}
// Handle logout endpoints
if endpoint.path.contains("/logout") {
// Logout endpoints may need special token handling
// The tokens are included in the request body, not headers
}
// Handle registration endpoints
if endpoint.path.contains("/register") {
// Registration endpoints don't need authentication headers
request.setValue(nil, forHTTPHeaderField: "Authorization")
}
}
/// Get API key from secure storage or configuration
private func getApiKey() -> String {
// TODO: Implement secure API key retrieval
// This should come from keychain or secure configuration
// For now, return empty string - this needs to be implemented
// based on how the original Objective-C code stored the API key
return ""
}
private func validateResponse(_ response: URLResponse) throws {
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
......