Skip to content

Commit a0dec6f

Browse files
committed
release: SDK 2.1.0
1 parent 72ab052 commit a0dec6f

16 files changed

+212
-44
lines changed

Package.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ let package = Package(
1818
targets: [
1919
.binaryTarget(
2020
name: "Batch",
21-
url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-2.0.2.zip",
22-
checksum: "2c8adaf4aec479d203263c02904ef7446f224d27b70df4164a5c7db5d5343f8c"
21+
url: "https://download.batch.com/sdk/ios/spm/BatchSDK-ios_spm-xcframework-2.1.0.zip",
22+
checksum: "7e91b40df3e2ce23ebf3b3589771770a9038042a8ccda6785be25e6b9c60307f"
2323
)
2424
]
2525
)

Sources/Batch/BatchCore.m

-5
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@ + (void)startWithAPIKey:(NSString *)key {
2323
[BACenterMulticastDelegate startWithAPIKey:key];
2424
}
2525

26-
// Set if Batch can try to use IDFA. Deprecated.
27-
+ (void)setUseIDFA:(BOOL)use {
28-
[BALogger publicForDomain:nil message:@"Ignoring 'setUseIDFA' API call: Batch has removed support for IDFA."];
29-
}
30-
3126
+ (void)setLoggerDelegate:(id<BatchLoggerDelegate>)loggerDelegate {
3227
[[[BACoreCenter instance] configuration] setLoggerDelegate:loggerDelegate];
3328
}

Sources/Batch/BatchProfileEditor.h

+39-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {
1414
BatchEmailSubscriptionStateUnsubscribed = 1,
1515
};
1616

17+
/// Enum defining the state of an SMS subscription
18+
typedef NS_ENUM(NSUInteger, BatchSMSSubscriptionState) {
19+
BatchSMSSubscriptionStateSubscribed = 0,
20+
BatchSMSSubscriptionStateUnsubscribed = 1,
21+
};
22+
1723
/// Provides profile attribute edition methods.
1824
///
1925
/// Once save() has been called once (or implicitly when using the editor block), you will
@@ -89,7 +95,7 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {
8995
/// Set the user email.
9096
///
9197
/// - Important: This method requires to already have a registered identifier for the user
92-
/// or to call ``BatchProfile.identify()`` method before this one.
98+
/// or to call ``BatchProfile/identify`` method before this one.
9399
/// - Parameters:
94100
/// - email: User email.
95101
/// - error Pointer to an error describing. Note that the error is only about validation and doesn't
@@ -99,10 +105,42 @@ typedef NS_ENUM(NSUInteger, BatchEmailSubscriptionState) {
99105

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

113+
/// Set the profile phone number.
114+
///
115+
/// - Important: This method requires to already have a registered identifier for the user
116+
/// or to call ``BatchProfile/identify:`` method before this one.
117+
/// - Parameters:
118+
/// - phoneNumber: A valid [E.164](https://en.wikipedia.org/wiki/E.164) formatted string. Must start with a "+" and
119+
/// not be longer than 15 digits without special characters (eg: "+33123456789"). nil to reset.
120+
/// - error Pointer to an error describing. Note that the error is only about validation and doesn't mean the value
121+
/// has been sent to the server yet.
122+
/// - Returns: A boolean indicating whether the attribute passed validation or not.
123+
///
124+
/// ## Examples:
125+
/// ```swift
126+
/// BatchProfile.identify("my_custom_user_id")
127+
/// let editor = BatchProfile.editor()
128+
/// try? editor.setPhoneNumber("+33123456789").save()
129+
/// ```
130+
/// ```objc
131+
/// [BatchProfile identify: @"my_custom_user_id"];
132+
/// BatchProfileEditor *editor = [BatchProfile editor];
133+
/// [editor setPhoneNumber:@"+33123456789" error:nil];
134+
/// ```
135+
- (BOOL)setPhoneNumber:(nullable NSString *)phoneNumber error:(NSError *_Nullable *_Nullable)error;
136+
137+
/// Set the profile SMS marketing subscription state.
138+
///
139+
/// Note that profile's subscription status is automatically set to unsubscribed when a user send a STOP message.
140+
/// - Parameters:
141+
/// - state: State of the subscription
142+
- (void)setSMSMarketingSubscriptionState:(BatchSMSSubscriptionState)state;
143+
106144
/// Set a boolean profile attribute for a key.
107145
///
108146
/// - Parameters:

Sources/Batch/BatchProfileEditor.m

+18
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,24 @@ - (void)setEmailMarketingSubscriptionState:(BatchEmailSubscriptionState)state {
9393
[_backingImpl setEmailMarketingSubscriptionState:swiftState];
9494
}
9595

96+
- (BOOL)setPhoneNumber:(nullable NSString *)phoneNumber error:(NSError **)error {
97+
INIT_AND_BLANK_ERROR_IF_NEEDED(error)
98+
ENSURE_ATTRIBUTE_VALUE_CLASS_NILABLE(phoneNumber, NSString.class)
99+
return [_backingImpl setPhoneNumber:phoneNumber error:error];
100+
}
101+
102+
- (void)setSMSMarketingSubscriptionState:(BatchSMSSubscriptionState)state {
103+
BATProfileEditorSMSSubscriptionState swiftState;
104+
switch (state) {
105+
case BatchSMSSubscriptionStateSubscribed:
106+
swiftState = BATProfileEditorSMSSubscriptionStateSubscribed;
107+
break;
108+
case BatchSMSSubscriptionStateUnsubscribed:
109+
swiftState = BATProfileEditorSMSSubscriptionStateUnsubscribed;
110+
}
111+
[_backingImpl setSMSMarketingSubscriptionState:swiftState];
112+
}
113+
96114
- (BOOL)addItemToStringArrayAttribute:(NSString *)element forKey:(NSString *)key error:(NSError **)error {
97115
INIT_AND_BLANK_ERROR_IF_NEEDED(error)
98116
ENSURE_KEY_STRING(key)

Sources/Batch/Modules/Opt Out/BAOptOut.m

-2
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ - (void)applyOptOut:(BOOL)shouldOptOut wipeData:(BOOL)wipeData {
149149

150150
- (NSMutableDictionary *)makeBaseEventData {
151151
NSMutableDictionary *data = [NSMutableDictionary new];
152-
153152
data[@"di"] = [[BAPropertiesCenter valueForShortName:@"di"] uppercaseString];
154-
data[@"idfa"] = [BAPropertiesCenter valueForShortName:@"idfa"];
155153
data[@"cus"] = [BAPropertiesCenter valueForShortName:@"cus"];
156154
data[@"tok"] = [BAPropertiesCenter valueForShortName:@"tok"];
157155
return data;

Sources/Batch/Modules/Profile/BAProfileCenter.swift

+6-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ public class BAProfileCenter: NSObject, BAProfileCenterProtocol {
6363
BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is invalid: it cannot be only made of whitespace or contain a newline.")
6464
return
6565
}
66+
67+
guard BATProfileDataValidators.isCustomIDBlocklisted(customID) == false else {
68+
BALogger.public(domain: loggerDomain, message: "Cannot identify, Custom ID is blocklisted: `\(customID)`. Please ensure you have correctly implemented the API.")
69+
return
70+
}
6671
}
6772

6873
// Compatibility
@@ -200,7 +205,7 @@ public class BAProfileCenter: NSObject, BAProfileCenterProtocol {
200205
}
201206

202207
func sendIdentifyEvent(customID: String?) {
203-
guard let installID = BatchUser.installationID else {
208+
guard let installID = BatchUser.installationID, !installID.isEmpty else {
204209
BALogger.error(domain: loggerDomain, message: "Could not track identify event: nil Installation ID")
205210
return
206211
}

Sources/Batch/Modules/Profile/BATEventAttributesSerializer.swift

+2
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ public class BATEventAttributesSerializer: NSObject {
3838
switch attributeValue.type {
3939
case .date, .string, .double, .integer, .bool:
4040
jsonAttributes[jsonKey] = attributeValue.value
41+
4142
case .URL:
4243
if let urlValue = attributeValue.value as? URL {
4344
jsonAttributes[jsonKey] = urlValue.absoluteString
4445
} else {
4546
throw BATSDKError.sdkInternal(subcode: 1, reason: "attribute isn't an URL")
4647
}
48+
4749
case .stringArray:
4850
if let arrayValue = attributeValue.value as? [String] {
4951
jsonAttributes[jsonKey] = arrayValue

Sources/Batch/Modules/Profile/BATProfileDataValidators.swift

+15-8
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,25 @@ import Foundation
1010
@objcMembers
1111
public class BATProfileDataValidators: NSObject {
1212
static let loggingDomain = "ProfileDataValidator"
13-
// \r\n\t is \s but for some reason \S doesn't validate those in a negation so we explicitly use those
14-
static let emailValidationRegexpPattern = "^[^@\\r\\n\\t]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$"
13+
14+
static let emailAddressPattern = "^[^@\\s]+@[A-z0-9\\-\\.]+\\.[A-z0-9]+$"
15+
static let phoneNumberPattern = "^\\+[0-9]{1,15}$"
1516

1617
public static let emailMaxLength = 256
1718
public static let customIDMaxLength = 1024
1819

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

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

27+
public static func isValidPhoneNumber(_ phoneNumber: String) -> Bool {
28+
let regexp = BATRegularExpression(pattern: phoneNumberPattern)
29+
return regexp.matches(phoneNumber)
30+
}
31+
2932
public static func isEmailTooLong(_ email: String) -> Bool {
3033
return email.count > emailMaxLength
3134
}
@@ -49,4 +52,8 @@ public class BATProfileDataValidators: NSObject {
4952

5053
return false
5154
}
55+
56+
public static func isCustomIDBlocklisted(_ customID: String) -> Bool {
57+
return blocklistedCustomIDs.contains(customID.lowercased())
58+
}
5259
}

Sources/Batch/Modules/Profile/BATProfileEditor.swift

+55-12
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,11 @@ import Foundation
99
fileprivate enum Maximums {
1010
static let stringArrayItems = 25
1111
static let stringLength = 64
12-
static let emailLength = 256
1312
static let urlLength = 2048
1413
}
1514

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

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

2523
var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState? { get }
2624

25+
var phoneNumber: (any BATProfileAttributeOperation)? { get }
26+
27+
var smsMarketingSubscription: BATProfileEditorSMSSubscriptionState? { get }
28+
2729
var language: (any BATProfileAttributeOperation)? { get }
2830

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

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

8687
private(set) var emailMarketingSubscription: BATProfileEditorEmailSubscriptionState?
8788

89+
private(set) var phoneNumber: (any BATProfileAttributeOperation)?
90+
91+
private(set) var smsMarketingSubscription: BATProfileEditorSMSSubscriptionState?
92+
8893
private(set) var language: (any BATProfileAttributeOperation)?
8994

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

122+
if !isProfileIdentified() {
123+
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.")
124+
}
125+
117126
if let value {
118127
let baseError = "Cannot set email address:"
119128

120-
if !canSetEmail() {
121-
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.")
122-
}
123-
124-
if value.count > Maximums.emailLength {
125-
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) address cannot be longer than \(Maximums.emailLength) characters")
129+
if BATProfileDataValidators.isEmailTooLong(value) {
130+
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) address cannot be longer than \(BATProfileDataValidators.emailMaxLength) characters")
126131
}
127132

128-
if !emailAddressRegexp.matches(value) {
133+
if !BATProfileDataValidators.isValidEmail(value) {
129134
throw BatchProfileError(code: .editorInvalidValue, reason: "\(baseError) invalid address")
130135
}
131136

@@ -145,6 +150,34 @@ public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, N
145150
}
146151
}
147152

153+
@objc
154+
public func setPhoneNumber(_ value: String?) throws {
155+
try checkIfConsumed()
156+
157+
if !isProfileIdentified() {
158+
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.")
159+
}
160+
161+
if let value {
162+
if !BATProfileDataValidators.isValidPhoneNumber(value) {
163+
throw BatchProfileError(code: .editorInvalidValue, reason: "Invalid phone number. Please make sure that the string starts with a `+` and is no longer than 15 digits.")
164+
}
165+
phoneNumber = BATProfileAttributeSetOperation(type: .string, value: value)
166+
} else {
167+
phoneNumber = BATProfileAttributeDeleteOperation()
168+
}
169+
}
170+
171+
@objc
172+
public func setSMSMarketingSubscriptionState(_ value: BATProfileEditorSMSSubscriptionState) {
173+
do {
174+
try checkIfConsumed()
175+
smsMarketingSubscription = value
176+
} catch {
177+
// Do nothing
178+
}
179+
}
180+
148181
@objc
149182
public func setLanguage(_ value: String?) throws {
150183
try checkIfConsumed()
@@ -393,14 +426,16 @@ public class BATProfileEditor: NSObject, BATSerializableProfileEditorProtocol, N
393426
let copy = BATProfileEditor()
394427
copy.email = self.email
395428
copy.emailMarketingSubscription = self.emailMarketingSubscription
429+
copy.phoneNumber = self.phoneNumber
430+
copy.smsMarketingSubscription = self.smsMarketingSubscription
396431
copy.language = self.language
397432
copy.region = self.region
398433
copy.customAttributes = self.customAttributes
399434
return copy
400435
}
401436

402-
func canSetEmail() -> Bool {
403-
// We can only set an email if the user is logged in
437+
func isProfileIdentified() -> Bool {
438+
// We can only set an email or a phone number if the user is logged in
404439
// This method is exposed for testing purposes
405440
return BAUserProfile.default().customIdentifier != nil
406441
}
@@ -478,3 +513,11 @@ public enum BATProfileEditorEmailSubscriptionState: UInt {
478513
case subscribed = 0
479514
case unsubscribed = 1
480515
}
516+
517+
/// SMS subscription state. This is already defined in BatchProfile.h, but we cannot reexpose
518+
/// an @objc method with a parameter from a public header, as this creates an import loop.
519+
@objc
520+
public enum BATProfileEditorSMSSubscriptionState: UInt {
521+
case subscribed = 0
522+
case unsubscribed = 1
523+
}

Sources/Batch/Modules/Profile/BATProfileOperationsSerializer.swift

+15
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ class BATProfileOperationsSerializer: NSObject {
2828
jsonParameters["email_marketing"] = serializedValue
2929
}
3030

31+
if let phoneNumber = profileEditor.phoneNumber {
32+
jsonParameters["phone_number"] = phoneNumber.value
33+
}
34+
35+
if let smsMarketingSubscription = profileEditor.smsMarketingSubscription {
36+
let serializedValue: String
37+
switch smsMarketingSubscription {
38+
case .subscribed:
39+
serializedValue = "subscribed"
40+
case .unsubscribed:
41+
serializedValue = "unsubscribed"
42+
}
43+
jsonParameters["sms_marketing"] = serializedValue
44+
}
45+
3146
if let language = profileEditor.language {
3247
jsonParameters["language"] = language.value
3348
}

Sources/Batch/Versions.h

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@
1111
Comments should not use the // form, as the plist preprocessor will include them
1212
*/
1313

14-
#define BASDKVersion 2.0.2
15-
#define BAAPILevel 200
14+
#define BASDKVersion 2.1.0
15+
#define BAAPILevel 210
1616
#define BAMessagingAPILevel 12

Sources/batchTests/Modules/Messaging/Webview/webviewBridgeLegacyTests.swift

+9-3
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,13 @@ fileprivate class MockWKWebView: WKWebView, MockDelegate {
194194
return mock
195195
}
196196

197-
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
198-
mock.call(javaScriptString, completionHandler)
199-
}
197+
#if compiler(>=6.0)
198+
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: (@MainActor @Sendable (Any?, (any Error)?) -> Void)? = nil) {
199+
mock.call(javaScriptString, completionHandler)
200+
}
201+
#else
202+
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
203+
mock.call(javaScriptString, completionHandler)
204+
}
205+
#endif
200206
}

Sources/batchTests/Modules/Profile/TestProfileEditor.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import Foundation
1010

1111
/// A test BATProfileEditor that has a controllable canSetEmail
1212
class TestProfileEditor: BATProfileEditor {
13-
public var test_canSetEmail = true
13+
public var test_isProfileIdentified = true
1414

15-
override func canSetEmail() -> Bool {
16-
return test_canSetEmail
15+
override func isProfileIdentified() -> Bool {
16+
return test_isProfileIdentified
1717
}
1818
}

0 commit comments

Comments
 (0)