Skip to content

Commit 35247e2

Browse files
committed
Merge branch 'master' into feature/itbl_track_anon_user
# Conflicts: # CHANGELOG.md # Iterable-iOS-AppExtensions.podspec # Iterable-iOS-SDK.podspec # swift-sdk.xcodeproj/project.pbxproj # swift-sdk/Core/Constants.swift # swift-sdk/Internal/InternalIterableAPI.swift # swift-sdk/Internal/IterableUserDefaults.swift # swift-sdk/Internal/Utilities/LocalStorage.swift # swift-sdk/SDK/IterableAPI.swift # swift-sdk/SDK/IterableConfig.swift # tests/common/MockLocalStorage.swift
2 parents 82fdf09 + ab02a03 commit 35247e2

File tree

99 files changed

+2268
-1022
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+2268
-1022
lines changed

.editorconfig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
root = true
2+
3+
[*]
4+
indent_style = space
5+
indent_size = 4
6+
tab_width = 4

.github/workflows/build-and-test.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: pull_request
44

55
jobs:
66
run-tests-job:
7-
runs-on: macos-12
7+
runs-on: macos-14
88

99
steps:
1010
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -13,12 +13,17 @@ jobs:
1313
with:
1414
xcode-version: latest-stable
1515

16+
- name: Setup Ruby and xcpretty
17+
run: |
18+
gem install erb
19+
gem install xcpretty
20+
1621
- name: Build and test
1722
run: |
18-
xcodebuild test -project swift-sdk.xcodeproj -scheme swift-sdk -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 13 Pro Max' -enableCodeCoverage YES CODE_SIGNING_REQUIRED=NO | xcpretty && exit ${PIPESTATUS[0]}
23+
xcodebuild test -project swift-sdk.xcodeproj -scheme swift-sdk -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16 Pro' -enableCodeCoverage YES CODE_SIGNING_REQUIRED=NO | xcpretty && exit ${PIPESTATUS[0]}
1924
2025
- name: CocoaPods lint
21-
run: pod lib lint
26+
run: pod lib lint --allow-warnings
2227

2328
- name: Upload coverage report to codecov.io
2429
run: bash <(curl -s https://codecov.io/bash) -X gcov -J 'IterableSDK' -J 'IterableAppExtensions'

.github/workflows/e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: pull_request
44

55
jobs:
66
run-e2e-job:
7-
runs-on: macos-12
7+
runs-on: macos-14
88

99
steps:
1010
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1616
- Merge anonymous profiles into an existing, known user profiles (when needed).
1717
- Anonymous user activation is currently in private beta. If you'd like to learn more about it or discuss using it, talk to your Iterable customer success manager (who can also provide detailed documentation).
1818

19+
## [Unreleased]
20+
### Added
21+
- Added `mobileFrameworkInfo` configuration option to `IterableConfig` to identify the mobile framework (Flutter, React Native, or Native) being used with the SDK.
22+
23+
## [6.5.9]
24+
### Added
25+
- Support for JSON-only in-app messages, JSON-only messages are now handled by the onNewInApp handler and consumed after retrieval
26+
- Enhanced notification state tracking to align with system notification permissions changes
27+
28+
### Changed
29+
- reorganized files and updated documentation url in podspec
30+
31+
## [6.5.8]
32+
### Fixed
33+
- Fixed incorrect tracking of pushOpen for push notifications with Wake App enabled. Tracking now happens only when users tap to open the app.
34+
- Fixed the default `notificationsEnabled` value returned when `autoPushRegistration` is set to `false`.
35+
36+
### Changed
37+
- Updated repository name on Fastline script and podspec files.
38+
- Comments out outdated tests that need to be revisited.
39+
- Updated sample app to use generic URLs.
40+
1941
## [6.5.7]
2042
### Fixed
2143
- Fixed deeplink re-routing issue where delegate would only return `false` value. Thanks to @scottasoutherland :)

Iterable-iOS-AppExtensions.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Pod::Spec.new do |s|
1616
s.source = { :git => "https://github.com/Iterable/iterable-swift-sdk.git", :tag => s.version }
1717
s.source_files = "notification-extension/*.{h,m,swift}"
1818

19+
s.documentation_url = "https://support.iterable.com/hc/en-us/articles/360035018152-Iterable-s-iOS-SDK"
20+
1921
s.pod_target_xcconfig = {
2022
'SWIFT_VERSION' => '5.3'
2123
}

Iterable-iOS-SDK.podspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Pod::Spec.new do |s|
1616
s.source = { :git => "https://github.com/Iterable/iterable-swift-sdk.git", :tag => s.version }
1717
s.source_files = "swift-sdk/**/*.{h,m,swift}"
1818
s.exclude_files = "swift-sdk/swiftui/**"
19+
20+
s.documentation_url = "https://support.iterable.com/hc/en-us/articles/360035018152-Iterable-s-iOS-SDK"
1921

2022
s.pod_target_xcconfig = {
2123
'SWIFT_VERSION' => '5.3'

swift-sdk.xcodeproj/project.pbxproj

Lines changed: 610 additions & 420 deletions
Large diffs are not rendered by default.

swift-sdk/Constants.swift renamed to swift-sdk/Core/Constants.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ enum Const {
7777
static let matchedCriteria = "itbl_matched_criteria"
7878
static let eventList = "itbl_event_list"
7979
static let anonymousUsageTrack = "itbl_anonymous_usage_track"
80+
static let isNotificationsEnabled = "itbl_isNotificationsEnabled"
81+
static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting"
82+
8083
static let attributionInfoExpiration = 24
8184
}
8285

@@ -115,6 +118,11 @@ enum Const {
115118
static let location = "Location"
116119
static let setCookie = "Set-Cookie"
117120
}
121+
122+
enum RemoteNotification {
123+
static let aps = "aps"
124+
static let contentAvailable = "content-available"
125+
}
118126
}
119127

120128
enum JsonKey {
@@ -252,6 +260,9 @@ enum JsonKey {
252260
}
253261
}
254262

263+
static let mobileFrameworkInfo = "mobileFrameworkInfo"
264+
265+
static let frameworkType = "frameworkType"
255266

256267
// embedded
257268
static let embeddedSessionId = "session"
@@ -325,6 +336,7 @@ enum JsonKey {
325336
static let packageName = "packageName"
326337
static let sdkVersion = "SDKVersion"
327338
static let content = "content"
339+
static let jsonOnly = "jsonOnly"
328340
}
329341

330342
enum Payload {

swift-sdk/IterableInAppMessage.swift renamed to swift-sdk/Core/Models/IterableInAppMessage.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ import Foundation
5151

5252
/// the urgency level of this message (nil will be treated as `unassigned` when displaying this message)
5353
public var priorityLevel: Double
54+
55+
/// Whether this message is a JSON-only message
56+
public let jsonOnly: Bool
5457

5558
// MARK: - Private/Internal
5659

@@ -64,7 +67,8 @@ import Foundation
6467
inboxMetadata: IterableInboxMetadata? = nil,
6568
customPayload: [AnyHashable: Any]? = nil,
6669
read: Bool = false,
67-
priorityLevel: Double = Const.PriorityLevel.unassigned) {
70+
priorityLevel: Double = Const.PriorityLevel.unassigned,
71+
jsonOnly: Bool = false) {
6872
self.messageId = messageId
6973
self.campaignId = campaignId
7074
self.trigger = trigger
@@ -76,5 +80,6 @@ import Foundation
7680
self.customPayload = customPayload
7781
self.read = read
7882
self.priorityLevel = priorityLevel
83+
self.jsonOnly = jsonOnly
7984
}
8085
}
File renamed without changes.

swift-sdk/Internal/DataFieldsHelper.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ struct DataFieldsHelper {
1414
device: UIDevice,
1515
bundle: Bundle,
1616
notificationsEnabled: Bool,
17-
deviceAttributes: [String: String]) -> [String: Any] {
17+
deviceAttributes: [String: String],
18+
mobileFrameworkInfo: IterableAPIMobileFrameworkInfo) -> [String: Any] {
1819
var dataFields = [String: Any]()
1920

2021
deviceAttributes.forEach { deviceAttribute in
@@ -33,6 +34,11 @@ struct DataFieldsHelper {
3334

3435
dataFields.addAll(other: createUIDeviceFields(device: device))
3536

37+
dataFields[JsonKey.mobileFrameworkInfo] = [
38+
JsonKey.frameworkType: mobileFrameworkInfo.frameworkType.rawValue,
39+
JsonKey.iterableSdkVersion: mobileFrameworkInfo.iterableSdkVersion ?? "unknown"
40+
]
41+
3642
return dataFields
3743
}
3844

swift-sdk/Internal/InternalIterableAPI.swift

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
256256

257257
// MARK: - API Request Calls
258258

259-
func register(token: Data,
259+
func register(token: String,
260260
onSuccess: OnSuccessHandler? = nil,
261261
onFailure: OnFailureHandler? = nil) {
262262

@@ -267,23 +267,26 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
267267
onFailure?(errorMessage, nil)
268268
return
269269
}
270-
271-
if !isEitherUserIdOrEmailSet() && localStorage.userIdAnnon == nil {
270+
271+
if !isEitherUserIdOrEmailSet() && localStorage.userIdAnnon == nil {
272272
if config.enableAnonActivation {
273273
anonymousUserManager.trackAnonTokenRegistration(token: token.hexString())
274274
}
275275
onFailure?("Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods", nil)
276276
return
277-
}
277+
}
278+
hexToken = token
278279

279-
hexToken = token.hexString()
280-
let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(),
281-
appName: appName,
282-
pushServicePlatform: config.pushPlatform,
283-
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
284-
deviceId: deviceId,
285-
deviceAttributes: deviceAttributes,
286-
sdkVersion: localStorage.sdkVersion)
280+
let mobileFrameworkInfo = config.mobileFrameworkInfo ?? createDefaultMobileFrameworkInfo()
281+
282+
let registerTokenInfo = RegisterTokenInfo(hexToken: token,
283+
appName: appName,
284+
pushServicePlatform: config.pushPlatform,
285+
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
286+
deviceId: deviceId,
287+
deviceAttributes: deviceAttributes,
288+
sdkVersion: localStorage.sdkVersion,
289+
mobileFrameworkInfo: mobileFrameworkInfo)
287290
requestHandler.register(registerTokenInfo: registerTokenInfo,
288291
notificationStateProvider: notificationStateProvider,
289292
onSuccess: { (_ data: [AnyHashable: Any]?) in
@@ -297,6 +300,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
297300
)
298301
}
299302

303+
func register(token: Data,
304+
onSuccess: OnSuccessHandler? = nil,
305+
onFailure: OnFailureHandler? = nil) {
306+
register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure)
307+
}
308+
300309
@discardableResult
301310
func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil,
302311
onFailure: OnFailureHandler? = nil) -> Pending<SendRequestValue, SendRequestError> {
@@ -305,12 +314,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
305314
onFailure?(errorMessage, nil)
306315
return SendRequestError.createErroredFuture(reason: errorMessage)
307316
}
317+
308318
guard userId != nil || email != nil else {
309319
let errorMessage = "either userId or email must be present"
310320
onFailure?(errorMessage, nil)
311321
return SendRequestError.createErroredFuture(reason: errorMessage)
312322
}
313323

324+
// We need to call register token here so that we can trigger the device registration
325+
// with the updated notification settings
326+
327+
register(token: hexToken)
328+
314329
return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure)
315330
}
316331

@@ -655,6 +670,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
655670
private var _userId: String?
656671
private var _successCallback: OnSuccessHandler? = nil
657672
private var _failureCallback: OnFailureHandler? = nil
673+
674+
private let notificationCenter: NotificationCenterProtocol
658675

659676

660677
/// the hex representation of this device token
@@ -839,6 +856,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
839856
//localStorage.email = nil // remove this before pushing the code (only for testing)
840857
inAppDisplayer = dependencyContainer.inAppDisplayer
841858
urlOpener = dependencyContainer.urlOpener
859+
notificationCenter = dependencyContainer.notificationCenter
842860
deepLinkManager = DeepLinkManager(redirectNetworkSessionProvider: dependencyContainer)
843861
}
844862

@@ -871,15 +889,58 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
871889
requestHandler.start()
872890

873891
checkRemoteConfiguration()
892+
893+
addForegroundObservers()
874894

875895
return inAppManager.start()
876896
}
877897

898+
private func addForegroundObservers() {
899+
notificationCenter.addObserver(self,
900+
selector: #selector(onAppDidBecomeActiveNotification(notification:)),
901+
name: UIApplication.didBecomeActiveNotification,
902+
object: nil)
903+
}
904+
905+
@objc private func onAppDidBecomeActiveNotification(notification: Notification) {
906+
guard config.autoPushRegistration else { return }
907+
908+
notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in
909+
guard let self = self else { return }
910+
911+
let storedEnabled = self.localStorage.isNotificationsEnabled
912+
let hasStoredPermission = self.localStorage.hasStoredNotificationSetting
913+
914+
if self.isEitherUserIdOrEmailSet() {
915+
if hasStoredPermission && (storedEnabled != systemEnabled) {
916+
if !systemEnabled {
917+
self.disableDeviceForCurrentUser()
918+
} else {
919+
self.notificationStateProvider.registerForRemoteNotifications()
920+
}
921+
}
922+
923+
// Always store the current state
924+
self.localStorage.isNotificationsEnabled = systemEnabled
925+
self.localStorage.hasStoredNotificationSetting = true
926+
}
927+
}
928+
}
929+
878930
private func handle(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
879931
guard let launchOptions = launchOptions else {
880932
return
881933
}
934+
882935
if let remoteNotificationPayload = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any] {
936+
937+
if let aps = remoteNotificationPayload[Const.RemoteNotification.aps] as? [String: Any],
938+
let contentAvailable = aps[Const.RemoteNotification.contentAvailable] as? Int,
939+
contentAvailable == 1 {
940+
ITBInfo("Received push notification with wakey content-available flag")
941+
return
942+
}
943+
883944
if let _ = IterableUtil.rootViewController {
884945
// we are ready
885946
IterableAppIntegration.implementation?.performDefaultNotificationAction(remoteNotificationPayload)
@@ -943,10 +1004,22 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
9431004
print("Error converting dictionary to data: \(error)")
9441005
}
9451006
}
1007+
}
1008+
1009+
private func createDefaultMobileFrameworkInfo() -> IterableAPIMobileFrameworkInfo {
1010+
let frameworkType = IterableAPIMobileFrameworkDetector.frameworkType()
1011+
return IterableAPIMobileFrameworkInfo(
1012+
frameworkType: frameworkType,
1013+
iterableSdkVersion: frameworkType == .native ? localStorage.sdkVersion : nil
1014+
)
9461015
}
9471016

9481017
deinit {
9491018
ITBInfo()
1019+
notificationCenter.removeObserver(self)
9501020
requestHandler.stop()
9511021
}
1022+
9521023
}
1024+
1025+

swift-sdk/Internal/InternalIterableAppIntegration.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ protocol NotificationStateProviderProtocol {
1616
struct SystemNotificationStateProvider: NotificationStateProviderProtocol {
1717
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
1818
UNUserNotificationCenter.current().getNotificationSettings { setttings in
19-
callback(setttings.authorizationStatus != .denied)
19+
let notificationsDisabled = setttings.authorizationStatus == .notDetermined || setttings.authorizationStatus == .denied
20+
callback(!notificationsDisabled)
2021
}
2122
}
2223

0 commit comments

Comments
 (0)