Skip to content

Commit

Permalink
Merge branch 'master' into feature/itbl_track_anon_user
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
joaodordio committed Jan 16, 2025
2 parents 82fdf09 + ab02a03 commit 35247e2
Show file tree
Hide file tree
Showing 99 changed files with 2,268 additions and 1,022 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*]
indent_style = space
indent_size = 4
tab_width = 4
11 changes: 8 additions & 3 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on: pull_request

jobs:
run-tests-job:
runs-on: macos-12
runs-on: macos-14

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Expand All @@ -13,12 +13,17 @@ jobs:
with:
xcode-version: latest-stable

- name: Setup Ruby and xcpretty
run: |
gem install erb
gem install xcpretty
- name: Build and test
run: |
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]}
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]}
- name: CocoaPods lint
run: pod lib lint
run: pod lib lint --allow-warnings

- name: Upload coverage report to codecov.io
run: bash <(curl -s https://codecov.io/bash) -X gcov -J 'IterableSDK' -J 'IterableAppExtensions'
2 changes: 1 addition & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on: pull_request

jobs:
run-e2e-job:
runs-on: macos-12
runs-on: macos-14

steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Merge anonymous profiles into an existing, known user profiles (when needed).
- 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).

## [Unreleased]
### Added
- Added `mobileFrameworkInfo` configuration option to `IterableConfig` to identify the mobile framework (Flutter, React Native, or Native) being used with the SDK.

## [6.5.9]
### Added
- Support for JSON-only in-app messages, JSON-only messages are now handled by the onNewInApp handler and consumed after retrieval
- Enhanced notification state tracking to align with system notification permissions changes

### Changed
- reorganized files and updated documentation url in podspec

## [6.5.8]
### Fixed
- Fixed incorrect tracking of pushOpen for push notifications with Wake App enabled. Tracking now happens only when users tap to open the app.
- Fixed the default `notificationsEnabled` value returned when `autoPushRegistration` is set to `false`.

### Changed
- Updated repository name on Fastline script and podspec files.
- Comments out outdated tests that need to be revisited.
- Updated sample app to use generic URLs.

## [6.5.7]
### Fixed
- Fixed deeplink re-routing issue where delegate would only return `false` value. Thanks to @scottasoutherland :)
Expand Down
2 changes: 2 additions & 0 deletions Iterable-iOS-AppExtensions.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Pod::Spec.new do |s|
s.source = { :git => "https://github.com/Iterable/iterable-swift-sdk.git", :tag => s.version }
s.source_files = "notification-extension/*.{h,m,swift}"

s.documentation_url = "https://support.iterable.com/hc/en-us/articles/360035018152-Iterable-s-iOS-SDK"

s.pod_target_xcconfig = {
'SWIFT_VERSION' => '5.3'
}
Expand Down
2 changes: 2 additions & 0 deletions Iterable-iOS-SDK.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ Pod::Spec.new do |s|
s.source = { :git => "https://github.com/Iterable/iterable-swift-sdk.git", :tag => s.version }
s.source_files = "swift-sdk/**/*.{h,m,swift}"
s.exclude_files = "swift-sdk/swiftui/**"

s.documentation_url = "https://support.iterable.com/hc/en-us/articles/360035018152-Iterable-s-iOS-SDK"

s.pod_target_xcconfig = {
'SWIFT_VERSION' => '5.3'
Expand Down
1,030 changes: 610 additions & 420 deletions swift-sdk.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions swift-sdk/Constants.swift → swift-sdk/Core/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ enum Const {
static let matchedCriteria = "itbl_matched_criteria"
static let eventList = "itbl_event_list"
static let anonymousUsageTrack = "itbl_anonymous_usage_track"
static let isNotificationsEnabled = "itbl_isNotificationsEnabled"
static let hasStoredNotificationSetting = "itbl_hasStoredNotificationSetting"

static let attributionInfoExpiration = 24
}

Expand Down Expand Up @@ -115,6 +118,11 @@ enum Const {
static let location = "Location"
static let setCookie = "Set-Cookie"
}

enum RemoteNotification {
static let aps = "aps"
static let contentAvailable = "content-available"
}
}

enum JsonKey {
Expand Down Expand Up @@ -252,6 +260,9 @@ enum JsonKey {
}
}

static let mobileFrameworkInfo = "mobileFrameworkInfo"

static let frameworkType = "frameworkType"

// embedded
static let embeddedSessionId = "session"
Expand Down Expand Up @@ -325,6 +336,7 @@ enum JsonKey {
static let packageName = "packageName"
static let sdkVersion = "SDKVersion"
static let content = "content"
static let jsonOnly = "jsonOnly"
}

enum Payload {
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ import Foundation

/// the urgency level of this message (nil will be treated as `unassigned` when displaying this message)
public var priorityLevel: Double

/// Whether this message is a JSON-only message
public let jsonOnly: Bool

// MARK: - Private/Internal

Expand All @@ -64,7 +67,8 @@ import Foundation
inboxMetadata: IterableInboxMetadata? = nil,
customPayload: [AnyHashable: Any]? = nil,
read: Bool = false,
priorityLevel: Double = Const.PriorityLevel.unassigned) {
priorityLevel: Double = Const.PriorityLevel.unassigned,
jsonOnly: Bool = false) {
self.messageId = messageId
self.campaignId = campaignId
self.trigger = trigger
Expand All @@ -76,5 +80,6 @@ import Foundation
self.customPayload = customPayload
self.read = read
self.priorityLevel = priorityLevel
self.jsonOnly = jsonOnly
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 7 additions & 1 deletion swift-sdk/Internal/DataFieldsHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ struct DataFieldsHelper {
device: UIDevice,
bundle: Bundle,
notificationsEnabled: Bool,
deviceAttributes: [String: String]) -> [String: Any] {
deviceAttributes: [String: String],
mobileFrameworkInfo: IterableAPIMobileFrameworkInfo) -> [String: Any] {
var dataFields = [String: Any]()

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

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

dataFields[JsonKey.mobileFrameworkInfo] = [
JsonKey.frameworkType: mobileFrameworkInfo.frameworkType.rawValue,
JsonKey.iterableSdkVersion: mobileFrameworkInfo.iterableSdkVersion ?? "unknown"
]

return dataFields
}

Expand Down
97 changes: 85 additions & 12 deletions swift-sdk/Internal/InternalIterableAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {

// MARK: - API Request Calls

func register(token: Data,
func register(token: String,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) {

Expand All @@ -267,23 +267,26 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
onFailure?(errorMessage, nil)
return
}
if !isEitherUserIdOrEmailSet() && localStorage.userIdAnnon == nil {

if !isEitherUserIdOrEmailSet() && localStorage.userIdAnnon == nil {
if config.enableAnonActivation {
anonymousUserManager.trackAnonTokenRegistration(token: token.hexString())
}
onFailure?("Iterable SDK must be initialized with an API key and user email/userId before calling SDK methods", nil)
return
}
}
hexToken = token

hexToken = token.hexString()
let registerTokenInfo = RegisterTokenInfo(hexToken: token.hexString(),
appName: appName,
pushServicePlatform: config.pushPlatform,
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
deviceId: deviceId,
deviceAttributes: deviceAttributes,
sdkVersion: localStorage.sdkVersion)
let mobileFrameworkInfo = config.mobileFrameworkInfo ?? createDefaultMobileFrameworkInfo()

let registerTokenInfo = RegisterTokenInfo(hexToken: token,
appName: appName,
pushServicePlatform: config.pushPlatform,
apnsType: dependencyContainer.apnsTypeChecker.apnsType,
deviceId: deviceId,
deviceAttributes: deviceAttributes,
sdkVersion: localStorage.sdkVersion,
mobileFrameworkInfo: mobileFrameworkInfo)
requestHandler.register(registerTokenInfo: registerTokenInfo,
notificationStateProvider: notificationStateProvider,
onSuccess: { (_ data: [AnyHashable: Any]?) in
Expand All @@ -297,6 +300,12 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
)
}

func register(token: Data,
onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) {
register(token: token.hexString(), onSuccess: onSuccess, onFailure: onFailure)
}

@discardableResult
func disableDeviceForCurrentUser(withOnSuccess onSuccess: OnSuccessHandler? = nil,
onFailure: OnFailureHandler? = nil) -> Pending<SendRequestValue, SendRequestError> {
Expand All @@ -305,12 +314,18 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
onFailure?(errorMessage, nil)
return SendRequestError.createErroredFuture(reason: errorMessage)
}

guard userId != nil || email != nil else {
let errorMessage = "either userId or email must be present"
onFailure?(errorMessage, nil)
return SendRequestError.createErroredFuture(reason: errorMessage)
}

// We need to call register token here so that we can trigger the device registration
// with the updated notification settings

register(token: hexToken)

return requestHandler.disableDeviceForCurrentUser(hexToken: hexToken, withOnSuccess: onSuccess, onFailure: onFailure)
}

Expand Down Expand Up @@ -655,6 +670,8 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
private var _userId: String?
private var _successCallback: OnSuccessHandler? = nil
private var _failureCallback: OnFailureHandler? = nil

private let notificationCenter: NotificationCenterProtocol


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

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

checkRemoteConfiguration()

addForegroundObservers()

return inAppManager.start()
}

private func addForegroundObservers() {
notificationCenter.addObserver(self,
selector: #selector(onAppDidBecomeActiveNotification(notification:)),
name: UIApplication.didBecomeActiveNotification,
object: nil)
}

@objc private func onAppDidBecomeActiveNotification(notification: Notification) {
guard config.autoPushRegistration else { return }

notificationStateProvider.isNotificationsEnabled { [weak self] systemEnabled in
guard let self = self else { return }

let storedEnabled = self.localStorage.isNotificationsEnabled
let hasStoredPermission = self.localStorage.hasStoredNotificationSetting

if self.isEitherUserIdOrEmailSet() {
if hasStoredPermission && (storedEnabled != systemEnabled) {
if !systemEnabled {
self.disableDeviceForCurrentUser()
} else {
self.notificationStateProvider.registerForRemoteNotifications()
}
}

// Always store the current state
self.localStorage.isNotificationsEnabled = systemEnabled
self.localStorage.hasStoredNotificationSetting = true
}
}
}

private func handle(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
guard let launchOptions = launchOptions else {
return
}

if let remoteNotificationPayload = launchOptions[UIApplication.LaunchOptionsKey.remoteNotification] as? [AnyHashable: Any] {

if let aps = remoteNotificationPayload[Const.RemoteNotification.aps] as? [String: Any],
let contentAvailable = aps[Const.RemoteNotification.contentAvailable] as? Int,
contentAvailable == 1 {
ITBInfo("Received push notification with wakey content-available flag")
return
}

if let _ = IterableUtil.rootViewController {
// we are ready
IterableAppIntegration.implementation?.performDefaultNotificationAction(remoteNotificationPayload)
Expand Down Expand Up @@ -943,10 +1004,22 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider {
print("Error converting dictionary to data: \(error)")
}
}
}

private func createDefaultMobileFrameworkInfo() -> IterableAPIMobileFrameworkInfo {
let frameworkType = IterableAPIMobileFrameworkDetector.frameworkType()
return IterableAPIMobileFrameworkInfo(
frameworkType: frameworkType,
iterableSdkVersion: frameworkType == .native ? localStorage.sdkVersion : nil
)
}

deinit {
ITBInfo()
notificationCenter.removeObserver(self)
requestHandler.stop()
}

}


3 changes: 2 additions & 1 deletion swift-sdk/Internal/InternalIterableAppIntegration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ protocol NotificationStateProviderProtocol {
struct SystemNotificationStateProvider: NotificationStateProviderProtocol {
func isNotificationsEnabled(withCallback callback: @escaping (Bool) -> Void) {
UNUserNotificationCenter.current().getNotificationSettings { setttings in
callback(setttings.authorizationStatus != .denied)
let notificationsDisabled = setttings.authorizationStatus == .notDetermined || setttings.authorizationStatus == .denied
callback(!notificationsDisabled)
}
}

Expand Down
Loading

0 comments on commit 35247e2

Please sign in to comment.