Skip to content

Commit

Permalink
release: SDK 2.1.0
Browse files Browse the repository at this point in the history
  • Loading branch information
arnaud-roland committed Oct 29, 2024
1 parent 72ab052 commit a0dec6f
Show file tree
Hide file tree
Showing 16 changed files with 212 additions and 44 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ let package = Package(
targets: [
.binaryTarget(
name: "Batch",
url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-2.0.2.zip",
checksum: "2c8adaf4aec479d203263c02904ef7446f224d27b70df4164a5c7db5d5343f8c"
url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-2.1.0.zip",
checksum: "7e91b40df3e2ce23ebf3b3589771770a9038042a8ccda6785be25e6b9c60307f"
)
]
)
5 changes: 0 additions & 5 deletions Sources/Batch/BatchCore.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ + (void)startWithAPIKey:(NSString *)key {
[BACenterMulticastDelegate startWithAPIKey:key];
}

// Set if Batch can try to use IDFA. Deprecated.
+ (void)setUseIDFA:(BOOL)use {
[BALogger publicForDomain:nil message:@"Ignoring 'setUseIDFA' API call: Batch has removed support for IDFA."];
}

+ (void)setLoggerDelegate:(id<BatchLoggerDelegate>)loggerDelegate {
[[[BACoreCenter instance] configuration] setLoggerDelegate:loggerDelegate];
}
Expand Down
40 changes: 39 additions & 1 deletion Sources/Batch/BatchProfileEditor.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {
BatchEmailSubscriptionStateUnsubscribed = 1,
};

/// Enum defining the state of an SMS subscription
typedef NS_ENUM(NSUInteger, BatchSMSSubscriptionState) {
BatchSMSSubscriptionStateSubscribed = 0,
BatchSMSSubscriptionStateUnsubscribed = 1,
};

/// Provides profile attribute edition methods.
///
/// Once save() has been called once (or implicitly when using the editor block), you will
Expand Down Expand Up @@ -89,7 +95,7 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {
/// Set the user email.
///
/// - Important: This method requires to already have a registered identifier for the user
/// or to call ``BatchProfile.identify()`` method before this one.
/// or to call ``BatchProfile/identify`` method before this one.
/// - Parameters:
/// - email: User email.
/// - error Pointer to an error describing. Note that the error is only about validation and doesn't
Expand All @@ -99,10 +105,42 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {

/// Set the user email subscription state.
///
/// Note that profile's subscription status is automatically set to unsubscribed when a user click an unsubscribe link.
/// - Parameters:
/// - state: Subscription state
- (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state;

/// Set the profile phone number.
///
/// - Important: This method requires to already have a registered identifier for the user
/// or to call ``BatchProfile/identify:`` method before this one.
/// - Parameters:
/// - phoneNumber: A valid [E.164](https://en.wikipedia.org/wiki/E.164) formatted string. Must start with a "+" and
/// not be longer than 15 digits without special characters (eg: "+33123456789"). nil to reset.
/// - error Pointer to an error describing. Note that the error is only about validation and doesn't mean the value
/// has been sent to the server yet.
/// - Returns: A boolean indicating whether the attribute passed validation or not.
///
/// ## Examples:
/// ```swift
/// BatchProfile.identify("my_custom_user_id")
/// let editor = BatchProfile.editor()
/// try? editor.setPhoneNumber("+33123456789").save()
/// ```
/// ```objc
/// [BatchProfile identify: @"my_custom_user_id"];
/// BatchProfileEditor *editor = [BatchProfile editor];
/// [editor setPhoneNumber:@"+33123456789" error:nil];
/// ```
- (BOOL)setPhoneNumber:(nullable NSString *)phoneNumber error:(NSError *_Nullable *_Nullable)error;

/// Set the profile SMS marketing subscription state.
///
/// Note that profile's subscription status is automatically set to unsubscribed when a user send a STOP message.
/// - Parameters:
/// - state: State of the subscription
- (void)setSMSMarketingSubscriptionState:(BatchSMSSubscriptionState)state;

/// Set a boolean profile attribute for a key.
///
/// - Parameters:
Expand Down
18 changes: 18 additions & 0 deletions Sources/Batch/BatchProfileEditor.m
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,24 @@ - (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state {
[_backingImpl setEmailMarketingSubscriptionState:swiftState];
}

- (BOOL)setPhoneNumber:(nullable NSString *)phoneNumber error:(NSError **)error {
INIT_AND_BLANK_ERROR_IF_NEEDED(error)
ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(phoneNumber, NSString.class)
return [_backingImpl setPhoneNumber:phoneNumber error:error];
}

- (void)setSMSMarketingSubscriptionState:(BatchSMSSubscriptionState)state {
BATProfileEditorSMSSubscriptionState swiftState;
switch (state) {
case BatchSMSSubscriptionStateSubscribed:
swiftState = BATProfileEditorSMSSubscriptionStateSubscribed;
break;
case BatchSMSSubscriptionStateUnsubscribed:
swiftState = BATProfileEditorSMSSubscriptionStateUnsubscribed;
}
[_backingImpl setSMSMarketingSubscriptionState:swiftState];
}

- (BOOL)addItemToStringArrayAttribute:(NSString *)element forKey:(NSString *)key error:(NSError **)error {
INIT_AND_BLANK_ERROR_IF_NEEDED(error)
ENSURE_KEY_STRING(key)
Expand Down
2 changes: 0 additions & 2 deletions Sources/Batch/Modules/Opt Out/BAOptOut.m
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,7 @@ - (void)applyOptOut:(BOOL)shouldOptOut wipeData:(BOOL)wipeData {

- (NSMutableDictionary *)makeBaseEventData {
NSMutableDictionary *data = [NSMutableDictionary new];

data[@"di"] = [[BAPropertiesCenter valueForShortName:@"di"] uppercaseString];
data[@"idfa"] = [BAPropertiesCenter valueForShortName:@"idfa"];
data[@"cus"] = [BAPropertiesCenter valueForShortName:@"cus"];
data[@"tok"] = [BAPropertiesCenter valueForShortName:@"tok"];
return data;
Expand Down
7 changes: 6 additions & 1 deletion Sources/Batch/Modules/Profile/BAProfileCenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ public class BAProfileCenter: NSObject, BAProfileCenterProtocol {
BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is invalid: it cannot be only made of whitespace or contain a newline.")
return
}

guard BATProfileDataValidators.isCustomIDBlocklisted(customID) == false else {
BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is blocklisted: `\(customID)`. Please ensure you have correctly implemented the API.")
return
}
}

// Compatibility
Expand Down Expand Up @@ -200,7 +205,7 @@ public class BAProfileCenter: NSObject, BAProfileCenterProtocol {
}

func sendIdentifyEvent(customID: String?) {
guard let installID = BatchUser.installationID else {
guard let installID = BatchUser.installationID, !installID.isEmpty else {
BALogger.error(domain: loggerDomain, message: "Could not track identify event: nil Installation ID")
return
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ public class BATEventAttributesSerializer: NSObject {
switch attributeValue.type {
case .date, .string, .double, .integer, .bool:
jsonAttributes[jsonKey] = attributeValue.value

case .URL:
if let urlValue = attributeValue.value as? URL {
jsonAttributes[jsonKey] = urlValue.absoluteString
} else {
throw BATSDKError.sdkInternal(subcode: 1, reason: "attribute isn't an URL")
}

case .stringArray:
if let arrayValue = attributeValue.value as? [String] {
jsonAttributes[jsonKey] = arrayValue
Expand Down
23 changes: 15 additions & 8 deletions Sources/Batch/Modules/Profile/BATProfileDataValidators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ import Foundation
@objcMembers
public class BATProfileDataValidators: NSObject {
static let loggingDomain = "ProfileDataValidator"
// \r\n\t is \s but for some reason \S doesn't validate those in a negation so we explicitly use those
static let emailValidationRegexpPattern = "^[^@\\r\\n\\t]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$"

static let emailAddressPattern = "^[^@\\s]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$"
static let phoneNumberPattern = "^\\+[0-9]{1,15}$"

public static let emailMaxLength = 256
public static let customIDMaxLength = 1024

public static func isValidEmail(_ email: String) -> Bool {
let regexp = BATRegularExpression(pattern: emailValidationRegexpPattern)
guard regexp.regexpFailedToInitialize == false else {
BALogger.debug(domain: loggingDomain, message: "Email regexp unavailable")
return false
}
static let blocklistedCustomIDs = ["undefined", "null", "nil", "(null)", "[object object]", "true", "false", "nan", "infinity", "-infinity"]

public static func isValidEmail(_ email: String) -> Bool {
let regexp = BATRegularExpression(pattern: emailAddressPattern)
return regexp.matches(email)
}

public static func isValidPhoneNumber(_ phoneNumber: String) -> Bool {
let regexp = BATRegularExpression(pattern: phoneNumberPattern)
return regexp.matches(phoneNumber)
}

public static func isEmailTooLong(_ email: String) -> Bool {
return email.count > emailMaxLength
}
Expand All @@ -49,4 +52,8 @@ public class BATProfileDataValidators: NSObject {

return false
}

public static func isCustomIDBlocklisted(_ customID: String) -> Bool {
return blocklistedCustomIDs.contains(customID.lowercased())
}
}
67 changes: 55 additions & 12 deletions Sources/Batch/Modules/Profile/BATProfileEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import Foundation
fileprivate enum Maximums {
static let stringArrayItems = 25
static let stringLength = 64
static let emailLength = 256
static let urlLength = 2048
}

fileprivate enum Consts {
static let attributeNamePattern = "^[a-zA-Z0-9_]{1,30}$"
static let emailAddressPattern = "^[^@\\s]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$"
}

/// Protocol that exposes BATProfileEditor's state, so that it can be serialized
Expand All @@ -24,6 +22,10 @@ protocol BATSerializableProfileEditorProtocol {

var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState? { get }

var phoneNumber: (any BATProfileAttributeOperation)? { get }

var smsMarketingSubscription: BATProfileEditorSMSSubscriptionState? { get }

var language: (any BATProfileAttributeOperation)? { get }

var region: (any BATProfileAttributeOperation)? { get }
Expand Down Expand Up @@ -79,12 +81,15 @@ public protocol BATInstallDataEditorCompatibilityProtocol {
@objc
public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, NSCopying {
private let attributeNameRegexp: BATRegularExpression = .init(pattern: Consts.attributeNamePattern)
private let emailAddressRegexp: BATRegularExpression = .init(pattern: Consts.emailAddressPattern)

private(set) var email: (any BATProfileAttributeOperation)?

private(set) var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState?

private(set) var phoneNumber: (any BATProfileAttributeOperation)?

private(set) var smsMarketingSubscription: BATProfileEditorSMSSubscriptionState?

private(set) var language: (any BATProfileAttributeOperation)?

private(set) var region: (any BATProfileAttributeOperation)?
Expand Down Expand Up @@ -114,18 +119,18 @@ public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, N
public func setEmail(_ value: String?) throws {
try checkIfConsumed()

if !isProfileIdentified() {
throw BatchProfileError(code: .editorInvalidValue, reason: "Emails cannot be set on a profile if it has not been identified first. Please call 'BatchProfile.idenfity()' with a non nil value beforehand.")
}

if let value {
let baseError = "Cannot set email address:"

if !canSetEmail() {
throw BatchProfileError(code: .editorInvalidValue, reason: "Emails cannot be set on a profile if it has not been identified first. Please call 'BatchProfile.idenfity()' with a non nil value beforehand.")
}

if value.count > Maximums.emailLength {
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) address cannot be longer than \(Maximums.emailLength) characters")
if BATProfileDataValidators.isEmailTooLong(value) {
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) address cannot be longer than \(BATProfileDataValidators.emailMaxLength) characters")
}

if !emailAddressRegexp.matches(value) {
if !BATProfileDataValidators.isValidEmail(value) {
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) invalid address")
}

Expand All @@ -145,6 +150,34 @@ public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, N
}
}

@objc
public func setPhoneNumber(_ value: String?) throws {
try checkIfConsumed()

if !isProfileIdentified() {
throw BatchProfileError(code: .editorInvalidValue, reason: "Phone number cannot be set on a profile if it has not been identified first. Please call 'BatchProfile.idenfity()' with a non nil value beforehand.")
}

if let value {
if !BATProfileDataValidators.isValidPhoneNumber(value) {
throw BatchProfileError(code: .editorInvalidValue, reason: "Invalid phone number. Please make sure that the string starts with a `+` and is no longer than 15 digits.")
}
phoneNumber = BATProfileAttributeSetOperation(type: .string, value: value)
} else {
phoneNumber = BATProfileAttributeDeleteOperation()
}
}

@objc
public func setSMSMarketingSubscriptionState(_ value: BATProfileEditorSMSSubscriptionState) {
do {
try checkIfConsumed()
smsMarketingSubscription = value
} catch {
// Do nothing
}
}

@objc
public func setLanguage(_ value: String?) throws {
try checkIfConsumed()
Expand Down Expand Up @@ -393,14 +426,16 @@ public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, N
let copy = BATProfileEditor()
copy.email = self.email
copy.emailMarketingSubscription = self.emailMarketingSubscription
copy.phoneNumber = self.phoneNumber
copy.smsMarketingSubscription = self.smsMarketingSubscription
copy.language = self.language
copy.region = self.region
copy.customAttributes = self.customAttributes
return copy
}

func canSetEmail() -> Bool {
// We can only set an email if the user is logged in
func isProfileIdentified() -> Bool {
// We can only set an email or a phone number if the user is logged in
// This method is exposed for testing purposes
return BAUserProfile.default().customIdentifier != nil
}
Expand Down Expand Up @@ -478,3 +513,11 @@ public enum BATProfileEditorEmailSubscriptionState: UInt {
case subscribed = 0
case unsubscribed = 1
}

/// SMS subscription state. This is already defined in BatchProfile.h, but we cannot reexpose
/// an @objc method with a parameter from a public header, as this creates an import loop.
@objc
public enum BATProfileEditorSMSSubscriptionState: UInt {
case subscribed = 0
case unsubscribed = 1
}
15 changes: 15 additions & 0 deletions Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ class BATProfileOperationsSerializer: NSObject {
jsonParameters["email_marketing"] = serializedValue
}

if let phoneNumber = profileEditor.phoneNumber {
jsonParameters["phone_number"] = phoneNumber.value
}

if let smsMarketingSubscription = profileEditor.smsMarketingSubscription {
let serializedValue: String
switch smsMarketingSubscription {
case .subscribed:
serializedValue = "subscribed"
case .unsubscribed:
serializedValue = "unsubscribed"
}
jsonParameters["sms_marketing"] = serializedValue
}

if let language = profileEditor.language {
jsonParameters["language"] = language.value
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/Batch/Versions.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
Comments should not use the // form, as the plist preprocessor will include them
*/

#define BASDKVersion 2.0.2
#define BAAPILevel 200
#define BASDKVersion 2.1.0
#define BAAPILevel 210
#define BAMessagingAPILevel 12
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,13 @@ fileprivate class MockWKWebView: WKWebView, MockDelegate {
return mock
}

override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
mock.call(javaScriptString, completionHandler)
}
#if compiler(>=6.0)
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: (@MainActor @Sendable (Any?, (any Error)?) -> Void)? = nil) {
mock.call(javaScriptString, completionHandler)
}
#else
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
mock.call(javaScriptString, completionHandler)
}
#endif
}
6 changes: 3 additions & 3 deletions Sources/batchTests/Modules/Profile/TestProfileEditor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import Foundation

/// A test BATProfileEditor that has a controllable canSetEmail
class TestProfileEditor: BATProfileEditor {
public var test_canSetEmail = true
public var test_isProfileIdentified = true

override func canSetEmail() -> Bool {
return test_canSetEmail
override func isProfileIdentified() -> Bool {
return test_isProfileIdentified
}
}
Loading

0 comments on commit a0dec6f

Please sign in to comment.