diff --git a/Airship.podspec b/Airship.podspec index 7ced71096..7745aaac5 100644 --- a/Airship.podspec +++ b/Airship.podspec @@ -1,4 +1,4 @@ -AIRSHIP_VERSION="16.11.3" +AIRSHIP_VERSION="16.12.0" Pod::Spec.new do |s| s.version = AIRSHIP_VERSION diff --git a/Airship/AirshipAutomation/Source/UAInAppAutomation+Internal.h b/Airship/AirshipAutomation/Source/UAInAppAutomation+Internal.h index 70f249ebb..92b2578df 100644 --- a/Airship/AirshipAutomation/Source/UAInAppAutomation+Internal.h +++ b/Airship/AirshipAutomation/Source/UAInAppAutomation+Internal.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN /** * Factory method. Use for testing. * + * @param config The UARuntimeConfigInstance. * @param automationEngine The automation engine. * @param audienceManager The audience manager. * @param remoteDataClient The remote data client. @@ -39,7 +40,8 @@ NS_ASSUME_NONNULL_BEGIN * @param privacyManager The privacy manager. * @return A in-app automation manager instance. */ -+ (instancetype)automationWithEngine:(UAAutomationEngine *)automationEngine ++ (instancetype)automationWithConfig:(UARuntimeConfig *)config + automationEngine:(UAAutomationEngine *)automationEngine audienceManager:(UAInAppAudienceManager *)audienceManager remoteDataClient:(UAInAppRemoteDataClient *)remoteDataClient dataStore:(UAPreferenceDataStore *)dataStore diff --git a/Airship/AirshipAutomation/Source/UAInAppAutomation.m b/Airship/AirshipAutomation/Source/UAInAppAutomation.m index 0340ec446..fab589b70 100644 --- a/Airship/AirshipAutomation/Source/UAInAppAutomation.m +++ b/Airship/AirshipAutomation/Source/UAInAppAutomation.m @@ -42,7 +42,6 @@ @interface UAInAppAutomation () *redirectURLs; - @end @implementation UAInAppAutomation @@ -51,7 +50,8 @@ + (UAInAppAutomation *)shared { return (UAInAppAutomation *)[UAirship componentForClassName:NSStringFromClass([self class])]; } -+ (instancetype)automationWithEngine:(UAAutomationEngine *)automationEngine ++ (instancetype)automationWithConfig:(UARuntimeConfig *)config + automationEngine:(UAAutomationEngine *)automationEngine audienceManager:(UAInAppAudienceManager *)audienceManager remoteDataClient:(UAInAppRemoteDataClient *)remoteDataClient dataStore:(UAPreferenceDataStore *)dataStore @@ -61,15 +61,16 @@ + (instancetype)automationWithEngine:(UAAutomationEngine *)automationEngine frequencyLimitManager:(UAFrequencyLimitManager *)frequencyLimitManager privacyManager:(UAPrivacyManager *)privacyManager { - return [[self alloc] initWithAutomationEngine:automationEngine - audienceManager:audienceManager - remoteDataClient:remoteDataClient - dataStore:dataStore - inAppMessageManager:inAppMessageManager - channel:channel - deferredScheduleAPIClient:deferredScheduleAPIClient - frequencyLimitManager:frequencyLimitManager - privacyManager:privacyManager]; + return [[self alloc] initWithConfig:config + automationEngine:automationEngine + audienceManager:audienceManager + remoteDataClient:remoteDataClient + dataStore:dataStore + inAppMessageManager:inAppMessageManager + channel:channel + deferredScheduleAPIClient:deferredScheduleAPIClient + frequencyLimitManager:frequencyLimitManager + privacyManager:privacyManager]; } + (instancetype)automationWithConfig:(UARuntimeConfig *)config @@ -101,26 +102,28 @@ + (instancetype)automationWithConfig:(UARuntimeConfig *)config UAFrequencyLimitManager *frequencyLimitManager = [UAFrequencyLimitManager managerWithConfig:config]; - return [[UAInAppAutomation alloc] initWithAutomationEngine:automationEngine - audienceManager:audienceManager - remoteDataClient:dataClient - dataStore:dataStore - inAppMessageManager:inAppMessageManager - channel:channel - deferredScheduleAPIClient:deferredScheduleAPIClient - frequencyLimitManager:frequencyLimitManager + return [[UAInAppAutomation alloc] initWithConfig:config + automationEngine:automationEngine + audienceManager:audienceManager + remoteDataClient:dataClient + dataStore:dataStore + inAppMessageManager:inAppMessageManager + channel:channel + deferredScheduleAPIClient:deferredScheduleAPIClient + frequencyLimitManager:frequencyLimitManager privacyManager:privacyManager]; } -- (instancetype)initWithAutomationEngine:(UAAutomationEngine *)automationEngine - audienceManager:(UAInAppAudienceManager *)audienceManager - remoteDataClient:(UAInAppRemoteDataClient *)remoteDataClient - dataStore:(UAPreferenceDataStore *)dataStore - inAppMessageManager:(UAInAppMessageManager *)inAppMessageManager - channel:(UAChannel *)channel - deferredScheduleAPIClient:(UADeferredScheduleAPIClient *)deferredScheduleAPIClient - frequencyLimitManager:(UAFrequencyLimitManager *)frequencyLimitManager - privacyManager:(UAPrivacyManager *)privacyManager { +- (instancetype)initWithConfig:(UARuntimeConfig *)config + automationEngine:(UAAutomationEngine *)automationEngine + audienceManager:(UAInAppAudienceManager *)audienceManager + remoteDataClient:(UAInAppRemoteDataClient *)remoteDataClient + dataStore:(UAPreferenceDataStore *)dataStore + inAppMessageManager:(UAInAppMessageManager *)inAppMessageManager + channel:(UAChannel *)channel + deferredScheduleAPIClient:(UADeferredScheduleAPIClient *)deferredScheduleAPIClient + frequencyLimitManager:(UAFrequencyLimitManager *)frequencyLimitManager + privacyManager:(UAPrivacyManager *)privacyManager { self = [super init]; @@ -149,6 +152,10 @@ - (instancetype)initWithAutomationEngine:(UAAutomationEngine *)automationEngine UA_STRONGIFY(self) [self onComponentEnableChange]; }; + + if (config.autoPauseInAppAutomationOnLaunch) { + self.paused = YES; + } } return self; @@ -161,6 +168,7 @@ - (void)airshipReady { name:UAPrivacyManager.changeEvent object:nil]; + [self updateSubscription]; [self updateEnginePauseState]; } diff --git a/Airship/AirshipConfig.xcconfig b/Airship/AirshipConfig.xcconfig index 295bbcc2e..75d3b352b 100644 --- a/Airship/AirshipConfig.xcconfig +++ b/Airship/AirshipConfig.xcconfig @@ -1,6 +1,6 @@ //* Copyright Airship and Contributors */ -CURRENT_PROJECT_VERSION = 16.11.3 +CURRENT_PROJECT_VERSION = 16.12.0 // Uncomment to include the preview build warning // OTHER_CFLAGS = $(inherited) -DUA_PREVIEW=1 diff --git a/Airship/AirshipCore/Source/AirshipKeychainAccess.swift b/Airship/AirshipCore/Source/AirshipKeychainAccess.swift index 81c75f0cb..95d14e5f4 100644 --- a/Airship/AirshipCore/Source/AirshipKeychainAccess.swift +++ b/Airship/AirshipCore/Source/AirshipKeychainAccess.swift @@ -61,7 +61,7 @@ public class AirshipKeychainAccess: NSObject { identifier: identifier, service: self.service ) - + // Write to old location in case of a downgrade if let bundleID = Bundle.main.bundleIdentifier { let _ = Keychain.writeCredentials( @@ -70,7 +70,7 @@ public class AirshipKeychainAccess: NSObject { service: bundleID ) } - + completionHandler?(result) } } @@ -124,8 +124,10 @@ public class AirshipKeychainAccess: NSObject { @objc public func readCredentials( identifier: String, - completionHandler: @escaping(AirshipKeychainCredentials? - ) -> Void) { + completionHandler: @escaping ( + AirshipKeychainCredentials? + ) -> Void + ) { self.dispatcher.dispatchAsync { let credentials = self.readCredentialsHelper( identifier: identifier @@ -142,16 +144,16 @@ public class AirshipKeychainAccess: NSObject { ) { return credentials } - + // If we do not have a new value, check // the old service location if let bundleID = Bundle.main.bundleIdentifier { - + let old = Keychain.readCredentials( identifier: identifier, service: bundleID ) - + if let old = old { // Migrate old data to new service location let _ = Keychain.writeCredentials( @@ -159,11 +161,11 @@ public class AirshipKeychainAccess: NSObject { identifier: identifier, service: self.service ) - + return old } } - + return nil } } @@ -176,9 +178,9 @@ fileprivate struct Keychain { service: String ) -> Bool { guard let identifierData = identifier.data(using: .utf8), - let passwordData = credentials.password.data( + let passwordData = credentials.password.data( using: .utf8 - ) + ) else { return false } @@ -186,12 +188,12 @@ fileprivate struct Keychain { deleteCredentials(identifier: identifier, service: service) let addquery: [String: Any] = [ - kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock, + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrGeneric as String: identifierData, kSecAttrAccount as String: credentials.username, - kSecValueData as String: passwordData + kSecValueData as String: passwordData, ] let status = SecItemAdd(addquery as CFDictionary, nil) @@ -230,26 +232,63 @@ fileprivate struct Keychain { kSecAttrService as String: service, kSecAttrGeneric as String: identifierData, kSecReturnAttributes as String: true, - kSecReturnData as String: true + kSecReturnData as String: true, ] + var item: CFTypeRef? let status = SecItemCopyMatching(searchQuery as CFDictionary, &item) guard status == errSecSuccess else { return nil } - guard let existingItem = item as? [String : Any], - let passwordData = existingItem[kSecValueData as String] as? Data, - let password = String(data: passwordData, encoding: String.Encoding.utf8), + guard let existingItem = item as? [String: Any] else { + return nil + } + + guard let passwordData = existingItem[kSecValueData as String] as? Data, + let password = String( + data: passwordData, + encoding: String.Encoding.utf8 + ), let username = existingItem[kSecAttrAccount as String] as? String else { return nil } + let attrAccessible = existingItem[kSecAttrAccessible as String] as? String + if attrAccessible != (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) { + updateThisDeviceOnly(identifier: identifier, service: service) + } + return AirshipKeychainCredentials( username: username, password: password ) } + + static func updateThisDeviceOnly(identifier: String, service: String) { + guard let identifierData = identifier.data(using: .utf8) else { + return + } + + let updateQuery: [String: Any] = [ + kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + ] + + let searchQuery: [String: Any] = [ + kSecClass as String: kSecClassGenericPassword, + kSecMatchLimit as String: kSecMatchLimitOne, + kSecAttrService as String: service, + kSecAttrGeneric as String: identifierData + ] + + let updateStatus = SecItemUpdate(searchQuery as CFDictionary, updateQuery as CFDictionary) + + if (updateStatus == errSecSuccess) { + AirshipLogger.trace("Updated keychain value \(identifier) to this device only") + } else { + AirshipLogger.error("Failed to update keychain value \(identifier) status:\(updateStatus)") + } + } } diff --git a/Airship/AirshipCore/Source/AirshipVersion.swift b/Airship/AirshipCore/Source/AirshipVersion.swift index 5d3be8535..adaacc812 100644 --- a/Airship/AirshipCore/Source/AirshipVersion.swift +++ b/Airship/AirshipCore/Source/AirshipVersion.swift @@ -5,7 +5,7 @@ import Foundation; @objc(UAirshipVersion) public class AirshipVersion : NSObject { - public static let version = "16.11.3" + public static let version = "16.12.0" @objc public class func get() -> String { diff --git a/Airship/AirshipCore/Source/Config.swift b/Airship/AirshipCore/Source/Config.swift index 81c230c0f..9570e4daa 100644 --- a/Airship/AirshipCore/Source/Config.swift +++ b/Airship/AirshipCore/Source/Config.swift @@ -37,7 +37,10 @@ public class Config: NSObject, NSCopying { /// The log level used for production apps. Defaults to `error`. @objc public var productionLogLevel: LogLevel = .error - + + /// Auto pause InAppAutomation on launch. Defaults to `false` + @objc + public var autoPauseInAppAutomationOnLaunch: Bool = false /// The airship cloud site. Defaults to `us`. @objc @@ -393,6 +396,7 @@ public class Config: NSObject, NSCopying { _detectProvisioningMode = config.detectProvisioningMode _defaultProvisioningMode = config._defaultProvisioningMode _inProduction = config._inProduction + autoPauseInAppAutomationOnLaunch = config.autoPauseInAppAutomationOnLaunch } public func copy(with zone: NSZone? = nil) -> Any { diff --git a/Airship/AirshipCore/Source/RuntimeConfig.swift b/Airship/AirshipCore/Source/RuntimeConfig.swift index cec0c37bb..7f7d32c01 100644 --- a/Airship/AirshipCore/Source/RuntimeConfig.swift +++ b/Airship/AirshipCore/Source/RuntimeConfig.swift @@ -41,6 +41,10 @@ open class RuntimeConfig: NSObject { /// Dictionary of custom config values. @objc public let customConfig: [AnyHashable : Any]? + + /// Auto pause InAppAutomation on launch. + @objc + public let autoPauseInAppAutomationOnLaunch: Bool /// If enabled, the Airship library automatically registers for remote notifications when push is enabled /// and intercepts incoming notifications in both the foreground and upon launch. @@ -275,6 +279,7 @@ open class RuntimeConfig: NSObject { self.site = config.site self.defaultAnalyticsURL = config.analyticsURL?.normalizeURLString() self.defaultDeviceAPIURL = config.deviceAPIURL?.normalizeURLString() + self.autoPauseInAppAutomationOnLaunch = config.autoPauseInAppAutomationOnLaunch if let initialConfigURL = config.initialConfigURL { self.defaultRemoteDataAPIURL = initialConfigURL.normalizeURLString() diff --git a/Airship/AirshipCore/Tests/UAInAppAutomationTest.m b/Airship/AirshipCore/Tests/UAInAppAutomationTest.m index 94a8da08a..7024d03b6 100644 --- a/Airship/AirshipCore/Tests/UAInAppAutomationTest.m +++ b/Airship/AirshipCore/Tests/UAInAppAutomationTest.m @@ -74,7 +74,8 @@ - (void)setUp { self.airship.privacyManager = self.privacyManager; [self.airship makeShared]; - self.inAppAutomation = [UAInAppAutomation automationWithEngine:self.mockAutomationEngine + self.inAppAutomation = [UAInAppAutomation automationWithConfig:self.config + automationEngine:self.mockAutomationEngine audienceManager:self.mockAudienceManager remoteDataClient:self.mockRemoteDataClient dataStore:self.dataStore @@ -86,6 +87,53 @@ - (void)setUp { XCTAssertNotNil(self.engineDelegate); } +- (void)testAutoPauseEnabled { + UAConfig *config = [[UAConfig alloc] init]; + config.inProduction = NO; + config.site = UACloudSiteUS; + config.developmentAppKey = @"test-app-key"; + config.developmentAppSecret = @"test-app-secret"; + config.autoPauseInAppAutomationOnLaunch = YES; + + UARuntimeConfig *runtimeConfig = [[UARuntimeConfig alloc] initWithConfig:config dataStore:self.dataStore]; + self.inAppAutomation = [UAInAppAutomation automationWithConfig:runtimeConfig + automationEngine:self.mockAutomationEngine + audienceManager:self.mockAudienceManager + remoteDataClient:self.mockRemoteDataClient + dataStore:self.dataStore + inAppMessageManager:self.mockInAppMessageManager + channel:self.mockChannel + deferredScheduleAPIClient:self.mockDeferredClient + frequencyLimitManager:self.mockFrequencyLimitManager + privacyManager:self.privacyManager]; + + XCTAssertTrue(self.inAppAutomation.isPaused); +} + +- (void)testAutoPauseDisabled { + UAConfig *config = [[UAConfig alloc] init]; + config.inProduction = NO; + config.site = UACloudSiteUS; + config.developmentAppKey = @"test-app-key"; + config.developmentAppSecret = @"test-app-secret"; + config.autoPauseInAppAutomationOnLaunch = NO; + UARuntimeConfig *runtimeConfig = [[UARuntimeConfig alloc] initWithConfig:config dataStore:self.dataStore]; + + self.inAppAutomation = [UAInAppAutomation automationWithConfig:runtimeConfig + automationEngine:self.mockAutomationEngine + audienceManager:self.mockAudienceManager + remoteDataClient:self.mockRemoteDataClient + dataStore:self.dataStore + inAppMessageManager:self.mockInAppMessageManager + channel:self.mockChannel + deferredScheduleAPIClient:self.mockDeferredClient + frequencyLimitManager:self.mockFrequencyLimitManager + privacyManager:self.privacyManager]; + + XCTAssertFalse(self.inAppAutomation.isPaused); +} + + - (void)testCheckEmptyAudience { UAScheduleAudience *emptyAudience = [UAScheduleAudience audienceWithBuilderBlock:^(UAScheduleAudienceBuilder *builder) { }]; diff --git a/AirshipContentExtension.podspec b/AirshipContentExtension.podspec index 54d970a6a..51624219d 100644 --- a/AirshipContentExtension.podspec +++ b/AirshipContentExtension.podspec @@ -1,4 +1,4 @@ -AIRSHIP_VERSION="16.11.3" +AIRSHIP_VERSION="16.12.0" Pod::Spec.new do |s| s.version = AIRSHIP_VERSION diff --git a/AirshipDebug.podspec b/AirshipDebug.podspec index 97ad30fba..7898a709b 100644 --- a/AirshipDebug.podspec +++ b/AirshipDebug.podspec @@ -1,4 +1,4 @@ -AIRSHIP_VERSION="16.11.3" +AIRSHIP_VERSION="16.12.0" Pod::Spec.new do |s| s.version = AIRSHIP_VERSION diff --git a/AirshipExtensions.podspec b/AirshipExtensions.podspec index 025fcc648..3bcf1cb49 100644 --- a/AirshipExtensions.podspec +++ b/AirshipExtensions.podspec @@ -1,4 +1,4 @@ -AIRSHIP_VERSION="16.11.3" +AIRSHIP_VERSION="16.12.0" Pod::Spec.new do |s| s.version = AIRSHIP_VERSION diff --git a/AirshipServiceExtension.podspec b/AirshipServiceExtension.podspec index 2260d5e17..12387cbc2 100644 --- a/AirshipServiceExtension.podspec +++ b/AirshipServiceExtension.podspec @@ -1,4 +1,4 @@ -AIRSHIP_VERSION="16.11.3" +AIRSHIP_VERSION="16.12.0" Pod::Spec.new do |s| s.version = AIRSHIP_VERSION diff --git a/CHANGELOG.md b/CHANGELOG.md index 285ed440c..5ca7806eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ [Migration Guides](https://github.com/urbanairship/ios-library/tree/main/Documentation/Migration) +## Version 16.12.0 June 12, 2023 +Minor release that adds `aspectRatio` to HTML and Modal IAA styles and a new config option `autoPauseInAppAutomationOnLaunch` to always pause IAA during app +init to be enabled later. + +### Changes +- Fixed channel restore from encrypted backups +- Added aspectRatio to HTML and Modal IAA styles +- Added `autoPauseInAppAutomationOnLaunch` config option + ## Version 16.11.3 March 24, 2023 Patch release that fixing Contact update merging order, improves Scene/Survey accessibility and reporting.