Toggle navigation
Toggle navigation
This project
Loading...
Sign in
open-source
/
warply_sdk_framework
Go to a project
Toggle navigation
Toggle navigation pinning
Projects
Groups
Snippets
Help
Project
Activity
Repository
Graphs
Network
Create a new issue
Commits
Issue Boards
Authored by
Manos Chorianopoulos
2025-06-23 13:05:52 +0300
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
ca49526402f8f30575a85ca3f1f354e765d7b1a7
ca495264
1 parent
96984394
Dynamic BaseURL Management, Language Configuration Support, complete HTTP header management
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
219 additions
and
39 deletions
CLIENT_DOCUMENTATION.md
SwiftWarplyFramework/SwiftWarplyFramework/Core/WarplySDK.swift
SwiftWarplyFramework/SwiftWarplyFramework/Network/NetworkService.swift
CLIENT_DOCUMENTATION.md
View file @
ca49526
...
...
@@ -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: "e
n
") { campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
") { 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: "e
n
")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
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 = "e
n" // or "el
"
WarplySDK.shared.applicationLocale = "e
l" // or "en
"
// Dark mode support
WarplySDK.shared.isDarkModeEnabled = true
...
...
@@ -775,7 +797,7 @@ WarplySDK.shared.updateRefreshToken(
```
swift
// Basic campaign retrieval
WarplySDK.shared.getCampaigns(language: "e
n
") { campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
") { campaigns in
guard let campaigns = campaigns else { return }
for campaign in campaigns {
...
...
@@ -794,7 +816,7 @@ let filters: [String: Any] = [
]
]
WarplySDK.shared.getCampaigns(language: "e
n
", filters: filters) { campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
", filters: filters) { campaigns in
// Handle filtered campaigns
}
```
...
...
@@ -820,7 +842,7 @@ Task {
### Get Supermarket Campaign
```
swift
WarplySDK.shared.getSupermarketCampaign(language: "e
n
") { campaign in
WarplySDK.shared.getSupermarketCampaign(language: "e
l
") { 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: "e
n
", completion: { coupons in
WarplySDK.shared.getCoupons(language: "e
l
", 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: "e
n
")
let coupons = try await WarplySDK.shared.getCoupons(language: "e
l
")
// 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: "e
n
")
let history = try await WarplySDK.shared.getRedeemedSMHistory(language: "e
l
")
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: "e
n
")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
// Success
} catch let error as WarplyError {
switch error {
...
...
@@ -1174,7 +1196,7 @@ Task {
}
// Completion handler error handling
WarplySDK.shared.getCoupons(language: "e
n
", completion: { coupons in
WarplySDK.shared.getCoupons(language: "e
l
", 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: "e
n
") { campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
") { 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: "e
n
")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
// 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: "e
n
")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
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: "e
n
")
let campaigns = try? await WarplySDK.shared.getCampaigns(language: "e
l
")
// Silently fails
}
```
...
...
@@ -1347,7 +1369,7 @@ class BadViewController: UIViewController {
```
swift
// ✅ Good: Main thread UI updates
Task {
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
n
")
let campaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
await MainActor.run {
self.updateCampaignsUI(campaigns)
...
...
@@ -1355,7 +1377,7 @@ Task {
}
// ✅ Also good: Using completion handlers
WarplySDK.shared.getCampaigns(language: "e
n
") { campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
") { 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: "e
n
")
let freshCampaigns = try await WarplySDK.shared.getCampaigns(language: "e
l
")
updateUI(with: freshCampaigns)
} catch {
// Handle error, keep cached data
...
...
@@ -1427,7 +1449,7 @@ if networkStatus != 1 {
}
// Check language parameter
WarplySDK.shared.getCampaigns(language: "e
n
") // Try different language
WarplySDK.shared.getCampaigns(language: "e
l
") // 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: "e
n
") {
[
weak self
]
campaigns in
WarplySDK.shared.getCampaigns(language: "e
l
") {
[
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: "e
n
")
async let coupons = WarplySDK.shared.getCoupons(language: "e
n
")
async let campaigns = WarplySDK.shared.getCampaigns(language: "e
l
")
async let coupons = WarplySDK.shared.getCoupons(language: "e
l
")
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! 🚀**
...
...
SwiftWarplyFramework/SwiftWarplyFramework/Core/WarplySDK.swift
View file @
ca49526
...
...
@@ -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
}
}
...
...
SwiftWarplyFramework/SwiftWarplyFramework/Network/NetworkService.swift
View file @
ca49526
...
...
@@ -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 comprehensive headers based on original Objective-C implementation
addWarplyHeaders
(
to
:
&
request
,
endpoint
:
endpoint
)
// Add headers
// 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
...
...
Please
register
or
login
to post a comment