diff --git a/BrazeKit.podspec b/BrazeKit.podspec index e0bbcf2749..4decb224e5 100644 --- a/BrazeKit.podspec +++ b/BrazeKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeKit' - s.version = '5.7.0' + s.version = '5.8.0' s.summary = 'Braze Main SDK library providing support for analytics and push notifications.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeKit-CocoaPods.zip', - :sha256 => 'dcef97f42830884252cecc2a46d863e7ed231e7b3d8cc734d35e849c57628415' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeKit-CocoaPods.zip', + :sha256 => 'bdf8cdde28e3dbbd0f3f089781e799a1922e5f6ee4f8299d4b8418ef053d8580' } s.swift_version = '5.0' @@ -19,4 +19,6 @@ Pod::Spec.new do |s| s.vendored_framework = 'BrazeKit.xcframework' s.resource_bundles = { 'BrazeKit' => 'Sources/BrazeKitResources/Resources/**/*' } + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeKitCompat.podspec b/BrazeKitCompat.podspec new file mode 100644 index 0000000000..15983ce4f1 --- /dev/null +++ b/BrazeKitCompat.podspec @@ -0,0 +1,25 @@ +Pod::Spec.new do |s| + s.name = 'BrazeKitCompat' + s.version = '5.8.0' + s.summary = 'Compatibility library for users migrating from AppboyKit.' + + s.homepage = 'https://braze.com' + s.documentation_url = 'https://braze-inc.github.io/braze-swift-sdk/documentation/brazekitcompat/' + s.license = { :type => 'Commercial' } + s.authors = 'Braze, Inc.' + + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.8.0' } + + s.swift_version = '5.0' + s.ios.deployment_target = '10.0' + s.tvos.deployment_target = '10.0' + s.static_framework = true + + s.source_files = 'Sources/BrazeKitCompat/**/*.{h,m}' +s.public_header_files = 'Sources/BrazeKitCompat/include/*.h' + + s.dependency 'BrazeKit', '5.8.0' + s.dependency 'BrazeLocation', '5.8.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/BrazeLocation.podspec b/BrazeLocation.podspec index 8743405de4..db08960059 100644 --- a/BrazeLocation.podspec +++ b/BrazeLocation.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeLocation' - s.version = '5.7.0' + s.version = '5.8.0' s.summary = 'Braze location library providing support for location analytics and geofence monitoring.' s.homepage = 'https://braze.com' @@ -9,8 +9,8 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeLocation-CocoaPods.zip', - :sha256 => '66372a30285e585189d20f20da3d602dd50f9541daee7bffd0b2729e28a19996' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeLocation-CocoaPods.zip', + :sha256 => '8d9b38a7df9714df3dcc2955de5914d6edef1e01852688ba0246498002a63318' } s.swift_version = '5.0' @@ -21,5 +21,7 @@ Pod::Spec.new do |s| # Depends on BrazeKit because BrazeKit includes the internal _BrazeLocationClient symbols required # for linking against BrazeLocation. - s.dependency 'BrazeKit', '5.7.0' + s.dependency 'BrazeKit', '5.8.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeNotificationService.podspec b/BrazeNotificationService.podspec index b1d7834712..a83f6a4a52 100644 --- a/BrazeNotificationService.podspec +++ b/BrazeNotificationService.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazeNotificationService' - s.version = '5.7.0' + s.version = '5.8.0' s.summary = 'Braze notification service extension library providing support for Rich Push notifications.' s.homepage = 'https://braze.com' @@ -9,12 +9,14 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeNotificationService-CocoaPods.zip', - :sha256 => '335ce992d5a53b0efc79097ea77ccf092ff92013d9a7e58d02ef7f86763b8c70' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeNotificationService-CocoaPods.zip', + :sha256 => '3b61399b20b56c58ee897b3a0c80cc22d002f0bbc0369152352e5bc9d4691a17' } s.swift_version = '5.0' s.ios.deployment_target = '10.0' s.vendored_framework = 'BrazeNotificationService.xcframework' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazePushStory.podspec b/BrazePushStory.podspec index 39fe054fa3..b4f8ccc2ce 100644 --- a/BrazePushStory.podspec +++ b/BrazePushStory.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'BrazePushStory' - s.version = '5.7.0' + s.version = '5.8.0' s.summary = 'Braze notification content extension library providing support for Push Stories.' s.homepage = 'https://braze.com' @@ -9,12 +9,14 @@ Pod::Spec.new do |s| s.authors = 'Braze, Inc.' s.source = { - :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazePushStory-CocoaPods.zip', - :sha256 => '741f055974e9e4fb70810ba716fe2177ad486bd7b7d32779a65087a6055bb597' + :http => 'https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazePushStory-CocoaPods.zip', + :sha256 => 'e9a98b6a947d221b679e3a073670621d30518b1d93fdcf6ae4257526ada13a08' } s.swift_version = '5.0' s.ios.deployment_target = '10.0' s.vendored_framework = 'BrazePushStory.xcframework' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeUI.podspec b/BrazeUI.podspec index 6a3835c699..13ef7b71a9 100644 --- a/BrazeUI.podspec +++ b/BrazeUI.podspec @@ -1,14 +1,14 @@ Pod::Spec.new do |s| s.name = 'BrazeUI' - s.version = '5.7.0' - s.summary = 'Braze-provided user interface library for In-App Messages.' + s.version = '5.8.0' + s.summary = 'Braze-provided user interface library for In-App Messages and Content Cards.' s.homepage = 'https://braze.com' s.documentation_url = 'https://braze-inc.github.io/braze-swift-sdk/documentation/brazeui/' s.license = { :type => 'Commercial' } s.authors = 'Braze, Inc.' - s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.7.0' } + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.8.0' } s.swift_version = '5.0' s.ios.deployment_target = '10.0' @@ -17,5 +17,7 @@ Pod::Spec.new do |s| s.source_files = 'Sources/BrazeUI/**/*.swift' s.resource_bundles = { 'BrazeUI' => 'Sources/BrazeUI/Resources/**/*' } - s.dependency 'BrazeKit', '5.7.0' + s.dependency 'BrazeKit', '5.8.0' + + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/BrazeUICompat.podspec b/BrazeUICompat.podspec new file mode 100644 index 0000000000..d3daa5dd38 --- /dev/null +++ b/BrazeUICompat.podspec @@ -0,0 +1,29 @@ +Pod::Spec.new do |s| + s.name = 'BrazeUICompat' + s.version = '5.8.0' + s.summary = 'Compatibility UI library for users migrating from AppboyUI.' + + s.homepage = 'https://braze.com' + s.documentation_url = 'https://braze-inc.github.io/braze-swift-sdk/documentation/brazeui/' + s.license = { :type => 'Commercial' } + s.authors = 'Braze, Inc.' + + s.source = { :git => 'https://github.com/braze-inc/braze-swift-sdk.git', :tag => '5.8.0' } + + s.swift_version = '5.0' + s.ios.deployment_target = '10.0' + s.static_framework = true + + s.source_files = 'Sources/BrazeUICompat/ABK*/**/*.{h,m}' +s.public_header_files = 'Sources/BrazeUICompat/ABK*/**/*.h' + s.resource_bundles = { 'BrazeUICompat' => 'Sources/BrazeUICompat/*/Resources/**/*.*' } + + s.dependency 'BrazeKitCompat', '5.8.0' + s.dependency 'SDWebImage', '>= 5.8.2', '< 6' + + s.user_target_xcconfig = { 'CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES' => 'YES' } + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'OTHER_CFLAGS' => '-Wno-deprecated-declarations -Wno-deprecated-implementations' + } +end diff --git a/CHANGELOG.md b/CHANGELOG.md index b32a292dc4..b2b6f8f64b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## 5.8.0 + +To help migrate your app from the Appboy-iOS-SDK to our Swift SDK, this release includes the `Appboy-iOS-SDK` [migration guide]: +- Follow step-by-step instructions to migrate each feature to the new APIs. +- Includes instructions for a minimal migration scenario via our compatibility libraries. + +##### Added +- Adds compatibility libraries `BrazeKitCompat` and `BrazeUICompat`: + - Provides all the old APIs from `Appboy-iOS-SDK` to easily start migrating to the Swift SDK. + - See the [migration guide] for more details. +- Adds support for [News Feed](https://www.braze.com/docs/user_guide/engagement_tools/news_feed) data models and analytics. + - News Feed UI is not supported by the Swift SDK. See the [migration guide] for instructions on using the compatibility UI. + +[migration guide]: https://braze-inc.github.io/braze-swift-sdk/documentation/braze/appboy-migration-guide + ## 5.7.0 ##### Fixed @@ -14,6 +29,17 @@ ## 5.6.4 +##### End of early access phase / Migration Guide / Compatibility Libraries + +This release marks the end of the early access phase for the Braze Swift SDK. `Appboy-iOS-SDK` is now deprecated and support will cease on . + +To help with your migration process, this release includes: +- [Appboy-iOS-SDK: Migration guide]: + - Follow step-by-step instructions to migrate each feature to the new APIs. + - Includes instructions for minimal migration scenario via our compatibility libraries. +- Compatibility libraries `BrazeKitCompat` and `BrazeUICompat`: + - Provides all the old APIs from `Appboy-iOS-SDK` to easily start migrating to the Swift SDK. + ##### Fixed - Fixes an issue preventing the execution of `BrazeDelegate` methods when setting the delegate using Objective-C. - Fixes an issue where triggering an in-app message twice with the same event did not place the message on the in-app message stack under certain conditions. @@ -27,7 +53,8 @@ - [`Braze.deviceId()`] - [`ContentCards.requestRefresh()`] - [`ContentCards.cardsStream`] as an alternative to [`ContentCards.subscribeToUpdates(_:)`] - + +[Appboy-iOS-SDK: Migration guide]: https://braze-inc.github.io/braze-swift-sdk/documentation/braze/appboy-migration-guide [`Braze.User.id()`]: https://braze-inc.github.io/braze-swift-sdk/documentation/brazekit/braze/user-swift.class/id() [`Braze.deviceId()`]: https://braze-inc.github.io/braze-swift-sdk/documentation/brazekit/braze/deviceid() [`ContentCards.requestRefresh()`]: https://braze-inc.github.io/braze-swift-sdk/documentation/brazekit/braze/contentcards-swift.class/requestrefresh() @@ -205,6 +232,7 @@ The following features are not supported yet: - Added in [5.5.0](#550) - ~~tvOS integration~~ - Added in [5.3.0](#530) -- News Feed +- ~~News Feed~~ + - Added in [5.7.0](#570) - ~~Content Cards~~ - Added in [5.2.0](#520) diff --git a/Examples/Swift/Package.swift b/Examples/Swift/Package.swift index e92d15f7f9..0d23aed0f0 100644 --- a/Examples/Swift/Package.swift +++ b/Examples/Swift/Package.swift @@ -1,3 +1,3 @@ -// This Package.swift is intentionally empty. It exists for the sole purpose of preventing Xcode -// from recursively including the Examples project sources when locally referencing the Braze SDK +// This Package.swift is intentionally empty. It exists for the sole purpose of preventing Xcode +// from recursively including the Examples project sources when locally referencing the Braze SDK // SwiftPM package. diff --git a/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift b/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift index 221a10c726..0fc6a02548 100644 --- a/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift +++ b/Examples/Swift/Sources/ContentCardUI-Customization/AppDelegate.swift @@ -28,15 +28,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Customizations -#warning( - """ - For demonstration purposes, this example application uses an alternate Content Card view controller initializer. +#warning(""" +For demonstration purposes, this example application uses an alternate Content Card view controller initializer. - In your implementation, you are expected to use the standard `init(braze:attributes:)` initializer to automatically link the UI to your braze instance. +In your implementation, you are expected to use the standard `init(braze:attributes:)` initializer to automatically link the UI to your braze instance. - See https://braze-inc.github.io/braze-swift-sdk/documentation/brazeui/brazecontentcardui/viewcontroller/init(braze:attributes:) - """ -) +See https://braze-inc.github.io/braze-swift-sdk/documentation/brazeui/brazecontentcardui/viewcontroller/init(braze:attributes:) +""") extension AppDelegate { @@ -103,13 +101,17 @@ extension AppDelegate { let headerCard: Braze.ContentCard = .banner( .init( data: .init(viewed: true), - image: .mockImage(width: 1200, height: 675, text: "Static header card", backgroundColor: .systemOrange, drawCorners: false) + image: .mockImage( + width: 1200, height: 675, text: "Static header card", backgroundColor: .systemOrange, + drawCorners: false) ) ) let footerCard: Braze.ContentCard = .banner( .init( data: .init(viewed: true), - image: .mockImage(width: 1200, height: 675, text: "Static footer card", backgroundColor: .systemOrange, drawCorners: false) + image: .mockImage( + width: 1200, height: 675, text: "Static footer card", backgroundColor: .systemOrange, + drawCorners: false) ) ) @@ -199,10 +201,9 @@ private var cards: [Braze.ContentCard] = [ classic, classicImage, banner, - captionedImage + captionedImage, ] private var navigationController: UINavigationController { UIApplication.shared.delegate!.window!!.rootViewController! as! UINavigationController } - diff --git a/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift b/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift index 4bfd407923..712d834326 100644 --- a/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift +++ b/Examples/Swift/Sources/ContentCardUI-Customization/Readme.swift @@ -56,7 +56,8 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ let classicPinned: Braze.ContentCard = withContext( .classic( .init( - data: .init(clickAction: .url(URL(string: "https://example.com")!, useWebView: true), pinned: true), + data: .init( + clickAction: .url(URL(string: "https://example.com")!, useWebView: true), pinned: true), title: "Classic (pinned)", description: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", domain: "example.com" diff --git a/Examples/Swift/Sources/ContentCardUI-Customization/Subclasses/CustomClassicImageCell.swift b/Examples/Swift/Sources/ContentCardUI-Customization/Subclasses/CustomClassicImageCell.swift index 47e8374932..c8db0e2189 100644 --- a/Examples/Swift/Sources/ContentCardUI-Customization/Subclasses/CustomClassicImageCell.swift +++ b/Examples/Swift/Sources/ContentCardUI-Customization/Subclasses/CustomClassicImageCell.swift @@ -1,6 +1,6 @@ -import UIKit import BrazeKit import BrazeUI +import UIKit /// A ClassicImageCell subclass which move the image to the trailing part of the cell, make it take /// the full height of the cell. @@ -29,7 +29,8 @@ class CustomClassicImageCell: BrazeContentCardUI.ClassicImageCell { // Use the original left padding for the text stack + support RTL languages textStack?.isLayoutMarginsRelativeArrangement = true - let isRightToLeft = UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft + let isRightToLeft = + UIView.userInterfaceLayoutDirection(for: semanticContentAttribute) == .rightToLeft textStack?.layoutMargins = UIEdgeInsets( top: 0, left: isRightToLeft ? 0 : attributes.padding.left, diff --git a/Examples/Swift/Sources/ContentCards-Custom-UI/AppDelegate.swift b/Examples/Swift/Sources/ContentCards-Custom-UI/AppDelegate.swift index 8d48125650..7471483a48 100644 --- a/Examples/Swift/Sources/ContentCards-Custom-UI/AppDelegate.swift +++ b/Examples/Swift/Sources/ContentCards-Custom-UI/AppDelegate.swift @@ -69,7 +69,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // A wrapper / compatibility representation of the card is accessible via `.json()` if let jsonData = card.json(), - let jsonString = String(data: jsonData, encoding: .utf8) { + let jsonString = String(data: jsonData, encoding: .utf8) + { print(jsonString) } @@ -109,7 +110,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // A wrapper / compatibility representation of the card is accessible via `.json()` if let jsonData = cardRaw.json(), - let jsonString = String(data: jsonData, encoding: .utf8) { + let jsonString = String(data: jsonData, encoding: .utf8) + { print(jsonString) } diff --git a/Examples/Swift/Sources/ContentCards-Custom-UI/CardsInfoViewController.swift b/Examples/Swift/Sources/ContentCards-Custom-UI/CardsInfoViewController.swift index d8a74641ed..e9fa710571 100644 --- a/Examples/Swift/Sources/ContentCards-Custom-UI/CardsInfoViewController.swift +++ b/Examples/Swift/Sources/ContentCards-Custom-UI/CardsInfoViewController.swift @@ -1,5 +1,5 @@ -import UIKit import BrazeKit +import UIKit final class CardsInfoViewController: UITableViewController { @@ -26,7 +26,8 @@ final class CardsInfoViewController: UITableViewController { sections = cards.enumerated().map { Self.cardSection(from: $1, index: $0) } super.init(style: .grouped) title = "Content Cards Info" - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissModal)) + let doneButton = UIBarButtonItem( + barButtonSystemItem: .done, target: self, action: #selector(dismissModal)) navigationItem.setRightBarButton(doneButton, animated: false) tableView.rowHeight = UITableView.automaticDimension @@ -62,7 +63,8 @@ final class CardsInfoViewController: UITableViewController { _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier") + let cell = + tableView.dequeueReusableCell(withIdentifier: "cellIdentifier") ?? UITableViewCell(style: .value1, reuseIdentifier: "cellIdentifier") let field = sections[indexPath.section].fields[indexPath.row] @@ -162,7 +164,7 @@ final class CardsInfoViewController: UITableViewController { header.value = "url" fields = [ Field(name: "url", value: url, indentation: indentation + 1), - Field(name: "useWebView", value: useWebView, indentation: indentation + 1) + Field(name: "useWebView", value: useWebView, indentation: indentation + 1), ] @unknown default: break @@ -171,5 +173,4 @@ final class CardsInfoViewController: UITableViewController { return [header] + fields } - } diff --git a/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift b/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift index 57aa7bd0b4..3d2b1197ac 100644 --- a/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift +++ b/Examples/Swift/Sources/ContentCards-Custom-UI/Readme.swift @@ -70,4 +70,3 @@ func cancelContentCardsUpdatesSubscription(_ viewController: ReadmeViewControlle func presentContentCardsInfoViewController(_ viewController: ReadmeViewController) { (UIApplication.shared.delegate as? AppDelegate)?.presentContentCardsInfoViewController() } - diff --git a/Examples/Swift/Sources/InAppMessageUI-Customization/AppDelegate.swift b/Examples/Swift/Sources/InAppMessageUI-Customization/AppDelegate.swift index 1122c4944d..34d9077500 100644 --- a/Examples/Swift/Sources/InAppMessageUI-Customization/AppDelegate.swift +++ b/Examples/Swift/Sources/InAppMessageUI-Customization/AppDelegate.swift @@ -105,7 +105,8 @@ extension AppDelegate: BrazeInAppMessageUIDelegate { // Modal - Custom button font if customization == "modal-button-font" { - context.attributes?.modal?.buttonsAttributes.font = UIFont(name: "AmericanTypewriter-Bold", size: 17)! + context.attributes?.modal?.buttonsAttributes.font = UIFont( + name: "AmericanTypewriter-Bold", size: 17)! } // ModalImage - Attributes @@ -157,5 +158,3 @@ extension AppDelegate: BrazeInAppMessageUIDelegate { } } - - diff --git a/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift b/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift index 3055e06309..3d176f4432 100644 --- a/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift +++ b/Examples/Swift/Sources/InAppMessageUI-Customization/Readme.swift @@ -100,39 +100,53 @@ let actions: [(String, String, (ReadmeViewController) -> Void)] = [ let slideup: Braze.InAppMessage = .slideup( .init( - graphic: .image(.mockImage(width: 200, height: 200, text: "🧁", textSize: 128, backgroundColor: .systemBlue, drawCorners: false)), + graphic: .image( + .mockImage( + width: 200, height: 200, text: "🧁", textSize: 128, backgroundColor: .systemBlue, + drawCorners: false)), message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." ) ) let modal: Braze.InAppMessage = .modal( .init( - graphic: .image(.mockImage(width: 1450, height: 500, text: "🧁", textSize: 256, backgroundColor: .systemBlue, drawCorners: false)), + graphic: .image( + .mockImage( + width: 1450, height: 500, text: "🧁", textSize: 256, backgroundColor: .systemBlue, + drawCorners: false)), header: "Hello world!", - message: "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", buttons: [.init(id: 0, text: "OK", clickAction: .none, themes: ["light": .primary])] ) ) let modalImage: Braze.InAppMessage = .modalImage( .init( - imageURL: .mockImage(width: 1200, height: 1200, text: "🧁", textSize: 512, backgroundColor: .systemBlue, drawCorners: false), + imageURL: .mockImage( + width: 1200, height: 1200, text: "🧁", textSize: 512, backgroundColor: .systemBlue, + drawCorners: false), buttons: [.init(id: 0, text: "OK", clickAction: .none, themes: ["light": .primary])] ) ) let full: Braze.InAppMessage = .full( .init( - imageURL: .mockImage(width: 1200, height: 1000, text: "🧁", textSize: 512, backgroundColor: .systemBlue, drawCorners: false), + imageURL: .mockImage( + width: 1200, height: 1000, text: "🧁", textSize: 512, backgroundColor: .systemBlue, + drawCorners: false), header: "Hello world!", - message: "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", buttons: [.init(id: 0, text: "OK", clickAction: .none, themes: ["light": .primary])] ) ) let fullImage: Braze.InAppMessage = .fullImage( .init( - imageURL: .mockImage(width: 1200, height: 2000, text: "🧁", textSize: 512, backgroundColor: .systemBlue, drawCorners: false), + imageURL: .mockImage( + width: 1200, height: 2000, text: "🧁", textSize: 512, backgroundColor: .systemBlue, + drawCorners: false), buttons: [.init(id: 0, text: "OK", clickAction: .none, themes: ["light": .primary])] ) ) diff --git a/Examples/Swift/Sources/InAppMessageUI-Customization/Subclasses/FullWidthSlideupView.swift b/Examples/Swift/Sources/InAppMessageUI-Customization/Subclasses/FullWidthSlideupView.swift index 3315285fbe..ad097afed3 100644 --- a/Examples/Swift/Sources/InAppMessageUI-Customization/Subclasses/FullWidthSlideupView.swift +++ b/Examples/Swift/Sources/InAppMessageUI-Customization/Subclasses/FullWidthSlideupView.swift @@ -43,7 +43,7 @@ class FullWidthSlideupView: BrazeInAppMessageUI.SlideupView { verticalConstraint, backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor), backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor), - backgroundView.heightAnchor.constraint(equalToConstant: 1000) + backgroundView.heightAnchor.constraint(equalToConstant: 1000), ]) applyTheme() diff --git a/Examples/Swift/Sources/InAppMessages-Custom-UI/CustomInAppMessagePresenter.swift b/Examples/Swift/Sources/InAppMessages-Custom-UI/CustomInAppMessagePresenter.swift index e7c05282cd..7b7ffb4fc7 100644 --- a/Examples/Swift/Sources/InAppMessages-Custom-UI/CustomInAppMessagePresenter.swift +++ b/Examples/Swift/Sources/InAppMessages-Custom-UI/CustomInAppMessagePresenter.swift @@ -43,7 +43,8 @@ struct CustomInAppMessagePresenter: BrazeInAppMessagePresenter { // A wrapper / compatibility representation of the in-app message is accessible via `.json()` if let jsonData = message.json(), - let jsonString = String(data: jsonData, encoding: .utf8) { + let jsonString = String(data: jsonData, encoding: .utf8) + { print(jsonString) } diff --git a/Examples/Swift/Sources/InAppMessages-Custom-UI/Extensions.swift b/Examples/Swift/Sources/InAppMessages-Custom-UI/Extensions.swift index 38e3755223..2d9c717aa2 100644 --- a/Examples/Swift/Sources/InAppMessages-Custom-UI/Extensions.swift +++ b/Examples/Swift/Sources/InAppMessages-Custom-UI/Extensions.swift @@ -1,5 +1,5 @@ -import Foundation import BrazeKit +import Foundation // MARK: - Braze @@ -27,7 +27,8 @@ extension Encodable { } guard let data = try? encoder.encode(self), - let prettyPrinted = String(data: data, encoding: .utf8) else { + let prettyPrinted = String(data: data, encoding: .utf8) + else { return "\(self)" } @@ -48,7 +49,8 @@ extension Dictionary where Key == String, Value == Any { } guard let data = try? JSONSerialization.data(withJSONObject: self, options: options), - let prettyPrinted = String(data: data, encoding: .utf8) else { + let prettyPrinted = String(data: data, encoding: .utf8) + else { return "\(self)" } diff --git a/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift b/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift index 348dad26b6..82e8b5b655 100644 --- a/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift +++ b/Examples/Swift/Sources/InAppMessages-Custom-UI/InAppMessageInfoViewController.swift @@ -1,5 +1,5 @@ -import UIKit import BrazeKit +import UIKit final class InAppMessageInfoViewController: UITableViewController { @@ -26,7 +26,8 @@ final class InAppMessageInfoViewController: UITableViewController { sections = Self.messageSections(from: message) + Self.dataSections(from: message) super.init(style: .grouped) title = "In-App Message Info" - let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissModal)) + let doneButton = UIBarButtonItem( + barButtonSystemItem: .done, target: self, action: #selector(dismissModal)) navigationItem.setRightBarButton(doneButton, animated: false) tableView.rowHeight = UITableView.automaticDimension @@ -62,7 +63,8 @@ final class InAppMessageInfoViewController: UITableViewController { _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) -> UITableViewCell { - let cell = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier") + let cell = + tableView.dequeueReusableCell(withIdentifier: "cellIdentifier") ?? UITableViewCell(style: .value1, reuseIdentifier: "cellIdentifier") let field = sections[indexPath.section].fields[indexPath.row] @@ -95,20 +97,25 @@ final class InAppMessageInfoViewController: UITableViewController { var themesSection: Section? switch message { case .slideup(let slideup): - section.fields = [ - Field(name: "type", value: "slideup"), - Field(name: "message", value: slideup.message, indentation: 1), - Field(name: "slideFrom", value: slideup.slideFrom.rawValue, indentation: 1) - ] + fields(from: slideup.graphic, indentation: 1) + section.fields = + [ + Field(name: "type", value: "slideup"), + Field(name: "message", value: slideup.message, indentation: 1), + Field(name: "slideFrom", value: slideup.slideFrom.rawValue, indentation: 1), + ] + fields(from: slideup.graphic, indentation: 1) themesSection = self.themesSection(from: slideup.themes) case .modal(let modal): - section.fields = [ - Field(name: "type", value: "modal"), - Field(name: "header", value: modal.header, indentation: 1), - Field(name: "message", value: modal.message, indentation: 1), - Field(name: "headerTextAlignment", value: modal.headerTextAlignment.rawValue, indentation: 1), - Field(name: "messageTextAlignment", value: modal.messageTextAlignment.rawValue, indentation: 1), - ] + fields(from: modal.graphic, indentation: 1) + section.fields = + [ + Field(name: "type", value: "modal"), + Field(name: "header", value: modal.header, indentation: 1), + Field(name: "message", value: modal.message, indentation: 1), + Field( + name: "headerTextAlignment", value: modal.headerTextAlignment.rawValue, indentation: 1), + Field( + name: "messageTextAlignment", value: modal.messageTextAlignment.rawValue, indentation: 1 + ), + ] + fields(from: modal.graphic, indentation: 1) buttonsSection = self.buttonsSection(from: modal.buttons) themesSection = self.themesSection(from: modal.themes) case .modalImage(let modalImage): @@ -169,7 +176,8 @@ final class InAppMessageInfoViewController: UITableViewController { Field(name: "headerTextColor", value: theme.headerTextColor.hexString, indentation: 1), Field(name: "closeButtonColor", value: theme.closeButtonColor.hexString, indentation: 1), Field(name: "iconColor", value: theme.iconColor.hexString, indentation: 1), - Field(name: "iconBackgroundColor", value: theme.iconBackgroundColor.hexString, indentation: 1), + Field( + name: "iconBackgroundColor", value: theme.iconBackgroundColor.hexString, indentation: 1), Field(name: "backgroundColor", value: theme.backgroundColor.hexString, indentation: 1), Field(name: "frameColor", value: theme.frameColor.hexString, indentation: 1), ] @@ -217,7 +225,7 @@ final class InAppMessageInfoViewController: UITableViewController { // Extras section.fields += [ Field(name: "extras", value: ""), - Field(name: message.extras.prettyPrint(), value: "", indentation: 1) + Field(name: message.extras.prettyPrint(), value: "", indentation: 1), ] return [section] @@ -237,11 +245,11 @@ final class InAppMessageInfoViewController: UITableViewController { header.value = "none" case .newsFeed: header.value = "newsFeed" - case .url(let url, useWebView: let useWebView): + case .url(let url, let useWebView): header.value = "url" fields = [ Field(name: "url", value: url, indentation: indentation + 1), - Field(name: "useWebView", value: useWebView, indentation: indentation + 1) + Field(name: "useWebView", value: useWebView, indentation: indentation + 1), ] @unknown default: break @@ -286,7 +294,9 @@ final class InAppMessageInfoViewController: UITableViewController { case .icon(let icon): header.value = "icon" fields = [ - Field(name: "icon (FontAwesome, may not render correctly here)", value: icon, indentation: indentation + 1) + Field( + name: "icon (FontAwesome, may not render correctly here)", value: icon, + indentation: indentation + 1) ] case .image(let url): header.value = "image" @@ -308,8 +318,11 @@ final class InAppMessageInfoViewController: UITableViewController { fields += [ Field(name: "theme", value: name, indentation: indentation), Field(name: "textColor", value: theme.textColor.hexString, indentation: indentation + 1), - Field(name: "backgroundColor", value: theme.backgroundColor.hexString, indentation: indentation + 1), - Field(name: "borderColor", value: theme.borderColor.hexString, indentation: indentation + 1), + Field( + name: "backgroundColor", value: theme.backgroundColor.hexString, + indentation: indentation + 1), + Field( + name: "borderColor", value: theme.borderColor.hexString, indentation: indentation + 1), ] } @@ -317,4 +330,3 @@ final class InAppMessageInfoViewController: UITableViewController { } } - diff --git a/Package.swift b/Package.swift index 9fe8e83902..4e084cd981 100644 --- a/Package.swift +++ b/Package.swift @@ -18,16 +18,19 @@ let package = Package( .library(name: "BrazeLocation", targets: ["BrazeLocation"]), .library(name: "BrazeNotificationService", targets: ["BrazeNotificationService"]), .library(name: "BrazePushStory", targets: ["BrazePushStory"]), + .library(name: "BrazeKitCompat", targets: ["BrazeKitCompat"]), + .library(name: "BrazeUICompat", targets: ["BrazeUICompat"]), ], dependencies: [ + .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.13.2"), /* ${dependencies-start} */ /* ${dependencies-end} */ ], targets: [ .binaryTarget( name: "BrazeKit", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeKit.zip", - checksum: "bc13bbaeceafc76e56800a7e9dfdc0886012855f073a1dfb1c42f1e7c7def1c7" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeKit.zip", + checksum: "688fa95883da207a84334180cf24e513413606342296de9a1a18a9e476ff94e0" ), .target( name: "BrazeKitResources", @@ -42,18 +45,38 @@ let package = Package( ), .binaryTarget( name: "BrazeLocation", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeLocation.zip", - checksum: "7edcc43e75f42de619aacfc2c7ec4cc03c3764236bd28806bb1c8da15b415818" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeLocation.zip", + checksum: "d30a4017cb8be420ef0f898505a42953fe4597c94c781bc44a079c9c4ba1bddf" ), .binaryTarget( name: "BrazeNotificationService", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazeNotificationService.zip", - checksum: "458cd5f89654daa37ff627975b9e251cbc28d9d66dc0d1fafc234145e81b6f1f" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazeNotificationService.zip", + checksum: "478b9f720794ff5d7c836ad07bb750b8af3bcad9fef96ae228d6b4fbbb91ea7f" ), .binaryTarget( name: "BrazePushStory", - url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.7.0/BrazePushStory.zip", - checksum: "3d7ce4e389ffc310e33e485cf6f11171e5dc91229d4d5be89f9546cc41d39ed6" + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.8.0/BrazePushStory.zip", + checksum: "70c2a629f61b5de3f95166c5caba330c4dc3c191dc350302f0e92d4226a3ca55" + ), + .target( + name: "BrazeKitCompat", + dependencies: [ + .target(name: "BrazeKit"), + .target(name: "BrazeLocation"), + ] + ), + .target( + name: "BrazeUICompat", + dependencies: [ + "BrazeKitCompat", + "SDWebImage", + ], + resources: [ + .process("ABKNewsFeed/Resources"), + .process("ABKInAppMessage/Resources"), + .process("ABKContentCards/Resources") + ], + publicHeadersPath: "include/AppboyUI" ), ] ) diff --git a/README.md b/README.md index e3f0e79bd1..18c23054a7 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@

- Version: 5.7.0 + Version: 5.8.0

-# Braze Swift SDK (Early Access) +# Braze Swift SDK - [Braze User Guide](https://www.braze.com/docs/user_guide/introduction/ "Braze User Guide") - [Braze Swift SDK Documentation](https://braze-inc.github.io/braze-swift-sdk) +- [Appboy-iOS-SDK: Migration guide](https://braze-inc.github.io/braze-swift-sdk/documentation/braze/appboy-migration-guide) ## Version Information - The Braze Swift SDK supports @@ -35,14 +36,6 @@ - Swift Package Manager - CocoaPods -## Upcoming Feature Roadmap - -The following features are planned for development. To request new Swift SDK features, please open a [Feature Request](https://github.com/braze-inc/braze-swift-sdk/issues). - -| Feature | Estimated Release | -|---|---| -| Objective-C Migration Library | December, 2022 | - ## Libraries @@ -70,12 +63,13 @@ The following features are planned for development. To request new Swift SDK fea Explore our [examples project](/Examples) which showcases multiple features' integrations. -## `appboy-ios-sdk` +## `Appboy-iOS-SDK` -The `appboy-ios-sdk` (Objective-C) SDK is now in maintenance mode, which means only critical bug fixes, and security updates will be made. No new features or minor bug fixes will be added to that library. +As of version 5.8.0, the Braze Swift SDK provides all the features available in the `Appboy-iOS-SDK`. -Later in 2022 we will announce our official deprecation and support policy for `appboy-ios-sdk`. For this reason, we encourage you to migrate to our new `braze-swift-sdk` as soon as possible. +We recommend all users to migrate to the Braze Swift SDK. For more information, please refer to our [migration guide](https://braze-inc.github.io/braze-swift-sdk/documentation/braze/appboy-migration-guide). +The `Appboy-iOS-SDK` (Objective-C) SDK is now in maintenance mode, which means only critical bug fixes, and security updates will be made. No new features or minor bug fixes will be added to that library. ## Questions? diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData+Compat.h new file mode 100644 index 0000000000..294dd9a8e2 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData+Compat.h @@ -0,0 +1,11 @@ +#import "ABKAttributionData.h" + +@class BRZUserAttributionData; + +@interface ABKAttributionData () + +@property(strong, nonatomic) BRZUserAttributionData *data; + +- (instancetype)initWithUserAttributionData:(BRZUserAttributionData *)data; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData.m b/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData.m new file mode 100644 index 0000000000..4111d3a2f1 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKAttributionData.m @@ -0,0 +1,54 @@ +#import "ABKAttributionData.h" +#import "ABKAttributionData+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKAttributionData + +- (NSString *)network { + return self.data.network; +} + +- (NSString *)campaign { + return self.data.campaign; +} + +- (NSString *)adGroup { + return self.data.adGroup; +} + +- (NSString *)creative { + return self.data.creative; +} + +- (instancetype)init { + return [self initWithNetwork:nil campaign:nil adGroup:nil creative:nil]; +} + +- (instancetype)initWithNetwork:(NSString *)network + campaign:(NSString *)campaign + adGroup:(NSString *)adGroup + creative:(NSString *)creative { + BRZUserAttributionData *data = + [[BRZUserAttributionData alloc] initWithNetwork:network + campaign:campaign + adGroup:adGroup + creative:creative]; + self = [self initWithUserAttributionData:data]; + return self; +} + +- (instancetype)initWithUserAttributionData:(BRZUserAttributionData *)data { + self = [super init]; + if (self) { + _data = data; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard+Compat.h new file mode 100644 index 0000000000..2e5b1e11e3 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard+Compat.h @@ -0,0 +1,5 @@ +#import "ABKBannerCard.h" + +@interface ABKBannerCard () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard.m new file mode 100644 index 0000000000..e0d9f55bef --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKBannerCard.m @@ -0,0 +1,33 @@ +#import "ABKBannerCard.h" +#import "ABKBannerCard+Compat.h" +#import "ABKCard+Compat.h" + +@import BrazeKit; + +@implementation ABKBannerCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +- (float)imageAspectRatio { + return self.card.imageAspectRatio; +} + +- (void)setImageAspectRatio:(float)imageAspectRatio { + self.card.imageAspectRatio = imageAspectRatio; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKBannerContentCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKBannerContentCard.m new file mode 100644 index 0000000000..2d66d4ef66 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKBannerContentCard.m @@ -0,0 +1,29 @@ +#import "ABKBannerContentCard.h" +#import "ABKContentCard+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKBannerContentCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (float)imageAspectRatio { + return self.card.imageAspectRatio; +} + +- (void)setImageAspectRatio:(float)imageAspectRatio { + self.card.imageAspectRatio = imageAspectRatio; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard+Compat.h new file mode 100644 index 0000000000..3f8a1124e0 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard+Compat.h @@ -0,0 +1,5 @@ +#import "ABKCaptionedImageCard.h" + +@interface ABKCaptionedImageCard () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard.m new file mode 100644 index 0000000000..f252286dd0 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageCard.m @@ -0,0 +1,49 @@ +#import "ABKCaptionedImageCard.h" +#import "ABKCaptionedImageCard+Compat.h" +#import "ABKCard+Compat.h" + +@import BrazeKit; + +@implementation ABKCaptionedImageCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (float)imageAspectRatio { + return self.card.imageAspectRatio; +} + +- (void)setImageAspectRatio:(float)imageAspectRatio { + self.card.imageAspectRatio = imageAspectRatio; +} + +- (NSString *)title { + return self.card.title; +} + +- (void)setTitle:(NSString *)title { + self.card.title = title; +} + +- (NSString *)cardDescription { + return self.card.cardDescription; +} + +- (void)setCardDescription:(NSString *)cardDescription { + self.card.cardDescription = cardDescription; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageContentCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageContentCard.m new file mode 100644 index 0000000000..d7c7cd64dd --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKCaptionedImageContentCard.m @@ -0,0 +1,53 @@ +#import "ABKCaptionedImageContentCard.h" +#import "ABKContentCard+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKCaptionedImageContentCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (float)imageAspectRatio { + return self.card.imageAspectRatio; +} + +- (void)setImageAspectRatio:(float)imageAspectRatio { + self.card.imageAspectRatio = imageAspectRatio; +} + +- (NSString *)title { + return self.card.title; +} + +- (void)setTitle:(NSString *)title { + self.card.title = title; +} + +- (NSString *)cardDescription { + return self.card.cardDescription; +} + +- (void)setCardDescription:(NSString *)cardDescription { + self.card.cardDescription = cardDescription; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKCard+Compat.h new file mode 100644 index 0000000000..c4882b5ca5 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKCard+Compat.h @@ -0,0 +1,11 @@ +#import "ABKCard.h" + +@class BRZNewsFeedCard; + +@interface ABKCard () + +@property (strong, nonatomic) BRZNewsFeedCard *card; + +- (instancetype)initWithNewsFeedCard:(BRZNewsFeedCard *)card; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKCard.m new file mode 100644 index 0000000000..fa396d4f2b --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKCard.m @@ -0,0 +1,118 @@ +#import "ABKCard.h" +#import "../BRZLog.h" +#import "ABKCard+Compat.h" +#import "ABKFeedController+Compat.h" + +@import BrazeKit; + +@implementation ABKCard + +- (NSString *)idString { + return self.card.identifier; +} + +- (BOOL)viewed { + return self.card.viewed; +} + +- (void)setViewed:(BOOL)viewed { + self.card.viewed = viewed; +} + +- (double)created { + return self.card.created; +} + +- (double)updated { + return self.card.updated; +} + +- (ABKCardCategory)categories { + return [ABKFeedController abkCategoriesWith:self.card.categories]; +} + +- (void)setCategories:(ABKCardCategory)categories { + self.card.categories = [ABKFeedController brzCategoriesWith:categories]; +} + +- (double)expiresAt { + return self.card.expires; +} + +- (NSDictionary *)extras { + return self.card.extras; +} + +- (void)setExtras:(NSDictionary *)extras { + self.card.extras = extras; +} + +- (NSString *)urlString { + return self.card.url.absoluteString; +} + +- (void)setUrlString:(NSString *)urlString { + self.card.url = [NSURL URLWithString:urlString]; +} + +- (BOOL)openUrlInWebView { + return self.card.useWebView; +} + +- (void)setOpenUrlInWebView:(BOOL)openUrlInWebView { + self.card.useWebView = openUrlInWebView; +} + ++ (ABKCard *)deserializeCardFromDictionary:(NSDictionary *)cardDictionary { + LogUnimplemented(); + return nil; +} + +- (NSData *)serializeToData { + LogUnimplemented(); + return nil; +} + +- (void)logCardImpression { + [self.card.context logImpression]; +} + +- (void)logCardClicked { + [self.card.context logClick]; +} + +- (BOOL)hasSameId:(ABKCard *)card { + return [self.idString isEqualToString:card.idString]; +} + +- (nonnull id)copyWithZone:(nullable NSZone *)zone { + LogUnimplemented(); + return [[ABKCard allocWithZone:zone] init]; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + LogUnimplemented(); +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + LogUnimplemented(); + return nil; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _card = [[BRZNewsFeedCard alloc] init]; + } + return self; +} + +- (instancetype)initWithNewsFeedCard:(BRZNewsFeedCard *)card { + self = [super init]; + if (self) { + _card = card; + } + return self; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard+Compat.h new file mode 100644 index 0000000000..da0c8ada89 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard+Compat.h @@ -0,0 +1,5 @@ +#import "ABKClassicCard.h" + +@interface ABKClassicCard () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard.m new file mode 100644 index 0000000000..46dd450510 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKClassicCard.m @@ -0,0 +1,41 @@ +#import "ABKClassicCard.h" +#import "ABKCard+Compat.h" +#import "ABKClassicCard+Compat.h" + +@import BrazeKit; + +@implementation ABKClassicCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (NSString *)cardDescription { + return self.card.cardDescription; +} + +- (void)setCardDescription:(NSString *)cardDescription { + self.card.cardDescription = cardDescription; +} + +- (NSString *)title { + return self.card.title; +} + +- (void)setTitle:(NSString *)title { + self.card.title = title; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKClassicContentCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKClassicContentCard.m new file mode 100644 index 0000000000..539954caa5 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKClassicContentCard.m @@ -0,0 +1,45 @@ +#import "ABKClassicContentCard.h" +#import "ABKContentCard+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKClassicContentCard + +- (NSString *)image { + return self.card.image.absoluteString; +} + +- (void)setImage:(NSString *)image { + self.card.image = [NSURL URLWithString:image]; +} + +- (NSString *)title { + return self.card.title; +} + +- (void)setTitle:(NSString *)title { + self.card.title = title; +} + +- (NSString *)cardDescription { + return self.card.cardDescription; +} + +- (void)setCardDescription:(NSString *)cardDescription { + self.card.cardDescription = cardDescription; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKContentCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKContentCard+Compat.h new file mode 100644 index 0000000000..588851447a --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKContentCard+Compat.h @@ -0,0 +1,11 @@ +#import "ABKContentCard.h" + +@class BRZContentCardRaw; + +@interface ABKContentCard () + +@property(strong, nonatomic) BRZContentCardRaw *card; + +- (instancetype)initWithContentCard:(BRZContentCardRaw *)card; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKContentCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKContentCard.m new file mode 100644 index 0000000000..2bf95f6201 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKContentCard.m @@ -0,0 +1,169 @@ +#import "ABKContentCard.h" +#import "ABKContentCard+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKContentCard + +- (NSString *)idString { + return self.card.identifier; +} + +- (BOOL)viewed { + return self.card.viewed; +} + +- (void)setViewed:(BOOL)viewed { + self.card.viewed = viewed; +} + +- (double)created { + return self.card.createdAt; +} + +- (BOOL)dismissible { + return self.card.dismissible; +} + +- (void)setDismissible:(BOOL)dismissible { + self.card.dismissible = dismissible; +} + +- (BOOL)pinned { + return self.card.pinned; +} + +- (void)setPinned:(BOOL)pinned { + self.card.pinned = pinned; +} + +- (BOOL)dismissed { + // TODO: Rename removed to dismissed + return self.card.removed; +} + +- (void)setDismissed:(BOOL)dismissed { + self.card.removed = dismissed; +} + +- (BOOL)clicked { + return self.card.clicked; +} + +- (void)setClicked:(BOOL)clicked { + self.card.clicked = clicked; +} + +- (NSDictionary *)extras { + return self.card.extras; +} + +- (void)setExtras:(NSDictionary *)extras { + if (extras == nil) { + extras = @{}; + } + self.card.extras = extras; +} + +- (BOOL)isTest { + return self.card.test; +} + +- (NSString *)urlString { + return self.card.url.absoluteString; +} + +- (void)setUrlString:(NSString *)urlString { + self.card.url = [NSURL URLWithString:urlString]; +} + +- (BOOL)openUrlInWebView { + return self.card.useWebView; +} + +- (void)setOpenUrlInWebView:(BOOL)openUrlInWebView { + self.card.useWebView = openUrlInWebView; +} + ++ (ABKContentCard *)deserializeCardFromDictionary: + (NSDictionary *)cardDictionary { + // TODO + return nil; +} + +- (NSData *)serializeToData { + return [self.card json]; +} + +- (void)logContentCardImpression { + self.card.viewed = YES; + [self.card.context logImpression]; +} + +- (void)logContentCardClicked { + [self.card.context logClick]; +} + +- (void)logContentCardDismissed { + [self.card.context logDismissed]; +} + +- (BOOL)isControlCard { + return self.card.type == BRZContentCardRawTypeControl; +} + +- (BOOL)hasSameId:(ABKContentCard *)card { + return self.idString == card.idString; +} + +- (nonnull id)copyWithZone:(nullable NSZone *)zone { + NSData *json = [self.card json]; + BRZContentCardRaw *card = [BRZContentCardRaw fromJson:json]; + return [[ABKContentCard allocWithZone:zone] initWithContentCard:card]; +} + +- (void)encodeWithCoder:(nonnull NSCoder *)coder { + [coder encodeDataObject:[self.card json]]; +} + +- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder { + NSData *json = [coder decodeDataObject]; + BRZContentCardRaw *card = [BRZContentCardRaw fromJson:json]; + return [[ABKContentCard alloc] initWithContentCard:card]; +} + +- (BOOL)isEqual:(id)other { + if (![other isKindOfClass:[ABKContentCard class]]) { + return NO; + } + + ABKContentCard *cardObject = (ABKContentCard *)other; + return [self hasSameId:cardObject]; +} + +- (NSUInteger)hash { + return [self.idString hash]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _card = [[BRZContentCardRaw alloc] init]; + } + return self; +} + +- (instancetype)initWithContentCard:(BRZContentCardRaw *)card { + self = [super init]; + if (self) { + _card = card; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController+Compat.h new file mode 100644 index 0000000000..d8758dfe97 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController+Compat.h @@ -0,0 +1,11 @@ +#import "ABKContentCardsController.h" + +@class BRZContentCards; + +@interface ABKContentCardsController () + +@property(strong, nonatomic) BRZContentCards *contentCardsApi; + +- (instancetype)initWithContentCardsApi:(BRZContentCards *)contentCardsApi; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController.m b/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController.m new file mode 100644 index 0000000000..b4f097e74c --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKContentCardsController.m @@ -0,0 +1,73 @@ +#import "ABKContentCardsController.h" +#import "ABKBannerContentCard.h" +#import "ABKCaptionedImageContentCard.h" +#import "ABKClassicContentCard.h" +#import "ABKContentCard+Compat.h" +#import "ABKContentCardsController+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +NSString *const ABKContentCardsProcessedNotification = + @"contentCardsProcessedNotification"; +NSString *const ABKContentCardsProcessedIsSuccessfulKey = @"isSuccessful"; + +@implementation ABKContentCardsController + +- (NSArray *)getContentCards { + NSMutableArray *abkCards = [NSMutableArray array]; + for (BRZContentCardRaw *card in self.contentCardsApi.cards) { + ABKContentCard *abkCard; + switch (card.type) { + case BRZContentCardRawTypeClassic: + abkCard = [[ABKClassicContentCard alloc] initWithContentCard:card]; + break; + case BRZContentCardRawTypeBanner: + abkCard = [[ABKBannerContentCard alloc] initWithContentCard:card]; + break; + case BRZContentCardRawTypeCaptionedImage: + abkCard = [[ABKCaptionedImageContentCard alloc] initWithContentCard:card]; + break; + case BRZContentCardRawTypeControl: + abkCard = [[ABKContentCard alloc] initWithContentCard:card]; + break; + default: + abkCard = [[ABKContentCard alloc] initWithContentCard:card]; + break; + } + [abkCards addObject:abkCard]; + } + return [abkCards copy]; +} + +- (NSDate *)lastUpdate { + return self.contentCardsApi.lastUpdate; +} + +- (NSInteger)unviewedContentCardCount { + NSInteger unviewed = 0; + for (BRZContentCardRaw *card in self.contentCardsApi.cards) { + if (!card.viewed && card.type != BRZContentCardRawTypeControl) { + unviewed += 1; + } + } + return unviewed; +} + +- (NSInteger)contentCardCount { + return self.contentCardsApi.cards.count; +} + +- (instancetype)initWithContentCardsApi:(BRZContentCards *)contentCardsApi { + self = [super init]; + if (self) { + _contentCardsApi = contentCardsApi; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKFacebookUser.m b/Sources/BrazeKitCompat/AppboyKit/ABKFacebookUser.m new file mode 100644 index 0000000000..3812d16b1b --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKFacebookUser.m @@ -0,0 +1,35 @@ +#import "ABKFacebookUser.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +NSInteger const DefaultNumberOfFriends = -1; + +@implementation ABKFacebookUser + +- (instancetype)initWithFacebookUserDictionary: + (NSDictionary *)facebookUserDictionary + numberOfFriends:(NSInteger)numberOfFriends + likes:(NSArray *)likes { + self = [super init]; + if (self) { + _facebookUserDictionary = + [[NSDictionary alloc] initWithDictionary:facebookUserDictionary + copyItems:YES]; + _numberOfFriends = numberOfFriends; + _likes = [[NSArray alloc] initWithArray:likes copyItems:YES]; + } + return self; +} + +// Setting default value of _numberOfFriends to be -1 +- (instancetype)init { + if (self = [super init]) { + _numberOfFriends = DefaultNumberOfFriends; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKFeedController+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKFeedController+Compat.h new file mode 100644 index 0000000000..fd3a673ac7 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKFeedController+Compat.h @@ -0,0 +1,14 @@ +#import "ABKFeedController.h" + +@class BRZNewsFeed; +@class BRZNewsFeedCardCategory; + +@interface ABKFeedController () + +@property (strong, nonatomic) BRZNewsFeed *newsFeedApi; + +- (instancetype)initWithNewsFeedApi:(BRZNewsFeed *)newsFeedApi; ++ (NSArray *)brzCategoriesWith:(ABKCardCategory)categories; ++ (ABKCardCategory)abkCategoriesWith:(NSArray *)categories; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKFeedController.m b/Sources/BrazeKitCompat/AppboyKit/ABKFeedController.m new file mode 100644 index 0000000000..78466a4e76 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKFeedController.m @@ -0,0 +1,145 @@ +#import "ABKFeedController.h" +#import "../BRZLog.h" +#import "ABKBannerCard.h" +#import "ABKCaptionedImageCard.h" +#import "ABKCard+Compat.h" +#import "ABKClassicCard.h" +#import "ABKFeedController+Compat.h" +#import "ABKTextAnnouncementCard.h" + +@import BrazeKit; + +NSString *const ABKFeedUpdatedNotification = @"feedUpdatedNotification"; +NSString *const ABKFeedUpdatedIsSuccessfulKey = @"isSuccessful"; + +@implementation ABKFeedController + +- (NSArray *)getNewsFeedCards { + NSMutableArray *abkCards = [NSMutableArray array]; + for (BRZNewsFeedCard *card in self.newsFeedApi.cards) { + ABKCard *abkCard; + switch (card.type) { + case BRZNewsFeedCardTypeClassic: + abkCard = [[ABKClassicCard alloc] initWithNewsFeedCard:card]; + break; + case BRZNewsFeedCardTypeBanner: + abkCard = [[ABKBannerCard alloc] initWithNewsFeedCard:card]; + break; + case BRZNewsFeedCardTypeCaptionedImage: + abkCard = [[ABKCaptionedImageCard alloc] initWithNewsFeedCard:card]; + break; + case BRZNewsFeedCardTypeTextAnnouncement: + abkCard = [[ABKTextAnnouncementCard alloc] initWithNewsFeedCard:card]; + break; + default: + abkCard = [[ABKCard alloc] initWithNewsFeedCard:card]; + break; + } + [abkCards addObject:abkCard]; + } + return [abkCards copy]; +} + +- (NSDate *)lastUpdate { + return self.newsFeedApi.lastUpdate; +} + +- (NSInteger)unreadCardCountForCategories:(ABKCardCategory)categories { + NSArray *brzCategories = + [ABKFeedController brzCategoriesWith:categories]; + NSInteger unread = 0; + for (BRZNewsFeedCard *card in self.newsFeedApi.cards) { + if (card.viewed) { + continue; + } + for (BRZNewsFeedCardCategory *category in card.categories) { + if ([brzCategories containsObject:category]) { + unread += 1; + break; + } + } + } + return unread; +} + +- (NSInteger)cardCountForCategories:(ABKCardCategory)categories { + return [self getCardsInCategories:categories].count; +} + +- (NSArray *)getCardsInCategories:(ABKCardCategory)categories { + if (categories == ABKCardCategoryAll) { + return [self getNewsFeedCards]; + } + + NSMutableArray *cards = [NSMutableArray array]; + for (ABKCard *card in [self getNewsFeedCards]) { + if (card.categories & categories) { + [cards addObject:card]; + } + } + return cards; +} + +- (instancetype)initWithNewsFeedApi:(BRZNewsFeed *)newsFeedApi { + self = [super init]; + if (self) { + _newsFeedApi = newsFeedApi; + } + return self; +} + ++ (NSArray *)brzCategoriesWith: + (ABKCardCategory)categories { + NSMutableArray *brzCategories = [NSMutableArray array]; + + if (categories & ABKCardCategoryNoCategory) { + [brzCategories addObject:BRZNewsFeedCardCategory.none]; + } + + if (categories & ABKCardCategoryNews) { + [brzCategories addObject:BRZNewsFeedCardCategory.news]; + } + + if (categories & ABKCardCategoryAdvertising) { + [brzCategories addObject:BRZNewsFeedCardCategory.advertising]; + } + + if (categories & ABKCardCategoryAnnouncements) { + [brzCategories addObject:BRZNewsFeedCardCategory.announcements]; + } + + if (categories & ABKCardCategorySocial) { + [brzCategories addObject:BRZNewsFeedCardCategory.social]; + } + + return brzCategories; +} + ++ (ABKCardCategory)abkCategoriesWith: + (NSArray *)categories { + ABKCardCategory abkCategories = 0; + + if ([categories containsObject:BRZNewsFeedCardCategory.none]) { + abkCategories |= ABKCardCategoryNoCategory; + } + + if ([categories containsObject:BRZNewsFeedCardCategory.news]) { + abkCategories |= ABKCardCategoryNews; + } + + if ([categories containsObject:BRZNewsFeedCardCategory.advertising]) { + abkCategories |= ABKCardCategoryAdvertising; + } + + if ([categories containsObject:BRZNewsFeedCardCategory.announcements]) { + abkCategories |= ABKCardCategoryAnnouncements; + } + + if ([categories containsObject:BRZNewsFeedCardCategory.social]) { + abkCategories |= ABKCardCategorySocial; + } + + return abkCategories; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessage.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessage.m new file mode 100644 index 0000000000..d2ac97ce18 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessage.m @@ -0,0 +1,291 @@ +#import "ABKInAppMessage.h" +#import "../BRZLog.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageDarkTheme+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; +@import UIKit; + +@implementation ABKInAppMessage + +- (NSString *)message { + return self.inAppMessage.message; +} + +- (void)setMessage:(NSString *)message { + self.inAppMessage.message = message; +} + +- (NSDictionary *)extras { + return self.inAppMessage.extras; +} + +- (void)setExtras:(NSDictionary *)extras { + self.inAppMessage.extras = extras; +} + +- (NSTimeInterval)duration { + return self.inAppMessage.duration; +} + +- (void)setDuration:(NSTimeInterval)duration { + self.inAppMessage.duration = duration; +} + +- (ABKInAppMessageClickActionType)inAppMessageClickActionType { + switch (self.inAppMessage.clickAction) { + case BRZInAppMessageRawClickActionNewsFeed: + return ABKInAppMessageDisplayNewsFeed; + case BRZInAppMessageRawClickActionURL: + return ABKInAppMessageRedirectToURI; + case BRZInAppMessageRawClickActionNone: + return ABKInAppMessageNoneClickAction; + default: + return ABKInAppMessageNoneClickAction; + } +} + +- (NSURL *)uri { + return self.inAppMessage.url; +} + +- (BOOL)openUrlInWebView { + return self.inAppMessage.useWebView; +} + +- (void)setOpenUrlInWebView:(BOOL)openUrlInWebView { + self.inAppMessage.useWebView = openUrlInWebView; +} + +- (ABKInAppMessageDismissType)inAppMessageDismissType { + return (ABKInAppMessageDismissType)self.inAppMessage.messageClose; +} + +- (void)setInAppMessageDismissType: + (ABKInAppMessageDismissType)inAppMessageDismissType { + self.inAppMessage.messageClose = + (BRZInAppMessageRawClose)inAppMessageDismissType; +} + +- (UIColor *)backgroundColor { + return self.inAppMessage.backgroundColor.uiColor; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + self.inAppMessage.backgroundColor = + [[BRZInAppMessageRawColor alloc] init:backgroundColor]; +} + +- (UIColor *)textColor { + return self.inAppMessage.textColor.uiColor; +} + +- (void)setTextColor:(UIColor *)textColor { + self.inAppMessage.textColor = + [[BRZInAppMessageRawColor alloc] init:textColor]; +} + +- (NSString *)icon { + return self.inAppMessage.icon; +} + +- (void)setIcon:(NSString *)icon { + self.inAppMessage.icon = icon; +} + +- (UIColor *)iconColor { + return self.inAppMessage.iconColor.uiColor; +} + +- (void)setIconColor:(UIColor *)iconColor { + self.inAppMessage.iconColor = + [[BRZInAppMessageRawColor alloc] init:iconColor]; +} + +- (UIColor *)iconBackgroundColor { + return self.inAppMessage.iconBackgroundColor.uiColor; +} + +- (void)setIconBackgroundColor:(UIColor *)iconBackgroundColor { + self.inAppMessage.iconBackgroundColor = + [[BRZInAppMessageRawColor alloc] init:iconBackgroundColor]; +} + +- (BOOL)enableDarkTheme { + return self.inAppMessage.themes[@"dark"] != nil; +} + +- (void)setEnableDarkTheme:(BOOL)enableDarkTheme { + NSMutableDictionary *themes = [self.inAppMessage.themes mutableCopy]; + if (enableDarkTheme) { + themes[@"dark"] = themes[@"_dark"] ?: themes[@"dark"]; + themes[@"_dark"] = nil; + } else { + themes[@"_dark"] = themes[@"dark"] ?: themes[@"_dark"]; + themes[@"dark"] = nil; + } + self.inAppMessage.themes = [themes copy]; +} + +- (ABKInAppMessageDarkTheme *)darkTheme { + BRZInAppMessageRawTheme *theme = self.inAppMessage.themes[@"dark"]; + if (!theme) { + return nil; + } + return [[ABKInAppMessageDarkTheme alloc] initWithTheme:theme]; +} + +- (void)setDarkTheme:(ABKInAppMessageDarkTheme *)darkTheme { + NSMutableDictionary *themes = [self.inAppMessage.themes mutableCopy]; + themes[@"dark"] = darkTheme.theme; + self.inAppMessage.themes = [themes copy]; +} + +- (NSInteger)overrideUserInterfaceStyle { + return self.inAppMessage._compat_overrideUserInterfaceStyle; +} + +- (void)setOverrideUserInterfaceStyle:(NSInteger)overrideUserInterfaceStyle { + self.inAppMessage._compat_overrideUserInterfaceStyle = + overrideUserInterfaceStyle; +} + +- (NSURL *)imageURI { + return self.inAppMessage.imageURL; +} + +- (void)setImageURI:(NSURL *)imageURI { + self.inAppMessage.imageURL = imageURI; +} + +- (ABKInAppMessageOrientation)orientation { + return (ABKInAppMessageOrientation)self.inAppMessage.orientation; +} + +- (void)setOrientation:(ABKInAppMessageOrientation)orientation { + self.inAppMessage.orientation = (BRZInAppMessageRawOrientation)orientation; +} + +- (NSTextAlignment)messageTextAlignment { + BOOL leftToRight = + [UIApplication sharedApplication].userInterfaceLayoutDirection == + UIUserInterfaceLayoutDirectionLeftToRight; + switch (self.inAppMessage.messageTextAlignment) { + case BRZInAppMessageRawTextAlignmentStart: + return NSTextAlignmentNatural; + case BRZInAppMessageRawTextAlignmentCenter: + return NSTextAlignmentCenter; + case BRZInAppMessageRawTextAlignmentEnd: + return leftToRight ? NSTextAlignmentRight : NSTextAlignmentLeft; + default: + return NSTextAlignmentNatural; + } +} + +- (void)setMessageTextAlignment:(NSTextAlignment)messageTextAlignment { + BOOL leftToRight = + [UIApplication sharedApplication].userInterfaceLayoutDirection == + UIUserInterfaceLayoutDirectionLeftToRight; + BRZInAppMessageRawTextAlignment alignment; + switch (messageTextAlignment) { + case NSTextAlignmentNatural: + case NSTextAlignmentJustified: + alignment = leftToRight ? BRZInAppMessageRawTextAlignmentStart + : BRZInAppMessageRawTextAlignmentEnd; + break; + case NSTextAlignmentCenter: + alignment = BRZInAppMessageRawTextAlignmentCenter; + break; + case NSTextAlignmentRight: + alignment = BRZInAppMessageRawTextAlignmentEnd; + break; + case NSTextAlignmentLeft: + alignment = BRZInAppMessageRawTextAlignmentStart; + break; + default: + alignment = BRZInAppMessageRawTextAlignmentStart; + break; + } + self.inAppMessage.messageTextAlignment = alignment; +} + +- (BOOL)animateIn { + return self.inAppMessage.animateIn; +} + +- (void)setAnimateIn:(BOOL)animateIn { + self.inAppMessage.animateIn = animateIn; +} + +- (BOOL)animateOut { + return self.inAppMessage.animateOut; +} + +- (void)setAnimateOut:(BOOL)animateOut { + self.inAppMessage.animateOut = animateOut; +} + +- (BOOL)isControl { + return self.inAppMessage.isControl; +} + +- (void)setIsControl:(BOOL)isControl { + self.inAppMessage.isControl = isControl; +} + +- (void)logInAppMessageImpression { + [self.inAppMessage.context logImpression]; +} + +- (void)logInAppMessageClicked { + [self.inAppMessage.context logClick]; +} + +- (void)setInAppMessageClickAction: + (ABKInAppMessageClickActionType)clickActionType + withURI:(NSURL *)uri { + switch (clickActionType) { + case ABKInAppMessageDisplayNewsFeed: + self.inAppMessage.clickAction = BRZInAppMessageRawClickActionNewsFeed; + self.inAppMessage.url = nil; + break; + case ABKInAppMessageRedirectToURI: + self.inAppMessage.clickAction = BRZInAppMessageRawClickActionURL; + self.inAppMessage.url = uri; + break; + case ABKInAppMessageNoneClickAction: + self.inAppMessage.clickAction = BRZInAppMessageRawClickActionNone; + self.inAppMessage.url = nil; + break; + default: + self.inAppMessage.clickAction = BRZInAppMessageRawClickActionNone; + self.inAppMessage.url = nil; + break; + } +} + +- (NSData *)serializeToData { + return [self.inAppMessage json]; +} + +- (instancetype)init { + return [self initWithInAppMessage:[[BRZInAppMessageRaw alloc] init]]; +} + +- (instancetype)initWithInAppMessage:(BRZInAppMessageRaw *)inAppMessage { + self = [super init]; + if (self) { + _inAppMessage = inAppMessage; + _imageContentMode = inAppMessage.type == BRZInAppMessageRawTypeFull + ? UIViewContentModeScaleAspectFill + : UIViewContentModeScaleAspectFit; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton+Compat.h new file mode 100644 index 0000000000..818d062e84 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton+Compat.h @@ -0,0 +1,11 @@ +#import "ABKInAppMessageButton.h" + +@class BRZInAppMessageRawButton; + +@interface ABKInAppMessageButton () + +@property(strong, nonatomic) BRZInAppMessageRawButton *button; + +- (instancetype)initWithButton:(BRZInAppMessageRawButton *)button; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton.m new file mode 100644 index 0000000000..af21971518 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageButton.m @@ -0,0 +1,107 @@ +#import "ABKInAppMessageButton.h" +#import "ABKInAppMessageButton+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageButton + +- (NSString *)buttonText { + return self.button.text; +} + +- (void)setButtonText:(NSString *)buttonText { + self.button.text = buttonText; +} + +- (UIColor *)buttonBackgroundColor { + return self.button.backgroundColor.uiColor; +} + +- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor { + self.button.backgroundColor = + [[BRZInAppMessageRawColor alloc] init:buttonBackgroundColor]; +} + +- (UIColor *)buttonBorderColor { + return self.button.borderColor.uiColor; +} + +- (void)setButtonBorderColor:(UIColor *)buttonBorderColor { + self.button.borderColor = + [[BRZInAppMessageRawColor alloc] init:buttonBorderColor]; +} + +- (UIColor *)buttonTextColor { + return self.button.textColor.uiColor; +} + +- (void)setButtonTextColor:(UIColor *)buttonTextColor { + self.button.textColor = + [[BRZInAppMessageRawColor alloc] init:buttonTextColor]; +} + +- (ABKInAppMessageClickActionType)buttonClickActionType { + switch (self.button.clickAction) { + case BRZInAppMessageRawClickActionNewsFeed: + return ABKInAppMessageDisplayNewsFeed; + case BRZInAppMessageRawClickActionURL: + return ABKInAppMessageRedirectToURI; + case BRZInAppMessageRawClickActionNone: + return ABKInAppMessageNoneClickAction; + default: + return ABKInAppMessageNoneClickAction; + } +} + +- (NSURL *)buttonClickedURI { + return self.button.url; +} + +- (BOOL)buttonOpenUrlInWebView { + return self.button.useWebView; +} + +- (void)setButtonOpenUrlInWebView:(BOOL)buttonOpenUrlInWebView { + self.button.useWebView = buttonOpenUrlInWebView; +} + +- (NSInteger)buttonID { + return self.button.identifier; +} + +- (void)setButtonClickAction:(ABKInAppMessageClickActionType)clickActionType + withURI:(NSURL *)uri { + switch (clickActionType) { + case ABKInAppMessageDisplayNewsFeed: + self.button.clickAction = BRZInAppMessageRawClickActionNewsFeed; + self.button.url = nil; + break; + case ABKInAppMessageRedirectToURI: + self.button.clickAction = BRZInAppMessageRawClickActionURL; + self.button.url = uri; + break; + case ABKInAppMessageNoneClickAction: + self.button.clickAction = BRZInAppMessageRawClickActionNone; + self.button.url = nil; + break; + default: + self.button.clickAction = BRZInAppMessageRawClickActionNone; + self.button.url = nil; + break; + } +} + +- (instancetype)initWithButton:(BRZInAppMessageRawButton *)button { + self = [super init]; + if (self) { + _button = button; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController+Compat.h new file mode 100644 index 0000000000..cb68ad2c77 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController+Compat.h @@ -0,0 +1,16 @@ +#import "ABKInAppMessageController.h" + +@import BrazeKit; + +@class BrazeDelegateWrapper; + +@interface ABKInAppMessageController () + +@property(strong, nonatomic) NSObject *presenter; +@property(strong, nonatomic) BrazeDelegateWrapper *delegateWrapper; +@property(assign, nonatomic) BOOL isCompatibility; + +- (instancetype)initWithInAppMessagePresenter: + (NSObject *)presenter delegateWrapper:(BrazeDelegateWrapper *)delegateWrapper; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController.m new file mode 100644 index 0000000000..0e22ca0f7c --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageController.m @@ -0,0 +1,199 @@ +#import "ABKInAppMessageController.h" +#import "../BrazeDelegateWrapper.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageController+Compat.h" +#import "ABKInAppMessageFull+Compat.h" +#import "ABKInAppMessageHTML+Compat.h" +#import "ABKInAppMessageHTMLFull+Compat.h" +#import "ABKInAppMessageModal+Compat.h" +#import "ABKInAppMessageSlideup+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@interface ABKInAppMessageController () + +@property(strong, nonatomic, readonly) NSArray *stack; + +- (void)tryPushOnStack:(BRZInAppMessageRaw *)message; +- (ABKInAppMessage *)convertToABKInAppMessage:(BRZInAppMessageRaw *)message; + +@end + +@implementation ABKInAppMessageController + +- (id)delegate { + return self.delegateWrapper.inAppMessageControllerDelegate; +} + +- (void)setDelegate:(id)delegate { + self.delegateWrapper.inAppMessageControllerDelegate = delegate; +} + +- (void)displayNextInAppMessageWithDelegate: + (id)delegate { + [self displayNextInAppMessage]; +} + +- (void)displayNextInAppMessage { + if (self.isCompatibility) { + SEL selector = NSSelectorFromString(@"handleExistingInAppMessagesInStack"); + if (![self.inAppMessageUIController respondsToSelector:selector]) { + NSLog(@"[BrazeKitCompat] Error: invalid 'inAppMessageUIController', cannot execute `displayNextInAppMessage`."); + return; + } + IMP imp = + [(NSObject *)self.inAppMessageUIController methodForSelector:selector]; + void (*handleExistingInAppMessagesInStack)(id, SEL) = (void *)imp; + handleExistingInAppMessagesInStack(self.inAppMessageUIController, selector); + return; + } + + if (!self.presenter) { + NSLog(@"[BrazeKitCompat] Error: invalid 'presenter', cannot execute `displayNextInAppMessage`."); + return; + } + + SEL selector = NSSelectorFromString(@"presentNext"); + if (![self.presenter respondsToSelector:selector]) { + NSLog(@"[BrazeKitCompat] Error: invalid 'presenter' ('presentNext'), cannot execute `displayNextInAppMessage`."); + return; + } + IMP imp = [self.presenter methodForSelector:selector]; + void (*presentNext)(id, SEL) = (void *)imp; + presentNext(self.presenter, selector); +} + +- (NSInteger)inAppMessagesRemainingOnStack { + return self.stack.count; +} + +- (void)addInAppMessage:(ABKInAppMessage *)newInAppMessage { + [self.presenter presentMessage:newInAppMessage.inAppMessage]; +} + +- (NSArray *)stack { + id stack; + if (self.isCompatibility) { + stack = [self.presenter valueForKey:@"inAppMessageStack"]; + if ([stack isKindOfClass:[NSArray class]]) { + NSMutableArray *unwrapped = [NSMutableArray array]; + for (ABKInAppMessage *message in stack) { + [unwrapped addObject:message.inAppMessage]; + } + stack = unwrapped; + } else { + NSLog(@"[BrazeKitCompat] Error: invalid 'inAppMessageStack'."); + } + } else { + stack = [self.presenter valueForKey:@"stack"]; + } + if ([stack isKindOfClass:[NSArray class]]) { + return stack; + } + return @[]; +} + +- (void)tryPushOnStack:(BRZInAppMessageRaw *)message { + // AppboyUI + if (self.isCompatibility) { + NSMutableArray *stack = + [self.presenter valueForKey:@"inAppMessageStack"]; + if (![stack isKindOfClass:[NSMutableArray class]]) { + NSLog(@"[BrazeKitCompat] Error: invalid 'inAppMessageStack', cannot push in-app message to stack."); + return; + } + [stack addObject:[self convertToABKInAppMessage:message]]; + return; + } + + // BrazeUI + SEL selector = NSSelectorFromString(@"_compat_tryPushOnStack:"); + if (![self.presenter respondsToSelector:selector]) { + NSLog(@"[BrazeKitCompat] Error: invalid 'presenter', cannot push in-app message on the stack."); + return; + } + IMP imp = [self.presenter methodForSelector:selector]; + void (*_compat_tryPushOnStack)(id, SEL, BRZInAppMessageRaw *) = (void *)imp; + _compat_tryPushOnStack(self.presenter, selector, message); +} + +- (ABKInAppMessage *)convertToABKInAppMessage:(BRZInAppMessageRaw *)message { + ABKInAppMessage *abkMessage; + switch (message.type) { + case BRZInAppMessageRawTypeSlideup: + abkMessage = [[ABKInAppMessageSlideup alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeModal: + abkMessage = [[ABKInAppMessageModal alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeFull: + abkMessage = [[ABKInAppMessageFull alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeHtmlFull: + abkMessage = [[ABKInAppMessageHTMLFull alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeHtml: + abkMessage = [[ABKInAppMessageHTML alloc] initWithInAppMessage:message]; + break; + default: + abkMessage = [[ABKInAppMessage alloc] initWithInAppMessage:message]; + break; + } + return abkMessage; +} + +- (instancetype)initWithInAppMessagePresenter: + (NSObject *)presenter + delegateWrapper: + (BrazeDelegateWrapper *)delegateWrapper { + self = [super init]; + if (self) { + _presenter = presenter; + _delegateWrapper = delegateWrapper; + } + return self; +} + +// ***** COMPAT ***** + +// Dynamically executed by the Swift SDK when available for compatibility +// purposes. +- (NSNumber *)_compat_beforeInAppMessageDisplayed: + (BRZInAppMessageRaw *)message { + ABKInAppMessage *abkMessage = [self convertToABKInAppMessage:message]; + + ABKInAppMessageDisplayChoice displayChoice = ABKDisplayInAppMessageNow; + if (abkMessage.isControl) { + if ([self.delegate respondsToSelector:@selector + (beforeControlMessageImpressionLogged:)]) { + displayChoice = + [self.delegate beforeControlMessageImpressionLogged:abkMessage]; + } + } else { + if ([self.delegate + respondsToSelector:@selector(beforeInAppMessageDisplayed:)]) { + displayChoice = [self.delegate beforeInAppMessageDisplayed:abkMessage]; + } + } + + switch (displayChoice) { + case ABKDisplayInAppMessageNow: + return @YES; + case ABKDisplayInAppMessageLater: + [self tryPushOnStack:message]; + return @NO; + case ABKDiscardInAppMessage: + return @NO; + default: + return @YES; + } +} + +// ****************** + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme+Compat.h new file mode 100644 index 0000000000..bf901eff65 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme+Compat.h @@ -0,0 +1,11 @@ +#import "ABKInAppMessageDarkButtonTheme.h" + +@class BRZInAppMessageRawButtonTheme; + +@interface ABKInAppMessageDarkButtonTheme () + +@property(strong, nonatomic) BRZInAppMessageRawButtonTheme *theme; + +- (instancetype)initWithTheme:(BRZInAppMessageRawButtonTheme *)theme; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme.m new file mode 100644 index 0000000000..67e393c485 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkButtonTheme.m @@ -0,0 +1,53 @@ +#import "ABKInAppMessageDarkButtonTheme.h" +#import "../BRZLog.h" +#import "ABKInAppMessageDarkButtonTheme+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageDarkButtonTheme + +- (UIColor *)buttonBackgroundColor { + return self.theme.backgroundColor.uiColor; +} + +- (void)setButtonBackgroundColor:(UIColor *)buttonBackgroundColor { + self.theme.backgroundColor = + [[BRZInAppMessageRawColor alloc] init:buttonBackgroundColor]; +} + +- (UIColor *)buttonBorderColor { + return self.theme.borderColor.uiColor; +} + +- (void)setButtonBorderColor:(UIColor *)buttonBorderColor { + self.theme.borderColor = + [[BRZInAppMessageRawColor alloc] init:buttonBorderColor]; +} + +- (UIColor *)buttonTextColor { + return self.theme.textColor.uiColor; +} + +- (void)setButtonTextColor:(UIColor *)buttonTextColor { + self.theme.textColor = [[BRZInAppMessageRawColor alloc] init:buttonTextColor]; +} + +- (instancetype)initWithFields:(NSDictionary *)darkButtonFields { + LogUnimplemented(); + return nil; +} + +- (instancetype)initWithTheme:(BRZInAppMessageRawButtonTheme *)theme { + self = [super init]; + if (self) { + _theme = theme; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme+Compat.h new file mode 100644 index 0000000000..f97301e20f --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme+Compat.h @@ -0,0 +1,11 @@ +#import "ABKInAppMessageDarkTheme.h" + +@class BRZInAppMessageRawTheme; + +@interface ABKInAppMessageDarkTheme () + +@property(strong, nonatomic) BRZInAppMessageRawTheme *theme; + +- (instancetype)initWithTheme:(BRZInAppMessageRawTheme *)theme; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme.m new file mode 100644 index 0000000000..dc09dd17cc --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageDarkTheme.m @@ -0,0 +1,102 @@ +#import "ABKInAppMessageDarkTheme.h" +#import "../BRZLog.h" +#import "ABKInAppMessageDarkButtonTheme+Compat.h" +#import "ABKInAppMessageDarkTheme+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageDarkTheme + +- (UIColor *)backgroundColor { + return self.theme.backgroundColor.uiColor; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor { + self.theme.backgroundColor = + [[BRZInAppMessageRawColor alloc] init:backgroundColor]; +} + +- (UIColor *)textColor { + return self.theme.textColor.uiColor; +} + +- (void)setTextColor:(UIColor *)textColor { + self.theme.textColor = [[BRZInAppMessageRawColor alloc] init:textColor]; +} + +- (UIColor *)iconColor { + return self.theme.iconColor.uiColor; +} + +- (void)setIconColor:(UIColor *)iconColor { + self.theme.iconColor = [[BRZInAppMessageRawColor alloc] init:iconColor]; +} + +- (UIColor *)headerTextColor { + return self.theme.headerTextColor.uiColor; +} + +- (void)setHeaderTextColor:(UIColor *)headerTextColor { + self.theme.headerTextColor = + [[BRZInAppMessageRawColor alloc] init:headerTextColor]; +} + +- (UIColor *)closeButtonColor { + return self.theme.closeButtonColor.uiColor; +} + +- (void)setCloseButtonColor:(UIColor *)closeButtonColor { + self.theme.closeButtonColor = + [[BRZInAppMessageRawColor alloc] init:closeButtonColor]; +} + +- (UIColor *)frameColor { + return self.theme.frameColor.uiColor; +} + +- (void)setFrameColor:(UIColor *)frameColor { + self.theme.frameColor = [[BRZInAppMessageRawColor alloc] init:frameColor]; +} + +- (NSArray *)buttons { + NSMutableArray *buttons = [NSMutableArray array]; + for (BRZInAppMessageRawButtonTheme *theme in self.theme.buttons) { + [buttons + addObject:[[ABKInAppMessageDarkButtonTheme alloc] initWithTheme:theme]]; + } + return [buttons copy]; +} + +- (void)setButtons:(NSArray *)buttons { + NSMutableArray *buttonThemes = [NSMutableArray array]; + for (ABKInAppMessageDarkButtonTheme *theme in buttons) { + [buttonThemes addObject:theme.theme]; + } + self.theme.buttons = [buttonThemes copy]; +} + +- (instancetype)initWithFields: + (NSDictionary *)darkThemeFields { + LogUnimplemented(); + return nil; +} + +- (UIColor *)getColorForKey:(NSString *)key { + LogUnimplemented(); + return nil; +} + +- (instancetype)initWithTheme:(BRZInAppMessageRawTheme *)theme { + self = [super init]; + if (self) { + _theme = theme; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull+Compat.h new file mode 100644 index 0000000000..adcc32ca1d --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageFull.h" + +@interface ABKInAppMessageFull () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull.m new file mode 100644 index 0000000000..67c9f0928d --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageFull.m @@ -0,0 +1,11 @@ +#import "ABKInAppMessageFull.h" +#import "ABKInAppMessageFull+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation ABKInAppMessageFull + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML+Compat.h new file mode 100644 index 0000000000..e903b78547 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageHTML.h" + +@interface ABKInAppMessageHTML () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML.m new file mode 100644 index 0000000000..046c3b094c --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTML.m @@ -0,0 +1,52 @@ +#import "ABKInAppMessageHTML.h" +#import "../BRZLog.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageHTML+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageHTML + +- (BOOL)trusted { + LogUnimplemented(); + return NO; +} + +- (void)setTrusted:(BOOL)trusted { + LogUnimplemented(); +} + +- (NSArray *)assetUrls { + NSMutableArray *assetUrls = [NSMutableArray array]; + for (NSURL *assetUrl in self.inAppMessage.assetURLs) { + [assetUrls addObject:assetUrl.absoluteString]; + } + return [assetUrls copy]; +} + +- (void)setAssetUrls:(NSArray *)assetUrls { + NSMutableArray *urls = [NSMutableArray array]; + for (NSString *assetUrl in assetUrls) { + NSURL *url = [NSURL URLWithString:assetUrl]; + if (url) { + [urls addObject:url]; + } + } + self.inAppMessage.assetURLs = [urls copy]; +} + +- (NSDictionary *)messageFields { + LogUnimplemented(); + return nil; +} + +- (void)setMessageFields:(NSDictionary *)messageFields { + LogUnimplemented(); +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase+Compat.h new file mode 100644 index 0000000000..3cb4d6d534 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageHTMLBase.h" + +@interface ABKInAppMessageHTMLBase () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase.m new file mode 100644 index 0000000000..ed1f7389ad --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLBase.m @@ -0,0 +1,31 @@ +#import "ABKInAppMessageHTMLBase.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageHTMLBase+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageHTMLBase + +- (NSURL *)assetsLocalDirectoryPath { + if (self.inAppMessage && self.inAppMessage.baseURL) { + return self.inAppMessage.baseURL; + } else { + NSLog(@"[BrazeKitCompat] Error: invalid in-app message baseURL, returning 'localhost' as assetsLocalDirectoryPath."); + return [NSURL URLWithString:@"localhost"]; + } +} + +- (void)setAssetsLocalDirectoryPath:(NSURL *)assetsLocalDirectoryPath { + self.inAppMessage.baseURL = assetsLocalDirectoryPath; +} + +- (void)logInAppMessageHTMLClickWithButtonID:(NSString *)buttonId { + [self.inAppMessage.context logClickWithButtonId:buttonId]; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull+Compat.h new file mode 100644 index 0000000000..162921b3d7 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageHTMLFull.h" + +@interface ABKInAppMessageHTMLFull () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull.m new file mode 100644 index 0000000000..fc4b539126 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageHTMLFull.m @@ -0,0 +1,20 @@ +#import "ABKInAppMessageHTMLFull.h" +#import "../BRZLog.h" +#import "ABKInAppMessageHTMLFull+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +@implementation ABKInAppMessageHTMLFull + +- (NSURL *)assetsZipRemoteUrl { + LogUnimplemented(); + return nil; +} + +- (void)setAssetsZipRemoteUrl:(NSURL *)assetsZipRemoteUrl { + LogUnimplemented(); +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive+Compat.h new file mode 100644 index 0000000000..41f5fb49f3 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageImmersive.h" + +@interface ABKInAppMessageImmersive () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive.m new file mode 100644 index 0000000000..ef5bbe493a --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageImmersive.m @@ -0,0 +1,124 @@ +#import "ABKInAppMessageImmersive.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageButton+Compat.h" +#import "ABKInAppMessageImmersive+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageImmersive + +- (NSString *)header { + return self.inAppMessage.header; +} + +- (void)setHeader:(NSString *)header { + self.inAppMessage.header = header; +} + +- (UIColor *)headerTextColor { + return self.inAppMessage.headerTextColor.uiColor; +} + +- (void)setHeaderTextColor:(UIColor *)headerTextColor { + self.inAppMessage.headerTextColor = + [[BRZInAppMessageRawColor alloc] init:headerTextColor]; +} + +- (UIColor *)closeButtonColor { + return self.inAppMessage.closeButtonColor.uiColor; +} + +- (void)setCloseButtonColor:(UIColor *)closeButtonColor { + self.inAppMessage.closeButtonColor = + [[BRZInAppMessageRawColor alloc] init:closeButtonColor]; +} + +- (NSArray *)buttons { + NSMutableArray *buttons = [NSMutableArray array]; + for (BRZInAppMessageRawButton *button in self.inAppMessage.buttons) { + [buttons addObject:[[ABKInAppMessageButton alloc] initWithButton:button]]; + } + return [buttons copy]; +} + +- (UIColor *)frameColor { + return self.inAppMessage.frameColor.uiColor; +} + +- (void)setFrameColor:(UIColor *)frameColor { + self.inAppMessage.frameColor = + [[BRZInAppMessageRawColor alloc] init:frameColor]; +} + +- (NSTextAlignment)headerTextAlignment { + BOOL leftToRight = + [UIApplication sharedApplication].userInterfaceLayoutDirection == + UIUserInterfaceLayoutDirectionLeftToRight; + switch (self.inAppMessage.headerTextAlignment) { + case BRZInAppMessageRawTextAlignmentStart: + return NSTextAlignmentNatural; + case BRZInAppMessageRawTextAlignmentCenter: + return NSTextAlignmentCenter; + case BRZInAppMessageRawTextAlignmentEnd: + return leftToRight ? NSTextAlignmentRight : NSTextAlignmentLeft; + default: + return NSTextAlignmentNatural; + } +} + +- (void)setHeaderTextAlignment:(NSTextAlignment)headerTextAlignment { + BOOL leftToRight = + [UIApplication sharedApplication].userInterfaceLayoutDirection == + UIUserInterfaceLayoutDirectionLeftToRight; + BRZInAppMessageRawTextAlignment alignment; + switch (headerTextAlignment) { + case NSTextAlignmentNatural: + case NSTextAlignmentJustified: + alignment = leftToRight ? BRZInAppMessageRawTextAlignmentStart + : BRZInAppMessageRawTextAlignmentEnd; + break; + case NSTextAlignmentCenter: + alignment = BRZInAppMessageRawTextAlignmentCenter; + break; + case NSTextAlignmentRight: + alignment = BRZInAppMessageRawTextAlignmentEnd; + break; + case NSTextAlignmentLeft: + alignment = BRZInAppMessageRawTextAlignmentStart; + break; + default: + alignment = BRZInAppMessageRawTextAlignmentStart; + break; + } + self.inAppMessage.headerTextAlignment = alignment; +} + +- (ABKInAppMessageImmersiveImageStyle)imageStyle { + return (ABKInAppMessageImmersiveImageStyle)self.inAppMessage.imageStyle; +} + +- (void)setImageStyle:(ABKInAppMessageImmersiveImageStyle)imageStyle { + self.inAppMessage.imageStyle = (BRZInAppMessageRawImageStyle)imageStyle; +} + +- (void)logInAppMessageClickedWithButtonID:(NSInteger)buttonId { + [self.inAppMessage.context logClickWithButtonId:[@(buttonId) stringValue]]; +} + +- (void)setInAppMessageButtons:(NSArray *)buttonArray { + NSMutableArray *buttons = [NSMutableArray array]; + for (ABKInAppMessageButton *button in buttonArray) { + if (![button isKindOfClass:[ABKInAppMessageButton class]]) { + continue; + } + [buttons addObject:button.button]; + } + self.inAppMessage.buttons = buttons; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal+Compat.h new file mode 100644 index 0000000000..12056f6819 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageModal.h" + +@interface ABKInAppMessageModal () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal.m new file mode 100644 index 0000000000..813bee5869 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageModal.m @@ -0,0 +1,11 @@ +#import "ABKInAppMessageModal.h" +#import "ABKInAppMessageModal+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation ABKInAppMessageModal + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup+Compat.h new file mode 100644 index 0000000000..e8003143bb --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup+Compat.h @@ -0,0 +1,5 @@ +#import "ABKInAppMessageSlideup.h" + +@interface ABKInAppMessageSlideup () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup.m new file mode 100644 index 0000000000..d58fcd007c --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageSlideup.m @@ -0,0 +1,42 @@ +#import "ABKInAppMessageSlideup.h" +#import "ABKInAppMessage+Compat.h" +#import "ABKInAppMessageSlideup+Compat.h" +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKInAppMessageSlideup + +- (BOOL)hideChevron { + return self.inAppMessage._compat_hideChevron; +} + +- (void)setHideChevron:(BOOL)hideChevron { + self.inAppMessage._compat_hideChevron = hideChevron; +} + +- (ABKInAppMessageSlideupAnchor)inAppMessageSlideupAnchor { + return (ABKInAppMessageSlideupAnchor)self.inAppMessage.slideFrom; +} + +- (void)setInAppMessageSlideupAnchor: + (ABKInAppMessageSlideupAnchor)inAppMessageSlideupAnchor { + self.inAppMessage.slideFrom = + (BRZInAppMessageRawSlideFrom)inAppMessageSlideupAnchor; +} + +- (UIColor *)chevronColor { + return self.inAppMessage.closeButtonColor.uiColor; +} + +- (void)setChevronColor:(UIColor *)chevronColor { + self.inAppMessage.closeButtonColor = + [[BRZInAppMessageRawColor alloc] init:chevronColor]; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge+Compat.h new file mode 100644 index 0000000000..046bb9fe5b --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge+Compat.h @@ -0,0 +1,13 @@ +#import "ABKInAppMessageWebViewBridge.h" + +#if !TARGET_OS_TV + +@class BRZWebViewBridgeScriptMessageHandler; + +@interface ABKInAppMessageWebViewBridge () + +@property(strong, nonatomic) BRZWebViewBridgeScriptMessageHandler *handler; + +@end + +#endif diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge.m b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge.m new file mode 100644 index 0000000000..b51546f9af --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKInAppMessageWebViewBridge.m @@ -0,0 +1,92 @@ +#import "ABKInAppMessageWebViewBridge.h" +#import "ABKInAppMessageWebViewBridge+Compat.h" +#import "Appboy+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +#if !TARGET_OS_TV + +@import BrazeKit; +@import WebKit; + +@interface ABKInAppMessageWebViewBridge () + +@property(nonatomic, weak) WKWebView *webView; + +@end + +@implementation ABKInAppMessageWebViewBridge + +- (instancetype)initWithWebView:(WKWebView *)webView + inAppMessage:(ABKInAppMessageHTML *)inAppMessage + appboyInstance:(Appboy *)appboy { + self = [super init]; + if (self) { + self.webView = webView; + __weak typeof(self) weakSelf = self; + __weak typeof(inAppMessage) weakMessage = inAppMessage; + // Create handler + BRZWebViewBridgeScriptMessageHandler *handler = + [[BRZWebViewBridgeScriptMessageHandler alloc] + initWithLogClick:^(NSString *_Nullable buttonId) { + if (buttonId && buttonId.length > 0) { + [weakMessage logInAppMessageHTMLClickWithButtonID:buttonId]; + } else { + [weakMessage logInAppMessageClicked]; + } + } + logError:^(NSError *_Nonnull error) { + NSLog(@"[BrazeKitCompat] ABKInAppMessageWebViewBridge Error: %@", + error.localizedDescription); + } + showNewsFeed:^{ + if ([weakSelf.delegate respondsToSelector:@selector + (webViewBridge:receivedClickAction:)]) { + [weakSelf.delegate + webViewBridge:weakSelf + receivedClickAction:ABKInAppMessageDisplayNewsFeed]; + } + } + closeMessage:^{ + if ([weakSelf.delegate respondsToSelector:@selector + (closeMessageWithWebViewBridge:)]) { + [weakSelf.delegate closeMessageWithWebViewBridge:weakSelf]; + } + } + braze:appboy.braze]; + self.handler = handler; + + // Attach to the web view + WKUserContentController *userContentController = + webView.configuration.userContentController; + [userContentController + addUserScript:BRZWebViewBridgeScriptMessageHandler.script]; + [userContentController + addScriptMessageHandler:handler + name:BRZWebViewBridgeScriptMessageHandler.name]; + } + return self; +} + +- (void)dealloc { + WKUserContentController *userContentController = + self.webView.configuration.userContentController; + [userContentController removeAllUserScripts]; + [userContentController + removeScriptMessageHandlerForName:BRZWebViewBridgeScriptMessageHandler + .name]; +} + +- (void)userContentController: + (nonnull WKUserContentController *)userContentController + didReceiveScriptMessage:(nonnull WKScriptMessage *)message { + // Nothing to do here, `ABKInAppMessageWebViewBridge` is a + // `WKScriptMessageHandler` for historical reasons. +} + +@end + +#endif + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager+Compat.h new file mode 100644 index 0000000000..45ee0bbe39 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager+Compat.h @@ -0,0 +1,11 @@ +#import "ABKLocationManager.h" + +@class Braze; + +@interface ABKLocationManager () + +@property(strong, nonatomic) Braze *braze; + +- (instancetype)initWithBraze:(Braze *)braze; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager.m b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager.m new file mode 100644 index 0000000000..0d1cfc0b44 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManager.m @@ -0,0 +1,38 @@ +#import "ABKLocationManager.h" +#import "../BRZLog.h" +#import "ABKLocationManager+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKLocationManager + +- (BOOL)enableLocationTracking { + return self.braze.configuration.location.automaticLocationCollection; +} + +- (BOOL)enableGeofences { + return self.braze.configuration.location.geofencesEnabled; +} + +- (BOOL)disableAutomaticGeofenceRequests { + return !self.braze.configuration.location.automaticGeofenceRequests; +} + +- (void)logSingleLocation { + LogUnimplemented(); +} + +- (instancetype)initWithBraze:(Braze *)braze { + self = [super init]; + if (self) { + _braze = braze; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider+Compat.h new file mode 100644 index 0000000000..5cf7a66e6e --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider+Compat.h @@ -0,0 +1,5 @@ +#import "ABKLocationManagerProvider.h" + +@interface ABKLocationManagerProvider () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider.m b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider.m new file mode 100644 index 0000000000..e35b7422a0 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKLocationManagerProvider.m @@ -0,0 +1,18 @@ +#import "ABKLocationManagerProvider.h" +#import "ABKLocationManagerProvider+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation ABKLocationManagerProvider + ++ (BOOL)locationServicesEnabled { +#if !TARGET_OS_TV + return YES; +#endif + return NO; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKLogLevel+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKLogLevel+Compat.h new file mode 100644 index 0000000000..e365f992ba --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKLogLevel+Compat.h @@ -0,0 +1,6 @@ +typedef enum { + ABKLogDebug = 1, + ABKLogWarn = 2, + ABKLogError = 4, + ABKDoNotLog = 8, +} ABKLogLevel; diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController+Compat.h new file mode 100644 index 0000000000..4f6d3a00f9 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController+Compat.h @@ -0,0 +1,13 @@ +#import "ABKModalWebViewController.h" + +#if !TARGET_OS_TV + +@class BRZWebViewController; + +@interface ABKModalWebViewController () + +@property(strong, nonnull) BRZWebViewController *webViewController; + +@end + +#endif diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController.m b/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController.m new file mode 100644 index 0000000000..18a56d0263 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKModalWebViewController.m @@ -0,0 +1,53 @@ +#import "ABKModalWebViewController.h" +#import "../BRZLog.h" +#import "ABKModalWebViewController+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +#if !TARGET_OS_TV + +@import BrazeKit; + +@implementation ABKModalWebViewController + +- (NSURL *)url { + return self.webViewController.url; +} + +- (void)setUrl:(NSURL *)url { + self.webViewController.url = url; +} + +- (WKWebView *)webView { + LogUnimplemented(); + return nil; +} + +- (void)setWebView:(WKWebView *)webView { + LogUnimplemented(); +} + +- (UIProgressView *)progressBar { + LogUnimplemented(); + return nil; +} + +- (void)setProgressBar:(UIProgressView *)progressBar { + LogUnimplemented(); +} + +- (instancetype)init { + BRZWebViewController *webViewController = [[BRZWebViewController alloc] init]; + self = [super initWithRootViewController:webViewController]; + if (self) { + _webViewController = webViewController; + } + return self; +} + +@end + +#endif + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization+Compat.h new file mode 100644 index 0000000000..f378420069 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization+Compat.h @@ -0,0 +1,5 @@ +#import "ABKNoConnectionLocalization.h" + +@interface ABKNoConnectionLocalization () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization.m b/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization.m new file mode 100644 index 0000000000..b9301640bb --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKNoConnectionLocalization.m @@ -0,0 +1,17 @@ +#import "ABKNoConnectionLocalization.h" +#import "ABKNoConnectionLocalization+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKNoConnectionLocalization + ++ (NSString *)getNoConnectionLocalizedString { + return [Braze _localize:@"braze.webview.no-connection"]; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils+Compat.h new file mode 100644 index 0000000000..a217c54dc4 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils+Compat.h @@ -0,0 +1,9 @@ +#import "ABKPushUtils.h" + +#if !TARGET_OS_TV + +@interface ABKPushUtils () + +@end + +#endif diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils.m b/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils.m new file mode 100644 index 0000000000..105f8e1de7 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKPushUtils.m @@ -0,0 +1,99 @@ +#import "ABKPushUtils.h" +#import "../BRZLog.h" +#import "ABKPushUtils+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +#if !TARGET_OS_TV + +@import BrazeKit; + +static NSString *const ABKApsPushPayloadKey = @"aps"; +static NSString *const ABKContentAvailablePushPayloadKey = @"content-available"; +static NSString *const ABKAppboyPushPayloadKey = @"ab"; +static NSString *const ABKSyncGeofencesPushPayloadKey = @"ab_sync_geos"; +static NSString *const ABKFetchTestTriggerPushPayloadKey = + @"ab_push_fetch_test_triggers_key"; +static NSString *const ABKUninstallTrackingPushPayloadKey = + @"appboy_uninstall_tracking"; +static NSString *const ABKPushStoryPayloadKey = @"ab_carousel"; +static NSString *const ABKContentCardPushPayloadKey = @"ab_cd"; + +@implementation ABKPushUtils + ++ (BOOL)isAppboyUserNotification:(UNNotificationResponse *)response { + return [self isAppboyRemoteNotification:response.notification.request.content + .userInfo]; +} + ++ (BOOL)isAppboyRemoteNotification:(NSDictionary *)userInfo { + NSDictionary *ab = userInfo[ABKAppboyPushPayloadKey]; + return [ab isKindOfClass:[NSDictionary class]] && ab.count > 0; +} + ++ (BOOL)isAppboyInternalRemoteNotification:(NSDictionary *)userInfo { + return [self isUninstallTrackingRemoteNotification:userInfo] || + [self isGeofencesSyncRemoteNotification:userInfo]; +} + ++ (BOOL)isUninstallTrackingUserNotification:(UNNotificationResponse *)response { + return + [self isUninstallTrackingRemoteNotification:response.notification.request + .content.userInfo]; +} + ++ (BOOL)isUninstallTrackingRemoteNotification:(NSDictionary *)userInfo { + return [userInfo[ABKUninstallTrackingPushPayloadKey] boolValue]; +} + ++ (BOOL)isGeofencesSyncUserNotification:(UNNotificationResponse *)response { + return [self isGeofencesSyncRemoteNotification:response.notification.request + .content.userInfo]; +} + ++ (BOOL)isGeofencesSyncRemoteNotification:(NSDictionary *)userInfo { + NSDictionary *appboyDict = userInfo[ABKAppboyPushPayloadKey]; + return [appboyDict[ABKSyncGeofencesPushPayloadKey] boolValue]; +} + ++ (BOOL)isAppboySilentRemoteNotification:(NSDictionary *)userInfo { + if (![self isAppboyRemoteNotification:userInfo]) { + return NO; + } + + NSDictionary *aps = userInfo[ABKApsPushPayloadKey]; + if (!([aps isKindOfClass:[NSDictionary class]] && aps.count > 0)) { + return NO; + } + + return [aps[ABKContentAvailablePushPayloadKey] intValue] == 1; +} + ++ (BOOL)isPushStoryRemoteNotification:(NSDictionary *)userInfo { + NSDictionary *appboyDict = userInfo[ABKAppboyPushPayloadKey]; + return [appboyDict[ABKPushStoryPayloadKey] boolValue]; +} + ++ (BOOL)notificationContainsContentCard:(NSDictionary *)userInfo { + return userInfo[ABKAppboyPushPayloadKey][ABKContentCardPushPayloadKey] != nil; +} + ++ (BOOL)shouldFetchTestTriggersFlagContainedInPayload:(NSDictionary *)userInfo { + return [userInfo[ABKFetchTestTriggerPushPayloadKey] boolValue]; +} + ++ (NSSet *)getAppboyUNNotificationCategorySet { + return BRZNotifications.categories; +} + ++ (NSSet *) + getAppboyUIUserNotificationCategorySet { + LogUnimplemented(); + return [NSSet set]; +} + +@end +#endif + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy+Compat.h new file mode 100644 index 0000000000..2919c7c9e5 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy+Compat.h @@ -0,0 +1,5 @@ +#import "ABKSDWebImageProxy.h" + +@interface ABKSDWebImageProxy () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy.m b/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy.m new file mode 100644 index 0000000000..bec85d70cb --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSDWebImageProxy.m @@ -0,0 +1,57 @@ +#import "ABKSDWebImageProxy.h" +#import "../BRZLog.h" +#import "ABKSDWebImageProxy+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation ABKSDWebImageProxy + ++ (void)setImageForView:(UIImageView *)imageView + showActivityIndicator:(BOOL)showActivityIndicator + withURL:(NSURL *)imageURL + imagePlaceHolder:(UIImage *)placeHolder + completed:(void (^)(UIImage *_Nullable, NSError *_Nullable, + NSInteger, NSURL *_Nullable))completion { + LogUnimplemented(); +} + ++ (void)loadImageWithURL:(NSURL *)url + options:(NSInteger)options + completed:(void (^)(UIImage *_Nonnull, NSData *_Nonnull, + NSError *_Nonnull, NSInteger, BOOL, + NSURL *_Nonnull))completion { + LogUnimplemented(); +} + ++ (void)diskImageExistsForURL:(NSURL *)url + completed:(void (^)(BOOL))completion { + LogUnimplemented(); +} + ++ (NSString *)cacheKeyForURL:(NSURL *)url { + LogUnimplemented(); + return nil; +} + ++ (void)removeSDWebImageForKey:(NSString *)key { + LogUnimplemented(); +} + ++ (UIImage *)imageFromCacheForKey:(NSString *)key { + LogUnimplemented(); + return nil; +} + ++ (void)clearSDWebImageCache { + LogUnimplemented(); +} + ++ (BOOL)isSupportedSDWebImageVersion { + LogUnimplemented(); + return NO; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError+Compat.h new file mode 100644 index 0000000000..81f1ec0bfd --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError+Compat.h @@ -0,0 +1,12 @@ +#import "ABKSdkAuthenticationError.h" + +@class BRZSDKAuthenticationError; + +@interface ABKSdkAuthenticationError () + +@property(strong, nonatomic) BRZSDKAuthenticationError *error; + +- (instancetype)initWithSDKAuthenticationError: + (BRZSDKAuthenticationError *)error; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError.m b/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError.m new file mode 100644 index 0000000000..775c8219df --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSdkAuthenticationError.m @@ -0,0 +1,38 @@ +#import "ABKSdkAuthenticationError.h" +#import "ABKSdkAuthenticationError+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKSdkAuthenticationError + +- (NSInteger)code { + return self.error.code; +} + +- (NSString *)reason { + return self.error.reason; +} + +- (NSString *)userId { + return self.error.userId; +} + +- (NSString *)signature { + return self.error.signature; +} + +- (instancetype)initWithSDKAuthenticationError: + (BRZSDKAuthenticationError *)error { + self = [super init]; + if (self) { + _error = error; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata+Compat.h new file mode 100644 index 0000000000..0027e030b1 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata+Compat.h @@ -0,0 +1 @@ +#import "ABKSdkMetadata.h" diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata.m b/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata.m new file mode 100644 index 0000000000..db5452dfcd --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKSdkMetadata.m @@ -0,0 +1,39 @@ +#import "ABKSdkMetadata.h" +#import "ABKSdkMetadata+Compat.h" + +@import BrazeKit; + +ABKSdkMetadata const ABKSdkMetadataAdjust = @"adj"; +ABKSdkMetadata const ABKSdkMetadataAirBridge = @"air"; +ABKSdkMetadata const ABKSdkMetadataAppsFlyer = @"apf"; +ABKSdkMetadata const ABKSdkMetadataBluedot = @"blt"; +ABKSdkMetadata const ABKSdkMetadataBranch = @"brc"; +ABKSdkMetadata const ABKSdkMetadataCordova = @"cdva"; +ABKSdkMetadata const ABKSdkMetadataCarthage = @"cg"; +ABKSdkMetadata const ABKSdkMetadataCocoaPods = @"coco"; +ABKSdkMetadata const ABKSdkMetadataCordovaPM = @"cvpm"; +ABKSdkMetadata const ABKSdkMetadataExpo = @"expo"; +ABKSdkMetadata const ABKSdkMetadataFoursquare = @"fsq"; +ABKSdkMetadata const ABKSdkMetadataFlutter = @"ft"; +ABKSdkMetadata const ABKSdkMetadataGoogleTagManager = @"gg"; +ABKSdkMetadata const ABKSdkMetadataGimbal = @"gmb"; +ABKSdkMetadata const ABKSdkMetadataGradle = @"gd"; +ABKSdkMetadata const ABKSdkMetadataIonic = @"ionc"; +ABKSdkMetadata const ABKSdkMetadataKochava = @"kch"; +ABKSdkMetadata const ABKSdkMetadataManual = @"manu"; +ABKSdkMetadata const ABKSdkMetadataMParticle = @"mp"; +ABKSdkMetadata const ABKSdkMetadataNativeScript = @"ns"; +ABKSdkMetadata const ABKSdkMetadataNPM = @"npm"; +ABKSdkMetadata const ABKSdkMetadataNuGet = @"nugt"; +ABKSdkMetadata const ABKSdkMetadataPub = @"pub"; +ABKSdkMetadata const ABKSdkMetadataRadar = @"rdr"; +ABKSdkMetadata const ABKSdkMetadataReactNative = @"rn"; +ABKSdkMetadata const ABKSdkMetadataSegment = @"sg"; +ABKSdkMetadata const ABKSdkMetadataSingular = @"sng"; +ABKSdkMetadata const ABKSdkMetadataSwiftPM = @"spm"; +ABKSdkMetadata const ABKSdkMetadataTealium = @"tl"; +ABKSdkMetadata const ABKSdkMetadataUnreal = @"un"; +ABKSdkMetadata const ABKSdkMetadataUnityPM = @"unpm"; +ABKSdkMetadata const ABKSdkMetadataUnity = @"ut"; +ABKSdkMetadata const ABKSdkMetadataVizbee = @"vzb"; +ABKSdkMetadata const ABKSdkMetadataXamarin = @"xam"; diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard+Compat.h new file mode 100644 index 0000000000..6b9b516fdf --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard+Compat.h @@ -0,0 +1,5 @@ +#import "ABKTextAnnouncementCard.h" + +@interface ABKTextAnnouncementCard () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard.m b/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard.m new file mode 100644 index 0000000000..57e9cd0bed --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKTextAnnouncementCard.m @@ -0,0 +1,33 @@ +#import "ABKTextAnnouncementCard.h" +#import "ABKCard+Compat.h" +#import "ABKTextAnnouncementCard+Compat.h" + +@import BrazeKit; + +@implementation ABKTextAnnouncementCard + +- (NSString *)title { + return self.card.title; +} + +- (void)setTitle:(NSString *)title { + self.card.title = title; +} + +- (NSString *)cardDescription { + return self.card.cardDescription; +} + +- (void)setCardDescription:(NSString *)cardDescription { + self.card.cardDescription = cardDescription; +} + +- (NSString *)domain { + return self.card.domain; +} + +- (void)setDomain:(NSString *)domain { + self.card.domain = domain; +} + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser+Compat.h new file mode 100644 index 0000000000..5a89783436 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser+Compat.h @@ -0,0 +1,5 @@ +#import "ABKTwitterUser.h" + +@interface ABKTwitterUser () + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser.m b/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser.m new file mode 100644 index 0000000000..55539408e8 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKTwitterUser.m @@ -0,0 +1,11 @@ +#import "ABKTwitterUser.h" +#import "ABKTwitterUser+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@implementation ABKTwitterUser + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKUser+Compat.h b/Sources/BrazeKitCompat/AppboyKit/ABKUser+Compat.h new file mode 100644 index 0000000000..4b5219b757 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKUser+Compat.h @@ -0,0 +1,11 @@ +#import "ABKUser.h" + +@class BRZUser; + +@interface ABKUser () + +@property(strong, nonatomic) BRZUser *user; + +- (instancetype)initWithUser:(BRZUser *)user; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/ABKUser.m b/Sources/BrazeKitCompat/AppboyKit/ABKUser.m new file mode 100644 index 0000000000..f57719903d --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/ABKUser.m @@ -0,0 +1,267 @@ +#import "ABKUser.h" +#import "ABKAttributionData+Compat.h" +#import "ABKUser+Compat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation ABKUser + +- (NSString *)firstName { + return self.user.firstName; +} + +- (void)setFirstName:(NSString *)firstName { + [self.user setFirstName:firstName]; +} + +- (NSString *)lastName { + return self.user.lastName; +} + +- (void)setLastName:(NSString *)lastName { + [self.user setLastName:lastName]; +} + +- (NSString *)email { + return self.user.email; +} + +- (void)setEmail:(NSString *)email { + [self.user setEmail:email]; +} + +- (NSDate *)dateOfBirth { + return self.user.dateOfBirth; +} + +- (void)setDateOfBirth:(NSDate *)dateOfBirth { + [self.user setDateOfBirth:dateOfBirth]; +} + +- (NSString *)country { + return self.user.country; +} + +- (void)setCountry:(NSString *)country { + [self.user setCountry:country]; +} + +- (NSString *)homeCity { + return self.user.homeCity; +} + +- (void)setHomeCity:(NSString *)homeCity { + [self.user setHomeCity:homeCity]; +} + +- (NSString *)language { + return self.user.language; +} + +- (void)setLanguage:(NSString *)language { + [self.user setLanguage:language]; +} + +- (NSString *)userID { + return self.user.userID; +} + +- (NSString *)avatarImageURL { + return self.user.avatarImageURL; +} + +- (void)setAvatarImageURL:(NSString *)avatarImageURL { + [self.user setAvatarImageURL:avatarImageURL]; +} + +- (ABKFacebookUser *)facebookUser { + return self.user.facebookUser; +} + +- (void)setFacebookUser:(ABKFacebookUser *)facebookUser { + [self.user setFacebookUser:facebookUser]; +} + +- (ABKTwitterUser *)twitterUser { + return self.user.twitterUser; +} + +- (void)setTwitterUser:(ABKTwitterUser *)twitterUser { + [self.user setTwitterUser:twitterUser]; +} + +- (ABKAttributionData *)attributionData { + // Cannot really be made available on BRZUser + return nil; +} + +- (void)setAttributionData:(ABKAttributionData *)attributionData { + [self.user setAttributionData:attributionData.data]; +} + +- (BOOL)addAlias:(NSString *)alias withLabel:(NSString *)label { + [self.user addAlias:alias label:label]; + return YES; +} + +- (BOOL)setGender:(ABKUserGenderType)gender { + BRZUserGender *brzGender; + switch (gender) { + case ABKUserGenderMale: + brzGender = BRZUserGender.male; + break; + case ABKUserGenderFemale: + brzGender = BRZUserGender.female; + break; + case ABKUserGenderOther: + brzGender = BRZUserGender.other; + break; + case ABKUserGenderUnknown: + brzGender = BRZUserGender.unknown; + break; + case ABKUserGenderNotApplicable: + brzGender = BRZUserGender.notApplicable; + break; + default: + brzGender = BRZUserGender.unknown; + break; + } + [self.user setGender:brzGender]; + return YES; +} + +- (BOOL)setEmailNotificationSubscriptionType: + (ABKNotificationSubscriptionType)emailNotificationSubscriptionType { + [self.user setEmailSubscriptionState:(BRZUserSubscriptionState) + emailNotificationSubscriptionType]; + return YES; +} + +- (BOOL)setPushNotificationSubscriptionType: + (ABKNotificationSubscriptionType)pushNotificationSubscriptionType { + [self.user setPushNotificationSubscriptionState: + (BRZUserSubscriptionState)pushNotificationSubscriptionType]; + return YES; +} + +- (BOOL)addToSubscriptionGroupWithGroupId:(NSString *)groupId { + [self.user addToSubscriptionGroupWithGroupId:groupId]; + return YES; +} + +- (BOOL)removeFromSubscriptionGroupWithGroupId:(NSString *)groupId { + [self.user removeFromSubscriptionGroupWithGroupId:groupId]; + return YES; +} + +- (BOOL)setCustomAttributeWithKey:(NSString *)key andBOOLValue:(BOOL)value { + [self.user setCustomAttributeWithKey:key boolValue:value]; + return YES; +} + +- (BOOL)setCustomAttributeWithKey:(NSString *)key + andIntegerValue:(NSInteger)value { + [self.user setCustomAttributeWithKey:key intValue:value]; + return YES; +} + +- (BOOL)setCustomAttributeWithKey:(NSString *)key andDoubleValue:(double)value { + [self.user setCustomAttributeWithKey:key doubleValue:value]; + return YES; +} + +- (BOOL)setCustomAttributeWithKey:(NSString *)key + andStringValue:(NSString *)value { + [self.user setCustomAttributeWithKey:key stringValue:value]; + return YES; +} + +- (BOOL)setCustomAttributeWithKey:(NSString *)key andDateValue:(NSDate *)value { + [self.user setCustomAttributeWithKey:key dateValue:value]; + return YES; +} + +- (BOOL)unsetCustomAttributeWithKey:(NSString *)key { + [self.user unsetCustomAttributeWithKey:key]; + return YES; +} + +- (BOOL)incrementCustomUserAttribute:(NSString *)key { + [self.user incrementCustomUserAttribute:key]; + return YES; +} + +- (BOOL)incrementCustomUserAttribute:(NSString *)key + by:(NSInteger)incrementValue { + [self.user incrementCustomUserAttribute:key by:incrementValue]; + return YES; +} + +- (BOOL)addToCustomAttributeArrayWithKey:(NSString *)key + value:(NSString *)value { + [self.user addToCustomAttributeArrayWithKey:key value:value]; + return YES; +} + +- (BOOL)removeFromCustomAttributeArrayWithKey:(NSString *)key + value:(NSString *)value { + [self.user removeFromCustomAttributeArrayWithKey:key value:value]; + return YES; +} + +- (BOOL)setCustomAttributeArrayWithKey:(NSString *)key + array:(NSArray *)valueArray { + [self.user setCustomAttributeArrayWithKey:key array:valueArray]; + return YES; +} + +- (BOOL)setLastKnownLocationWithLatitude:(double)latitude + longitude:(double)longitude + horizontalAccuracy:(double)horizontalAccuracy { + [self.user setLastKnownLocationWithLatitude:latitude + longitude:longitude + horizontalAccuracy:horizontalAccuracy]; + return YES; +} + +- (BOOL)setLastKnownLocationWithLatitude:(double)latitude + longitude:(double)longitude + horizontalAccuracy:(double)horizontalAccuracy + altitude:(double)altitude + verticalAccuracy:(double)verticalAccuracy { + [self.user setLastKnownLocationWithLatitude:latitude + longitude:longitude + altitude:altitude + horizontalAccuracy:horizontalAccuracy + verticalAccuracy:verticalAccuracy]; + return YES; +} + +- (BOOL)addLocationCustomAttributeWithKey:(NSString *)key + latitude:(double)latitude + longitude:(double)longitude { + [self.user setLocationCustomAttributeWithKey:key + latitude:latitude + longitude:longitude]; + return YES; +} + +- (BOOL)removeLocationCustomAttributeWithKey:(NSString *)key { + [self.user unsetLocationCustomAttributeWithKey:key]; + return YES; +} + +- (instancetype)initWithUser:(BRZUser *)user { + self = [super init]; + if (self) { + _user = user; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/AppboyKit/Appboy+Compat.h b/Sources/BrazeKitCompat/AppboyKit/Appboy+Compat.h new file mode 100644 index 0000000000..9f53e94e48 --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/Appboy+Compat.h @@ -0,0 +1,11 @@ +#import "Appboy.h" + +@class Braze; + +@interface Appboy () + +@property(strong, nonatomic) Braze *braze; + +- (instancetype)initWithBraze:(Braze *)braze; + +@end diff --git a/Sources/BrazeKitCompat/AppboyKit/Appboy.m b/Sources/BrazeKitCompat/AppboyKit/Appboy.m new file mode 100644 index 0000000000..29922abc0b --- /dev/null +++ b/Sources/BrazeKitCompat/AppboyKit/Appboy.m @@ -0,0 +1,297 @@ +#import "Appboy.h" +#import "ABKContentCardsController+Compat.h" +#import "ABKLocationManager+Compat.h" +#import "ABKUser+Compat.h" +#import "Appboy+Compat.h" +#import "_ABKBRZCompat.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" + +@import BrazeKit; + +@implementation Appboy + ++ (Appboy *)sharedInstance { + return _ABKBRZCompat.shared.appboy; +} + ++ (Appboy *)unsafeInstance { + if (_ABKBRZCompat.shared.appboy == nil) { + NSString *exceptionReason = + @"[Appboy unsafeInstance] called before Braze initialized. Please call " + "[Appboy startWithApiKey:inApplication:withLaunchOptions:] before " + "using the unsafeInstance. If you " + "cannot guarantee Braze will be initialized before accessing the " + "unsafeInstance, please use " + "[Appboy sharedInstance] instead."; + NSException *appboyException = [NSException + exceptionWithName:@"InstanceAccessedBeforeInitializedException" + reason:exceptionReason + userInfo:nil]; + @throw appboyException; + } + return _ABKBRZCompat.shared.appboy; +} + ++ (void)startWithApiKey:(NSString *)apiKey + inApplication:(UIApplication *)application + withLaunchOptions:(NSDictionary *)launchOptions { + [Braze startWithApiKey:apiKey + inApplication:application + withLaunchOptions:launchOptions + withAppboyOptions:nil]; +} + ++ (void)startWithApiKey:(NSString *)apiKey + inApplication:(UIApplication *)application + withLaunchOptions:(NSDictionary *)launchOptions + withAppboyOptions:(NSDictionary *)appboyOptions { + [Braze startWithApiKey:apiKey + inApplication:application + withLaunchOptions:launchOptions + withAppboyOptions:appboyOptions]; +} + +- (ABKFeedController *)feedController { + return self.braze.feedController; +} + +- (ABKContentCardsController *)contentCardsController { + return self.braze.contentCardsController; +} + +- (ABKRequestProcessingPolicy)requestProcessingPolicy { + return (ABKRequestProcessingPolicy)self.braze.requestProcessingPolicy; +} + +- (void)setRequestProcessingPolicy: + (ABKRequestProcessingPolicy)requestProcessingPolicy { + self.braze.requestProcessingPolicy = + (_COMPAT_ABKRequestProcessingPolicy)requestProcessingPolicy; +} + +- (id)idfaDelegate { + return self.braze.idfaDelegate; +} + +- (void)setIdfaDelegate:(id)idfaDelegate { + self.braze.idfaDelegate = idfaDelegate; +} + +- (id)sdkAuthenticationDelegate { + return self.braze.sdkAuthenticationDelegate; +} + +- (void)setSdkAuthenticationDelegate: + (id)sdkAuthenticationDelegate { + self.braze.sdkAuthenticationDelegate = sdkAuthenticationDelegate; +} + +#if !TARGET_OS_TV + +- (ABKInAppMessageController *)inAppMessageController { + return self.braze.inAppMessageController; +} + +- (ABKLocationManager *)locationManager { + return self.braze.locationManager; +} + +- (id)appboyUrlDelegate { + return self.braze.appboyUrlDelegate; +} + +- (void)setAppboyUrlDelegate:(id)appboyUrlDelegate { + self.braze.appboyUrlDelegate = appboyUrlDelegate; +} + +- (id)imageDelegate { + return self.braze.imageDelegate; +} + +- (void)setImageDelegate:(id)imageDelegate { + self.braze.imageDelegate = imageDelegate; +} + +- (ABKSDKFlavor)sdkFlavor { + return (ABKSDKFlavor)self.braze.sdkFlavor; +} + +- (void)setSdkFlavor:(ABKSDKFlavor)sdkFlavor { + self.braze.sdkFlavor = (_COMPAT_ABKSDKFlavor)sdkFlavor; +} + +#endif + +- (void)requestImmediateDataFlush { + [self.braze requestImmediateDataFlush]; +} + +- (void)flushDataAndProcessRequestQueue { + [self.braze flushDataAndProcessRequestQueue]; +} + +- (void)shutdownServerCommunication { + // - not available + // [self.braze shutdownServerCommunication]; +} + +- (void)changeUser:(NSString *)userId { + [self.braze changeUser:userId]; +} + +- (void)changeUser:(NSString *)userId sdkAuthSignature:(NSString *)signature { + [self.braze changeUser:userId sdkAuthSignature:signature]; +} + +- (void)setSdkAuthenticationSignature:(NSString *)signature { + [self.braze setSDKAuthenticationSignature:signature]; +} + +- (void)unsubscribeFromSdkAuthenticationErrors { + [self.braze unsubscribeFromSdkAuthenticationErrors]; +} + +- (void)logCustomEvent:(NSString *)eventName { + [self.braze logCustomEvent:eventName]; +} + +- (void)logCustomEvent:(NSString *)eventName + withProperties:(NSDictionary *)properties { + [self.braze logCustomEvent:eventName withProperties:properties]; +} + +- (void)logPurchase:(NSString *)productIdentifier + inCurrency:(NSString *)currencyCode + atPrice:(NSDecimalNumber *)price { + [self.braze logPurchase:productIdentifier + inCurrency:currencyCode + atPrice:price]; +} + +- (void)logPurchase:(NSString *)productIdentifier + inCurrency:(NSString *)currencyCode + atPrice:(NSDecimalNumber *)price + withProperties:(NSDictionary *)properties { + [self.braze logPurchase:productIdentifier + inCurrency:currencyCode + atPrice:price + withProperties:properties]; +} + +- (void)logPurchase:(NSString *)productIdentifier + inCurrency:(NSString *)currencyCode + atPrice:(NSDecimalNumber *)price + withQuantity:(NSUInteger)quantity { + [self.braze logPurchase:productIdentifier + inCurrency:currencyCode + atPrice:price + withQuantity:quantity]; +} + +- (void)logPurchase:(NSString *)productIdentifier + inCurrency:(NSString *)currencyCode + atPrice:(NSDecimalNumber *)price + withQuantity:(NSUInteger)quantity + andProperties:(NSDictionary *)properties { + [self.braze logPurchase:productIdentifier + inCurrency:currencyCode + atPrice:price + withQuantity:quantity + andProperties:properties]; +} + +- (void)logFeedDisplayed { + [self.braze logFeedDisplayed]; +} + +- (void)logContentCardsDisplayed { + [self.braze logContentCardsDisplayed]; +} + +- (void)requestFeedRefresh { + [self.braze requestFeedRefresh]; +} + +- (void)requestContentCardsRefresh { + [self.braze requestContentCardsRefresh]; +} + +- (void)requestGeofencesWithLongitude:(double)longitude + latitude:(double)latitude { + [self.braze requestGeofencesWithLongitude:longitude latitude:latitude]; +} + +- (NSString *)getDeviceId { + return [self.braze getDeviceId]; +} + +#if !TARGET_OS_TV + +- (void)registerDeviceToken:(NSData *)deviceToken { + [self.braze registerDeviceToken:deviceToken]; +} + +- (void)registerApplication:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)notification { + [self.braze registerApplication:application + didReceiveRemoteNotification:notification]; +} + +- (void)registerApplication:(UIApplication *)application + didReceiveRemoteNotification:(NSDictionary *)notification + fetchCompletionHandler: + (void (^)(UIBackgroundFetchResult))completionHandler { + [self.braze registerApplication:application + didReceiveRemoteNotification:notification + fetchCompletionHandler:completionHandler]; +} + +- (void)getActionWithIdentifier:(NSString *)identifier + forRemoteNotification:(NSDictionary *)userInfo + completionHandler:(void (^)(void))completionHandler { +} + +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler { + [self.braze userNotificationCenter:center + didReceiveNotificationResponse:response + withCompletionHandler:completionHandler]; +} + +- (void)pushAuthorizationFromUserNotificationCenter:(BOOL)pushAuthGranted { + [self.braze pushAuthorizationFromUserNotificationCenter:pushAuthGranted]; +} + +#endif + +- (void)addSdkMetadata:(NSArray *)metadata { + [self.braze addSdkMetadata:metadata]; +} + ++ (void)wipeDataAndDisableForAppRun { + [Braze wipeDataAndDisableForAppRun]; +} + ++ (void)disableSDK { + [Braze disableSDK]; +} + ++ (void)requestEnableSDKOnNextAppRun { + [Braze requestEnableSDKOnNextAppRun]; +} + +- (instancetype)initWithBraze:(Braze *)braze { + self = [super init]; + if (self) { + _braze = braze; + _user = [[ABKUser alloc] initWithUser:braze.user]; + } + return self; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/BRZLog.h b/Sources/BrazeKitCompat/BRZLog.h new file mode 100644 index 0000000000..6c8d9f9c2f --- /dev/null +++ b/Sources/BrazeKitCompat/BRZLog.h @@ -0,0 +1,9 @@ +@import Foundation; + +#define LogUnimplemented() brzLogUnimplemented(_cmd, [self class]); + +#define LogUnimplementedMessage(M) brzLogUnimplementedMessage(M, _cmd, [self class]); + +void brzLogUnimplemented(SEL selector, Class class); + +void brzLogUnimplementedMessage(NSString *message, SEL selector, Class class); diff --git a/Sources/BrazeKitCompat/BRZLog.m b/Sources/BrazeKitCompat/BRZLog.m new file mode 100644 index 0000000000..a21edc9814 --- /dev/null +++ b/Sources/BrazeKitCompat/BRZLog.m @@ -0,0 +1,11 @@ +#import "BRZLog.h" + +void brzLogUnimplemented(SEL selector, Class class) { + NSLog(@"[BrazeKitCompat] '%@.%@' is not implemented.", + NSStringFromClass(class), NSStringFromSelector(selector)); +} + +void brzLogUnimplementedMessage(NSString *message, SEL selector, Class class) { + NSLog(@"[BrazeKitCompat] '%@.%@' is not implemented. %@", + NSStringFromClass(class), NSStringFromSelector(selector), message); +} diff --git a/Sources/BrazeKitCompat/BrazeDelegateWrapper.h b/Sources/BrazeKitCompat/BrazeDelegateWrapper.h new file mode 100644 index 0000000000..d2ade0bcdf --- /dev/null +++ b/Sources/BrazeKitCompat/BrazeDelegateWrapper.h @@ -0,0 +1,17 @@ +@import BrazeKit; +@import Foundation; + +@protocol ABKURLDelegate; +@protocol ABKSdkAuthenticationDelegate; +@protocol ABKInAppMessageControllerDelegate; + +@interface BrazeDelegateWrapper : NSObject + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +@property(strong, nonatomic) id urlDelegate; +@property(strong, nonatomic) id sdkAuthDelegate; +@property(weak, nonatomic) id inAppMessageControllerDelegate; +#pragma clang diagnostic pop + +@end diff --git a/Sources/BrazeKitCompat/BrazeDelegateWrapper.m b/Sources/BrazeKitCompat/BrazeDelegateWrapper.m new file mode 100644 index 0000000000..ddee362d23 --- /dev/null +++ b/Sources/BrazeKitCompat/BrazeDelegateWrapper.m @@ -0,0 +1,64 @@ +#import "BrazeDelegateWrapper.h" +#import "ABKInAppMessageControllerDelegate.h" +#import "ABKSdkAuthenticationDelegate.h" +#import "ABKURLDelegate.h" +#import "AppboyKit/ABKSdkAuthenticationError+Compat.h" + +@implementation BrazeDelegateWrapper + +- (BOOL)braze:(Braze *)braze shouldOpenURL:(BRZURLContext *)context { + if ([self.urlDelegate respondsToSelector:@selector + (handleAppboyURL:fromChannel:withExtras:)]) { + return ![self.urlDelegate handleAppboyURL:context.url + fromChannel:(ABKChannel)context.channel + withExtras:context.extras]; + } + return YES; +} + +- (void)braze:(Braze *)braze + sdkAuthenticationFailedWithError:(BRZSDKAuthenticationError *)error { + if ([self.sdkAuthDelegate + respondsToSelector:@selector(handleSdkAuthenticationError:)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + ABKSdkAuthenticationError *authError = [[ABKSdkAuthenticationError alloc] + initWithSDKAuthenticationError:error]; +#pragma clang diagnostic pop + [self.sdkAuthDelegate handleSdkAuthenticationError:authError]; + } +} + +- (void)braze:(Braze *)braze + noMatchingTriggerForEvent:(enum BRZTriggerEvent)event + name:(NSString *)name + properties:(NSDictionary *)properties { + if (![self.inAppMessageControllerDelegate + respondsToSelector:@selector(noMatchingTriggerForEvent:name:)]) { + return; + } + + ABKTriggerEventType abkEvent; + switch (event) { + case BRZTriggerEventSessionStart: + abkEvent = ABKTriggerEventTypeSessionStart; + break; + case BRZTriggerEventCustomEvent: + abkEvent = ABKTriggerEventTypeCustomEvent; + break; + case BRZTriggerEventPurchase: + abkEvent = ABKTriggerEventTypePurchase; + break; + case BRZTriggerEventOther: + abkEvent = ABKTriggerEventTypeOther; + break; + default: + abkEvent = ABKTriggerEventTypeOther; + break; + } + + [self.inAppMessageControllerDelegate noMatchingTriggerForEvent:abkEvent + name:name]; +} + +@end diff --git a/Sources/BrazeKitCompat/_ABKBRZCompat.m b/Sources/BrazeKitCompat/_ABKBRZCompat.m new file mode 100644 index 0000000000..fe729c55da --- /dev/null +++ b/Sources/BrazeKitCompat/_ABKBRZCompat.m @@ -0,0 +1,674 @@ +#import "ABKIDFADelegate.h" +#import "ABKURLDelegate.h" +#import "Appboy.h" +#import "AppboyKit/ABKContentCardsController+Compat.h" +#import "AppboyKit/ABKFeedController+Compat.h" +#import "AppboyKit/ABKInAppMessageController+Compat.h" +#import "AppboyKit/ABKLocationManager+Compat.h" +#import "AppboyKit/Appboy+Compat.h" +#import "BRZLog.h" +#import "BrazeDelegateWrapper.h" +#import + +#import "AppboyKit.h" +#import "AppboyKit/ABKLogLevel+Compat.h" +#import "_ABKBRZCompat.h" + +@import BrazeLocation; +@import BrazeKit; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +NSString *const ABKRequestProcessingPolicyOptionKey = + @"ABKRquestProcessingPolicy"; +NSString *const ABKFlushIntervalOptionKey = @"ABKFlushInterval"; +NSString *const ABKEnableAutomaticLocationCollectionKey = + @"ABKEnableLocationAutomaticChecking"; +NSString *const ABKEnableGeofencesKey = @"ABKEnableGeofencesKey"; +NSString *const ABKDisableAutomaticGeofenceRequestsKey = + @"ABKDisableAutomaticGeofenceRequests"; +NSString *const ABKSessionTimeoutKey = @"ABKSessionTimeout"; +NSString *const ABKMinimumTriggerTimeIntervalKey = + @"ABKMinimumTriggerTimeInterval"; +NSString *const ABKSDKFlavorKey = @"ABKSDKFlavorKey"; +NSString *const ABKDeviceWhitelistKey = @"ABKDeviceWhitelistKey"; +NSString *const ABKDeviceAllowlistKey = @"ABKDeviceAllowlistKey"; +NSString *const ABKEnableDismissModalOnOutsideTapKey = + @"ABKEnableDismissModalOnOutsideTap"; +NSString *const ABKEnableSDKAuthenticationKey = + @"ABKEnableSDKAuthenticationKey"; +NSString *const ABKIDFADelegateKey = @"ABKIDFADelegate"; +NSString *const ABKEndpointKey = @"ABKEndpointKey"; +NSString *const ABKInAppMessageControllerDelegateKey = + @"ABKInAppMessageControllerDelegate"; +NSString *const ABKURLDelegateKey = @"ABKURLDelegate"; +NSString *const ABKImageDelegateKey = @"ABKImageDelegate"; +NSString *const ABKSdkAuthenticationDelegateKey = + @"ABKSdkAuthenticationDelegate"; +NSString *const ABKEphemeralEventsKey = @"ABKEphemeralEvents"; +NSString *const ABKPushStoryAppGroupKey = @"ABKPushStoryAppGroupKey"; +NSString *const ABKLogLevelKey = @"ABKLogLevelKey"; + +static NSString *const AppboyAPIKeys = @"AppboyAPIKeys"; +static NSString *const ABKPlistSessionTimeoutKey = @"SessionTimeout"; +static NSString *const ABKPlistEnableAutomaticLocationCollectionKey = + @"EnableAutomaticLocationCollection"; +static NSString *const ABKPlistEnableGeofencesKey = @"EnableGeofences"; +static NSString *const ABKPlistDisableAutomaticGeofenceRequestsKey = + @"DisableAutomaticGeofenceRequests"; +static NSString *const ABKPlistEphemeralEventsKey = @"EphemeralEvents"; +static NSString *const ABKPlistPushStoryAppGroupKey = @"PushStoryAppGroup"; +static NSString *const ABKPlistEnableDismissModalOnOutsideTap = + @"DismissModalOnOutsideTap"; +static NSString *const ABKPlistEnableSDKAuthenticationKey = + @"EnableSDKAuthentication"; +static NSString *const ABKPlistLogLevelKey = @"LogLevel"; +static NSString *const ABKPersistentDataPlistEndpointKey = @"Endpoint"; + +@interface _ABKBRZCompat () + +@property(assign, nonatomic) BOOL enableDismissModalOnOutsideTap; +@property(strong, nonatomic, readonly) BrazeDelegateWrapper *delegateWrapper; +@property(strong, nonatomic) BRZCancellable *contentCardsSubscription; +@property(strong, nonatomic) BRZCancellable *newsFeedCardsSubscription; + +@property(strong, nonatomic, readwrite) + ABKInAppMessageController *inAppMessageController; + +@end + +@implementation _ABKBRZCompat +@synthesize feedController = _feedController; +@synthesize contentCardsController = _contentCardsController; +@synthesize delegateWrapper = _delegateWrapper; +@synthesize locationManager = _locationManager; + ++ (_ABKBRZCompat *)shared { + static id shared; + static dispatch_once_t token; + dispatch_once(&token, ^{ + shared = [[_ABKBRZCompat alloc] init]; + }); + return shared; +} + ++ (NSString *)sdkVersion { + return Braze.sdkVersion; +} + +#pragma mark - Singleton + +#pragma mark - Start + ++ (void)startWithApiKey:(NSString *)apiKey + appboyOptions:(nullable NSDictionary *)appboyOptions { + _ABKBRZCompat *shared = _ABKBRZCompat.shared; + + BRZConfiguration *configuration = + [_ABKBRZCompat configurationWithApiKey:apiKey + appboyOptions:appboyOptions]; + Braze *braze = [[Braze alloc] initWithConfiguration:configuration]; + + shared.braze = braze; + shared.appboy = [[Appboy alloc] initWithBraze:braze]; + + // Update initialized flag + shared.initialized = @(YES); + + // Delegates + // - delegateWrapper is a proxy for old delegates + braze.delegate = shared.delegateWrapper; + + // - IDFA + id idfaDelegate = appboyOptions[ABKIDFADelegateKey]; + braze.idfaDelegate = idfaDelegate; + + // - URL Delegate + id urlDelegate = appboyOptions[ABKURLDelegateKey]; + braze.appboyUrlDelegate = urlDelegate; + + // - Image Delegate + id imageDelegate = appboyOptions[ABKImageDelegateKey]; + braze.imageDelegate = imageDelegate; + + if (braze.imageDelegate == nil) { + Class ABKSDWebImageImageDelegateClass = + NSClassFromString(@"ABKSDWebImageImageDelegate"); + if (ABKSDWebImageImageDelegateClass != nil) { + braze.imageDelegate = [[ABKSDWebImageImageDelegateClass alloc] init]; + } + } + + // - SDKAuthentication Delegate + id sdkAuthenticationDelegate = + appboyOptions[ABKSdkAuthenticationDelegateKey]; + braze.sdkAuthenticationDelegate = sdkAuthenticationDelegate; + + // Notifications + __weak Braze *weakBraze = braze; + // - Content Cards + shared.contentCardsSubscription = [braze.contentCards + subscribeToUpdates:^( + __unused NSArray *_Nonnull cards) { + [NSNotificationCenter.defaultCenter + postNotificationName:ABKContentCardsProcessedNotification + object:weakBraze + userInfo:@{ + ABKContentCardsProcessedIsSuccessfulKey : @(YES) + }]; + }]; + // - NewsFeed + shared.newsFeedCardsSubscription = + [braze.newsFeed subscribeToUpdates:^( + __unused NSArray *_Nonnull cards) { + [NSNotificationCenter.defaultCenter + postNotificationName:ABKFeedUpdatedNotification + object:weakBraze + userInfo:@{ + ABKFeedUpdatedIsSuccessfulKey : @(YES) + }]; + }]; + + // In-App Messages + shared.inAppMessageController = [[ABKInAppMessageController alloc] + initWithInAppMessagePresenter:nil + delegateWrapper:shared.delegateWrapper]; + shared.inAppMessageController.enableDismissModalOnOutsideTap = + shared.enableDismissModalOnOutsideTap; + + // - Dynamically load ABKInAppMessageUIController if available + BOOL isUISetup = NO; + Class ABKInAppMessageUIController = + NSClassFromString(@"ABKInAppMessageUIController"); + if (!isUISetup && ABKInAppMessageUIController) { + id uiController = + [[ABKInAppMessageUIController alloc] init]; + shared.inAppMessageController.inAppMessageUIController = uiController; + shared.inAppMessageController.presenter = uiController; + shared.inAppMessageController.isCompatibility = YES; + braze.inAppMessagePresenter = uiController; + isUISetup = YES; + } + + // - Dynamically load BrazeInAppMessageUI if available + // ⚠️ This does not includes GIF support. GIF support must be provided + // via setting `BRZGIFViewProvider.shared`. + Class BrazeInAppMessageUI = NSClassFromString(@"BrazeInAppMessageUI"); + if (!isUISetup && BrazeInAppMessageUI) { + NSObject *uiPresenter = + [[BrazeInAppMessageUI alloc] init]; + shared.inAppMessageController.presenter = uiPresenter; + braze.inAppMessagePresenter = uiPresenter; + isUISetup = YES; + } + + // - InAppMessageController Delegate + id inAppMessageControllerDelegate = + appboyOptions[ABKInAppMessageControllerDelegateKey]; + shared.inAppMessageController.delegate = inAppMessageControllerDelegate; +} + +- (ABKFeedController *)feedController { + if (_feedController == nil) { + _feedController = + [[ABKFeedController alloc] initWithNewsFeedApi:self.braze.newsFeed]; + } + return _feedController; +} + +- (ABKContentCardsController *)contentCardsController { + if (_contentCardsController == nil) { + _contentCardsController = [[ABKContentCardsController alloc] + initWithContentCardsApi:self.braze.contentCards]; + } + return _contentCardsController; +} + +- (id)idfaDelegate { + return nil; +} + +- (void)setIdfaDelegate:(id)idfaDelegate { + if ([idfaDelegate conformsToProtocol:@protocol(ABKIDFADelegate)]) { + NSString *idfa = [idfaDelegate advertisingIdentifierString]; + BOOL trackingAuthorized = + [idfaDelegate isAdvertisingTrackingEnabledOrATTAuthorized]; + [self.braze setIdentifierForAdvertiser:idfa]; + [self.braze setAdTrackingEnabled:trackingAuthorized]; + } +} + +- (id)sdkAuthenticationDelegate { + return self.delegateWrapper.sdkAuthDelegate; +} + +- (void)setSdkAuthenticationDelegate: + (id)sdkAuthenticationDelegate { + self.delegateWrapper.sdkAuthDelegate = sdkAuthenticationDelegate; +} + +- (ABKLocationManager *)locationManager { + if (_locationManager == nil) { + _locationManager = [[ABKLocationManager alloc] initWithBraze:self.braze]; + } + return _locationManager; +} + +- (id)appboyUrlDelegate { + return self.delegateWrapper.urlDelegate; +} + +- (void)setAppboyUrlDelegate:(id)appboyUrlDelegate { + self.delegateWrapper.urlDelegate = appboyUrlDelegate; +} + +#pragma mark - Misc. + +- (BrazeDelegateWrapper *)delegateWrapper { + if (_delegateWrapper == nil) { + _delegateWrapper = [[BrazeDelegateWrapper alloc] init]; + } + return _delegateWrapper; +} + +#pragma mark - Configuration + ++ (BRZConfiguration *)configurationWithApiKey:(NSString *)apiKey + appboyOptions:(NSDictionary *)appboyOptions { + NSDictionary *plist = [_ABKBRZCompat plistDictionary]; + BRZConfiguration *configuration = + [[BRZConfiguration alloc] initWithApiKey:apiKey + endpoint:@"sdk.iad-01.braze.com"]; + + // Request Policy + ABKRequestProcessingPolicy policy = + [_ABKBRZCompat integerForKey:ABKRequestProcessingPolicyOptionKey + defaultValue:ABKAutomaticRequestProcessing + inOptions:appboyOptions + plist:plist]; + switch (policy) { + case ABKManualRequestProcessing: + configuration.api.requestPolicy = BRZRequestPolicyManual; + break; + case ABKAutomaticRequestProcessing: + configuration.api.requestPolicy = BRZRequestPolicyAutomatic; + break; + default: + break; + } + + // Flush interval + NSNumber *flushInterval = + [_ABKBRZCompat numberForKey:ABKFlushIntervalOptionKey + defaultValue:@(configuration.api.flushInterval) + inOptions:appboyOptions + plist:plist]; + configuration.api.flushInterval = [flushInterval doubleValue]; + + // Automatic Location Collection + BOOL automaticLocationCollection = [_ABKBRZCompat + booleanForKey:ABKEnableAutomaticLocationCollectionKey + defaultValue:configuration.location.automaticLocationCollection + inOptions:appboyOptions + plist:plist]; + configuration.location.automaticLocationCollection = + automaticLocationCollection; + + // Enable Geofences + BOOL enableGeofences = + [_ABKBRZCompat booleanForKey:ABKEnableGeofencesKey + defaultValue:configuration.location.geofencesEnabled + inOptions:appboyOptions + plist:plist]; + configuration.location.geofencesEnabled = enableGeofences; + + // Disable Automatic Geofences Requests + BOOL disableAutomaticGeofencesRequest = [_ABKBRZCompat + booleanForKey:ABKDisableAutomaticGeofenceRequestsKey + defaultValue:!configuration.location.automaticGeofenceRequests + inOptions:appboyOptions + plist:plist]; + configuration.location.automaticGeofenceRequests = + !disableAutomaticGeofencesRequest; + + // Automatically add BrazeLocation when needed + if (automaticLocationCollection || enableGeofences) { + configuration.location.brazeLocation = [[BrazeLocation alloc] init]; + } + + // Endpoint + NSString *endpoint = [_ABKBRZCompat stringForKey:ABKEndpointKey + defaultValue:configuration.api.endpoint + inOptions:appboyOptions + plist:plist]; + configuration.api.endpoint = endpoint; + + // EnableDismissModalOnOutsideTap + BOOL enableDismissModalOnOutsideTap = + [_ABKBRZCompat booleanForKey:ABKEnableDismissModalOnOutsideTapKey + defaultValue:NO + inOptions:appboyOptions + plist:plist]; + _ABKBRZCompat.shared.enableDismissModalOnOutsideTap = + enableDismissModalOnOutsideTap; + + // SDK Authentication + BOOL enableSDKAuthentication = + [_ABKBRZCompat booleanForKey:ABKEnableSDKAuthenticationKey + defaultValue:configuration.api.sdkAuthentication + inOptions:appboyOptions + plist:plist]; + configuration.api.sdkAuthentication = enableSDKAuthentication; + + // Session Timeout + NSInteger sessionTimeout = + [_ABKBRZCompat integerForKey:ABKSessionTimeoutKey + defaultValue:configuration.sessionTimeout + inOptions:appboyOptions + plist:plist]; + configuration.sessionTimeout = sessionTimeout; + + // Minimum Trigger TimerInterval + NSNumber *triggerMinimumTimeInterval = + [_ABKBRZCompat numberForKey:ABKMinimumTriggerTimeIntervalKey + defaultValue:@(configuration.triggerMinimumTimeInterval) + inOptions:appboyOptions + plist:plist]; + configuration.triggerMinimumTimeInterval = + [triggerMinimumTimeInterval doubleValue]; + + // SDK Flavor + ABKSDKFlavor sdkFlavor = [_ABKBRZCompat integerForKey:ABKSDKFlavorKey + defaultValue:0 + inOptions:appboyOptions + plist:plist]; + // - BRZSDKFlavor matches ABKSDKFlavor, we can directly assign here. + configuration.api.sdkFlavor = (BRZSDKFlavor)sdkFlavor; + + // Device Allow List + configuration.devicePropertyAllowList = [_ABKBRZCompat + devicePropertiesInOptions:appboyOptions + defaultValue:configuration.devicePropertyAllowList]; + + // Ephemeral Events + NSArray *ephemeralEvents = [_ABKBRZCompat arrayForKey:ABKEphemeralEventsKey + inOptions:appboyOptions + plist:plist]; + configuration.ephemeralEvents = ephemeralEvents; + + // Push Story App Group + NSString *pushStoryAppGroup = + [_ABKBRZCompat stringForKey:ABKPushStoryAppGroupKey + defaultValue:nil + inOptions:appboyOptions + plist:plist]; + configuration.push.appGroup = pushStoryAppGroup; + + // Log Level + ABKLogLevel logLevel = + (ABKLogLevel)[_ABKBRZCompat integerForKey:ABKLogLevelKey + defaultValue:ABKLogError + inOptions:appboyOptions + plist:plist]; + if (logLevel <= 0) { + logLevel = ABKLogDebug; + } + switch (logLevel) { + case ABKLogDebug: + configuration.logger.level = BRZLoggerLevelDebug; + break; + case ABKLogWarn: + configuration.logger.level = BRZLoggerLevelInfo; + break; + case ABKLogError: + configuration.logger.level = BRZLoggerLevelError; + break; + case ABKDoNotLog: + configuration.logger.level = BRZLoggerLevelDisabled; + break; + default: + break; + } + + return configuration; +} + +#pragma mark - Configuration Helpers + ++ (NSDictionary *)plistDictionary { + NSDictionary *infoPlist = [NSBundle mainBundle].infoDictionary; + NSDictionary *brazeDict = infoPlist[@"Braze"]; + NSDictionary *appboyDict = infoPlist[@"Appboy"]; + if (!brazeDict && !appboyDict) { + return nil; + } + return [_ABKBRZCompat merge:appboyDict with:brazeDict]; +} + ++ (NSString *)stringForKey:(NSString *)key + defaultValue:(NSString *)defaultValue + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + id value = options[key]; + if (value && [value isKindOfClass:[NSString class]]) { + return value; + } + value = plist[[_ABKBRZCompat plistKeyForKey:key]]; + if (value && [value isKindOfClass:[NSString class]]) { + return value; + } + return defaultValue; +} + ++ (NSInteger)integerForKey:(NSString *)key + defaultValue:(NSInteger)defaultValue + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + id value = options[key]; + if (value && [value respondsToSelector:@selector(integerValue)]) { + return [value integerValue]; + } + value = plist[[_ABKBRZCompat plistKeyForKey:key]]; + if (value && [value respondsToSelector:@selector(integerValue)]) { + return [value integerValue]; + } + return defaultValue; +} + ++ (NSNumber *)numberForKey:(NSString *)key + defaultValue:(NSNumber *)defaultValue + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + id value = options[key]; + if (value && [value respondsToSelector:@selector(doubleValue)]) { + return @([value doubleValue]); + } + value = plist[[_ABKBRZCompat plistKeyForKey:key]]; + if (value && [value respondsToSelector:@selector(doubleValue)]) { + return @([value doubleValue]); + } + return defaultValue; +} + ++ (BOOL)booleanForKey:(NSString *)key + defaultValue:(BOOL)defaultValue + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + id value = options[key]; + if (value && [value respondsToSelector:@selector(boolValue)]) { + return [value boolValue]; + } + value = plist[[_ABKBRZCompat plistKeyForKey:key]]; + if (value && [value respondsToSelector:@selector(boolValue)]) { + return [value boolValue]; + } + return defaultValue; +} + ++ (nullable NSArray *)arrayForKey:(NSString *)key + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + id value = options[key]; + if ([value isKindOfClass:[NSArray class]] && + [_ABKBRZCompat objectIsValidAndNotEmpty:value]) { + return value; + } + value = plist[[_ABKBRZCompat plistKeyForKey:key]]; + if ([value isKindOfClass:[NSArray class]] && + [_ABKBRZCompat objectIsValidAndNotEmpty:value]) { + return value; + } + return nil; +} + ++ (id)idForKey:(NSString *)key + inOptions:(NSDictionary *)options + plist:(NSDictionary *)plist { + if (options[key]) { + return options[key]; + } + return plist[[_ABKBRZCompat plistKeyForKey:key]]; +} + ++ (NSString *)plistKeyForKey:(NSString *)key { + static dispatch_once_t once; + static NSDictionary *optionsToPlistKeysMap; + dispatch_once(&once, ^{ + optionsToPlistKeysMap = @{ + ABKEndpointKey : ABKPersistentDataPlistEndpointKey, + ABKSessionTimeoutKey : ABKPlistSessionTimeoutKey, + ABKEnableDismissModalOnOutsideTapKey : + ABKPlistEnableDismissModalOnOutsideTap, + ABKEnableAutomaticLocationCollectionKey : + ABKPlistEnableAutomaticLocationCollectionKey, + ABKEnableGeofencesKey : ABKPlistEnableGeofencesKey, + ABKDisableAutomaticGeofenceRequestsKey : + ABKPlistDisableAutomaticGeofenceRequestsKey, + ABKPushStoryAppGroupKey : ABKPlistPushStoryAppGroupKey, + ABKEnableSDKAuthenticationKey : ABKPlistEnableSDKAuthenticationKey, + ABKLogLevelKey : ABKPlistLogLevelKey, + ABKEphemeralEventsKey : ABKPlistEphemeralEventsKey + }; + }); + + NSString *plistKey = optionsToPlistKeysMap[key]; + if (plistKey) { + return plistKey; + } + return key; +} + ++ (NSArray *) + devicePropertiesInOptions:(NSDictionary *)options + defaultValue:(NSArray *)defaultValue { + // Use a non-representable ABKDeviceOptions to mark an invalid default value. + NSUInteger invalid = 0x8BADF00D; + ABKDeviceOptions allowList = + [_ABKBRZCompat integerForKey:ABKDeviceAllowlistKey + defaultValue:invalid + inOptions:options + plist:nil]; + if (allowList == invalid) { + allowList = [_ABKBRZCompat integerForKey:ABKDeviceWhitelistKey + defaultValue:invalid + inOptions:options + plist:nil]; + } + + if (allowList == invalid) { + return defaultValue; + } + + NSMutableArray *deviceProperties = + [NSMutableArray array]; + if (allowList & ABKDeviceOptionResolution) { + [deviceProperties addObject:BRZDeviceProperty.resolution]; + } + if (allowList & ABKDeviceOptionCarrier) { + [deviceProperties addObject:BRZDeviceProperty.carrier]; + } + if (allowList & ABKDeviceOptionLocale) { + [deviceProperties addObject:BRZDeviceProperty.locale]; + } + if (allowList & ABKDeviceOptionModel) { + [deviceProperties addObject:BRZDeviceProperty.model]; + } + if (allowList & ABKDeviceOptionOSVersion) { + [deviceProperties addObject:BRZDeviceProperty.osVersion]; + } + if (allowList & ABKDeviceOptionIDFV) { + NSLog(@"[BrazeKitCompat] Warning: use `braze.set(identifierForVendor:)` to set the identifier for vendor."); + } + if (allowList & ABKDeviceOptionIDFA) { + NSLog(@"[BrazeKitCompat] Warning: use `braze.set(identifierForAdvertiser:)` to set the identifier for advertiser."); + } + if (allowList & ABKDeviceOptionPushEnabled) { + [deviceProperties addObject:BRZDeviceProperty.pushEnabled]; + } + if (allowList & ABKDeviceOptionTimezone) { + [deviceProperties addObject:BRZDeviceProperty.timeZone]; + } + if (allowList & ABKDeviceOptionPushAuthStatus) { + [deviceProperties addObject:BRZDeviceProperty.pushAuthStatus]; + } + if (allowList & ABKDeviceOptionAdTrackingEnabled) { + NSLog(@"[BrazeKitCompat] Warning: use `braze.set(adTrackingEnabled:)` to set the ad tracking enabled flag."); + } + if (allowList & ABKDeviceOptionPushDisplayOptions) { + [deviceProperties addObject:BRZDeviceProperty.pushDisplayOptions]; + } + + return deviceProperties; +} + ++ (BOOL)objectIsValidAndNotEmpty:(id)object { + if (object == nil || object == [NSNull null]) { + return NO; + } + if ([object isKindOfClass:[NSArray class]] || + [object isKindOfClass:[NSDictionary class]]) { + return [object count] > 0; + } + if ([object isKindOfClass:[NSString class]]) { + return [object length] > 0; + } + if ([object isKindOfClass:[NSURL class]]) { + return [[object absoluteString] length] > 0; + } + return YES; +} + ++ (NSDictionary *)merge:(NSDictionary *)dict1 with:(NSDictionary *)dict2 { + NSMutableDictionary *result = + [NSMutableDictionary dictionaryWithDictionary:dict1]; + [dict2 enumerateKeysAndObjectsUsingBlock:^(id key, id obj, + __unused BOOL *stop) { + id dict1ObjectForKey = dict1[key]; + if (dict1ObjectForKey) { + if ([obj isKindOfClass:[NSDictionary class]]) { + if ([dict1ObjectForKey isKindOfClass:[NSDictionary class]]) { + NSDictionary *newVal = [_ABKBRZCompat merge:dict1ObjectForKey + with:(NSDictionary *)obj]; + result[key] = newVal; + } else { + result[key] = obj; + } + } else { + if (![dict1ObjectForKey isKindOfClass:[NSDictionary class]] && obj) { + result[key] = obj; + } else { + result[key] = dict1ObjectForKey; + } + } + } else { + result[key] = obj; + } + }]; + + return (NSDictionary *)[result mutableCopy]; +} + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeKitCompat/include/ABKAttributionData.h b/Sources/BrazeKitCompat/include/ABKAttributionData.h new file mode 100644 index 0000000000..c3de022162 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKAttributionData.h @@ -0,0 +1,31 @@ +#import +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKAttributionData + */ +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("renamed to 'Braze.User.AttributionData' / 'BRZUserAttributionData'") +@interface ABKAttributionData : NSObject + +/*! + * @param network The attribution network + * @param campaign The attribution campaign + * @param adGroup The attribution adGroup + * @param creative The attribution creative + * + * @discussion: Creates an ABKAttributionData object to send to Braze servers. + */ +- (instancetype)initWithNetwork:(nullable NSString *)network + campaign:(nullable NSString *)campaign + adGroup:(nullable NSString *)adGroup + creative:(nullable NSString *)creative; + +@property (nonatomic, readonly, nullable) NSString *network; +@property (nonatomic, readonly, nullable) NSString *campaign; +@property (nonatomic, readonly, nullable) NSString *adGroup; +@property (nonatomic, readonly, nullable) NSString *creative; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKBannerCard.h b/Sources/BrazeKitCompat/include/ABKBannerCard.h new file mode 100644 index 0000000000..f09df60f38 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKBannerCard.h @@ -0,0 +1,27 @@ +#import "ABKCard.h" + +/* + * Braze Public API: ABKBannerCard + */ +NS_ASSUME_NONNULL_BEGIN +@interface ABKBannerCard : ABKCard + +/* + * This property is the URL of the card's image. + */ +@property (copy) NSString *image; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +/* + * This property is the aspect ratio of the card's image. It is meant to serve as a hint before + * image loading completes. Note that the property may not be supplied in certain circumstances. + */ +@property float imageAspectRatio; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKBannerContentCard.h b/Sources/BrazeKitCompat/include/ABKBannerContentCard.h new file mode 100644 index 0000000000..fb23e63893 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKBannerContentCard.h @@ -0,0 +1,18 @@ +#import "ABKContentCard.h" +#import "BrazePreprocessor.h" + +BRZ_DEPRECATED("renamed to 'Braze.ContentCard.Banner'") +@interface ABKBannerContentCard : ABKContentCard + +/* + * The URL of the card's image. + */ +@property (copy) NSString *image; + +/* + * This property is the aspect ratio of the card's image. It is meant to serve as a hint before + * image loading completes. Note that the property may not be supplied in certain circumstances. + */ +@property float imageAspectRatio; + +@end diff --git a/Sources/BrazeKitCompat/include/ABKCaptionedImageCard.h b/Sources/BrazeKitCompat/include/ABKCaptionedImageCard.h new file mode 100644 index 0000000000..b6e545164e --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKCaptionedImageCard.h @@ -0,0 +1,37 @@ +#import "ABKCard.h" + +/* + * Braze Public API: ABKCaptionedImageCard + */ +NS_ASSUME_NONNULL_BEGIN +@interface ABKCaptionedImageCard : ABKCard + +/* + * This property is the URL of the card's image. + */ +@property (copy) NSString *image; + +/* + * This property is the aspect ratio of the card's image. It is meant to serve as a hint before + * image loading completes. Note that the property may not be supplied in certain circumstances. + */ +@property float imageAspectRatio; + +/* + * The title text for the card. + */ +@property (copy) NSString *title; + +/* + * The description text for the card. + */ +@property (copy) NSString *cardDescription; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKCaptionedImageContentCard.h b/Sources/BrazeKitCompat/include/ABKCaptionedImageContentCard.h new file mode 100644 index 0000000000..9cd1336e81 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKCaptionedImageContentCard.h @@ -0,0 +1,36 @@ +#import "ABKContentCard.h" +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.ContentCard.CaptionedImage'") +@interface ABKCaptionedImageContentCard : ABKContentCard + +/* + * The URL of the card's image. + */ +@property (copy) NSString *image; + +/* + * This property is the aspect ratio of the card's image. It is meant to serve as a hint before + * image loading completes. Note that the property may not be supplied in certain circumstances. + */ +@property float imageAspectRatio; + +/* + * The title text for the card. + */ +@property (copy) NSString *title; + +/* + * The description text for the card. + */ +@property (copy) NSString *cardDescription; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKCard.h b/Sources/BrazeKitCompat/include/ABKCard.h new file mode 100644 index 0000000000..f1907dbef7 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKCard.h @@ -0,0 +1,91 @@ +#import +#import "ABKFeedController.h" + +/* + * Braze Public API: ABKCard + */ +NS_ASSUME_NONNULL_BEGIN +@interface ABKCard : NSObject + +/* + * Card's ID. + */ +@property (readonly) NSString *idString; + +/* + * This property reflects if the card is read or unread by the user. + */ +@property (nonatomic) BOOL viewed; + +/* + * The property is the unix timestamp of the card's creation time from Braze dashboard. + */ +@property (nonatomic, readonly) double created; + +/* + * The property is the unix timestamp of the card's latest update time from Braze dashboard. + */ +@property (nonatomic, readonly) double updated; + +/* + * The categories assigned to the card. + */ +@property ABKCardCategory categories; + +/* + * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card + * doesn't an expire date. + */ +@property (readonly) double expiresAt; + +/*! + * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. + * You may want to design and implement a custom handler to access this data depending on your use case. + */ +@property (strong, nullable) NSDictionary *extras; + +//Optional: +/* + * The URL string that will be opened after the card is clicked on. + */ +@property (copy, nullable) NSString *urlString; + +/*! + * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView + * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in + * an external web browser app. + * + * This property defaults to NO. + */ +@property BOOL openUrlInWebView; + +/* + * @param cardDictionary The dictionary for card deserialization. + * + * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. + * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. + */ ++ (nullable ABKCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; + +/* + * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. + */ +- (nullable NSData *)serializeToData; + +/* + * Manually log an impression to Braze for the card. + * This should only be used for custom news feed view controller. ABKFeedViewController already has card impression logging. + */ +- (void)logCardImpression; + +/* + * Manually log a click to Braze for the card. + * This should only be used for custom news feed view controller. ABKFeedViewController already has card click logging. + * The SDK will only log a card click when the card has the url property with a valid url. + */ +- (void)logCardClicked; + +- (BOOL)hasSameId:(ABKCard *)card; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKClassicCard.h b/Sources/BrazeKitCompat/include/ABKClassicCard.h new file mode 100644 index 0000000000..89c1d87df1 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKClassicCard.h @@ -0,0 +1,32 @@ +#import "ABKCard.h" + +/* + * Braze Public API: ABKClassicCard + */ +NS_ASSUME_NONNULL_BEGIN +@interface ABKClassicCard : ABKCard + +/* + * This property is the URL of the card's image. + */ +@property (copy, nullable) NSString *image; + +/* + * The description text for the card. + */ +@property (copy) NSString *cardDescription; + + +/* + * The news title text for the card. + */ +@property (copy, nullable) NSString *title; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKClassicContentCard.h b/Sources/BrazeKitCompat/include/ABKClassicContentCard.h new file mode 100644 index 0000000000..cb7e63263c --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKClassicContentCard.h @@ -0,0 +1,30 @@ +#import "ABKContentCard.h" +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.ContentCard.Classic' / 'Braze.ContentCard.ClassicImage'") +@interface ABKClassicContentCard : ABKContentCard + +/* + * The URL of the card's image. + */ +@property (copy, nullable) NSString *image; + +/* + * The news title text for the card. + */ +@property (copy) NSString *title; + +/* + * The description text for the card. + */ +@property (copy) NSString *cardDescription; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKContentCard.h b/Sources/BrazeKitCompat/include/ABKContentCard.h new file mode 100644 index 0000000000..d47d93271b --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKContentCard.h @@ -0,0 +1,114 @@ +#import +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKContentCard + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.ContentCard'") +@interface ABKContentCard : NSObject + +/*! + * Card's ID. + */ +@property (readonly) NSString *idString; + +/*! + * This property reflects if the card is read or unread by the user. + */ +@property (nonatomic) BOOL viewed; + +/*! + * The property is the unix timestamp of the card's creation time from Braze dashboard. + */ +@property (nonatomic, readonly) double created; + +/*! + * The property is the unix timestamp of the card's expiration time. When the value is less than 0, it means the card + * doesn't an expire date. + */ +@property (readonly) double expiresAt; + +/*! + * This property reflects if the card can be dismissed by the user. + */ +@property (nonatomic) BOOL dismissible; + +/*! + * This property reflects if the card has been pinned by the user. + */ +@property (nonatomic) BOOL pinned; + +/*! + * This property reflects if the card has been dimissed. + */ +@property (nonatomic) BOOL dismissed; + +/*! + * This property reflects if the card has been clicked. + */ +@property (nonatomic) BOOL clicked; + +/*! + * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. + * You may want to design and implement a custom handler to access this data depending on your use case. + */ +@property (strong, nullable) NSDictionary *extras; + +/*! + * This property is set to YES if the instance represents a test content card + */ +@property (nonatomic, readonly) BOOL isTest; + +/*! + * The URL string that will be opened after the card is clicked on. + */ +@property (copy, nullable) NSString *urlString; + +/*! + * When the card's urlString is not nil, if the property is set to YES, the URL will be opened in a modal WKWebView + * inside the app. If this property is set to NO, the URL will be opened by the OS and web URLs will be opened in + * an external web browser app. + * + * This property defaults to NO. + */ +@property BOOL openUrlInWebView; + +/*! + * @param cardDictionary The dictionary for card deserialization. + * + * Deserializes the dictionary to a card for use by wrappers such as Braze's Unity SDK for iOS. + * When the deserialization isn't successful, this method returns nil; otherwise, it returns the deserialized card. + */ ++ (nullable ABKContentCard *)deserializeCardFromDictionary:(nullable NSDictionary *)cardDictionary; + +/*! + * Serializes the card to binary data for use by wrappers such as Braze's Unity SDK for iOS. + */ +- (nullable NSData *)serializeToData; + +/*! + * Manually log an impression to Braze for the card. + * This should only be used for custom content card view controllers. + */ +- (void)logContentCardImpression; + +/*! + * Manually log a click to Braze for the card. + * This should only be used for custom contentcard view controllers. + */ +- (void)logContentCardClicked; + +/*! + * Manually dismiss a card. + * Sets the card's `dismissed` property to YES and logs the dismissal to Braze. + * Only has effect if the card is dismissible and if the `dismissed` property is currently set to NO. + */ +- (void)logContentCardDismissed; + +- (BOOL)isControlCard; + +- (BOOL)hasSameId:(ABKContentCard *)card; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKContentCardsController.h b/Sources/BrazeKitCompat/include/ABKContentCardsController.h new file mode 100644 index 0000000000..e0f5452c98 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKContentCardsController.h @@ -0,0 +1,75 @@ +#import +#import "BrazePreprocessor.h" + +/* ------------------------------------------------------------------------------------------------------ + * Notifications + */ + +/*! + * When Content Cards are updated, Braze will post a notification through the NSNotificationCenter. + * The name of the notification is the string constant referred to by ABKContentCardsProcessedNotification. The + * userInfo dictionary associated with the notification will has one object, with key the same string + * as ABKContentCardsProcessedIsSuccessfulKey, to indicate whether the update is successful or not. + * + * To listen for this notification, you would register an object as an observer of the notification + * using something like: + * + *
+ *   [[NSNotificationCenter defaultCenter] addObserver:self
+ *                                            selector:@selector(contentCardsUpdatedNotificationReceived:)
+ *                                                name:ABKContentCardsProcessedNotification
+ *                                              object:nil];
+ * 
+ * + * where "contentCardsUpdatedNotificationReceived:" is your callback method for handling the notification: + * + *
+ *   - (void)contentCardsUpdatedNotificationReceived:(NSNotification *)notification {
+ *     BOOL updateIsSuccessful = [notification.userInfo[ABKContentCardsProcessedIsSuccessfulKey] boolValue];
+ *     < Check if update was successful and do something in response to the notification >
+ *   }
+ * 
+ */ +NS_ASSUME_NONNULL_BEGIN + +extern NSString *const ABKContentCardsProcessedNotification; +extern NSString *const ABKContentCardsProcessedIsSuccessfulKey; + +/* + * Braze Public API: ABKContentCardsController + */ +BRZ_DEPRECATED("renamed to 'Braze.ContentCards'") +@interface ABKContentCardsController : NSObject + +/*! + * The latest content cards that are saved in memory and disk. + */ +@property (readonly, getter=getContentCards) NSArray *contentCards; + +/*! + * The NSDate object that indicates the last time the contentCards property was updated from Braze server. + */ +@property (readonly, nullable) NSDate *lastUpdate; + +/*! + * Returns the count of unviewed cards, excluding control cards. + * A "view" happens when a card becomes visible in the Content Cards view. This differentiates + * between cards which are off-screen in the scrolling view, and those which + * are on-screen; when a card scrolls onto the screen, it's counted as viewed. + * + * Cards are counted as viewed only once -- if a card scrolls off the screen and + * back on, it's not re-counted. + * + * Cards are counted only once even if they appear in multiple Content Cards views or across multiple devices. + */ +- (NSInteger)unviewedContentCardCount; + +/*! + * Returns the count of available cards, including control cards. + * Cards are counted only once even if they appear in multiple Content Cards views. + */ +- (NSInteger)contentCardCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKFacebookUser.h b/Sources/BrazeKitCompat/include/ABKFacebookUser.h new file mode 100644 index 0000000000..0fd24d4bfb --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKFacebookUser.h @@ -0,0 +1,38 @@ +#import +#import "ABKUser.h" +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN +extern NSInteger const DefaultNumberOfFriends; + +/* + * Braze Public API: ABKFacebookUser + */ +BRZ_DEPRECATED("Facebook user data is not supported by Braze anymore.") +@interface ABKFacebookUser : NSObject + +/*! + * @param facebookUserDictionary The dictionary returned from facebook with facebook graph api endpoint "/me". Please + * refer to https://developers.facebook.com/docs/graph-api/reference/v4.0/user for more information. + * @param numberOfFriends The length of the friends array from facebook. You can get the array from the dictionary returned + * from facebook with facebook graph api endpoint "/me/friends", under the key "data". Please refer to + * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/friends for more information. + * @param likes The array of user's facebook likes from facebook. You can get the array from the dictionary returned + * from facebook with facebook graph api endpoint "/me/likes", under the key "data"; Please refer to + * https://developers.facebook.com/docs/graph-api/reference/v4.0/user/likes for more information. + * + * @discussion: This method is to generate a ABKFacebookUser so you can pass the user's facebook account data to Braze. + * After a ABKFacebookUser object is generated, you can check the value of properties but you cannot change it. + * If you want to update the user's facebook data, you need to generate a new ABKFacebookUser instance and set it as + * [Appboy sharedInstance].user.facebookUser. + */ +- (instancetype)initWithFacebookUserDictionary:(nullable NSDictionary *)facebookUserDictionary + numberOfFriends:(NSInteger)numberOfFriends + likes:(nullable NSArray *)likes; + +@property (readonly, nullable) NSDictionary *facebookUserDictionary; +@property (readonly) NSInteger numberOfFriends; +@property (readonly, nullable) NSArray *likes; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKFeedController.h b/Sources/BrazeKitCompat/include/ABKFeedController.h new file mode 100644 index 0000000000..2c55e6469e --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKFeedController.h @@ -0,0 +1,99 @@ +#import + +/* ------------------------------------------------------------------------------------------------------ + * Notifications + */ + +/*! + * When the news feed is updated, Braze will post a notification through the NSNotificationCenter. + * The name of the notification is the string constant referred to by ABKFeedUpdatedNotification. The + * userInfo dictionary associated with the notification will has one object, with key the same string + * as ABKFeedUpdatedIsSuccessfulKey, to indicate whether the update is successful or not. + * + * To listen for this notification, you would register an object as an observer of the notification + * using something like: + * + *
+ *   [[NSNotificationCenter defaultCenter] addObserver:self
+ *                                            selector:@selector(feedUpdatedNotificationReceived:)
+ *                                                name:ABKFeedUpdatedNotification
+ *                                              object:nil];
+ * 
+ * + * where "feedUpdatedNotificationReceived:" is your callback method for handling the notification: + * + *
+ *   - (void)feedUpdatedNotificationReceived:(NSNotification *)notification {
+ *     BOOL updateIsSuccessful = [notification.userInfo[ABKFeedUpdatedIsSuccessfulKey] boolValue];
+ *     < Do something in response to the notification >
+ *   }
+ * 
+ */ +NS_ASSUME_NONNULL_BEGIN +extern NSString *const ABKFeedUpdatedNotification; +extern NSString *const ABKFeedUpdatedIsSuccessfulKey; + +/* ------------------------------------------------------------------------------------------------------ + * Enums + */ + +/*! +* Values representing the news feed cards' categories recognized by the SDK. +*/ +typedef NS_OPTIONS(NSUInteger, ABKCardCategory) { + ABKCardCategoryNoCategory = 1 << 0, + ABKCardCategoryNews = 1 << 1, + ABKCardCategoryAdvertising = 1 << 2, + ABKCardCategoryAnnouncements = 1 << 3, + ABKCardCategorySocial = 1 << 4, + ABKCardCategoryAll = 1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 +}; + +/* + * Braze Public API: ABKFeedController + */ +@interface ABKFeedController : NSObject + +/*! + * The latest cards of the Braze News Feed saved in memory and disk. Right now the available card types are ABKBannerCard, + * ABKCaptionedImageCard, ABKClassicCard and ABKTextAnnouncementCard. They are all subclasses + * of ABKCard. + */ +@property (readonly, getter=getNewsFeedCards) NSArray *newsFeedCards; + +/*! + * The NSDate object that indicates the last time the newsFeedCards property was updated from the Braze server. + */ +@property (readonly, nullable) NSDate *lastUpdate; + +/*! + * This method returns the number of currently active cards which have not been viewed in the given categories. + * A "view" happens when a card becomes visible in the feed view. This differentiates + * between cards which are off-screen in the scrolling view, and those which + * are on-screen; when a card scrolls onto the screen, it's counted as viewed. + * + * Cards are counted as viewed only once -- if a card scrolls off the screen and + * back on, it's not re-counted. + * + * Cards are counted only once even if they appear in multiple feed views or across multiple devices. + */ +- (NSInteger)unreadCardCountForCategories:(ABKCardCategory)categories; + +/*! + * This method returns the total number of currently active cards belongs to given categories. Cards are + * counted only once even if they appear in multiple feed views. + */ +- (NSInteger)cardCountForCategories:(ABKCardCategory)categories; + +/*! + * @param categories An ABKCardCategory indicating the categories that you want to get. You can pass more than one category + * at one time by using "|" to separate categories like: ABKCardCategoryNews | ABKCardCategoryAnnouncements | ABKCardCategorySocial + * @return An array of cards of the given categories. + * + * @discussion This method will find the cards of given categories and return them. + * When the given categories don't exist in any card, this method will return an empty array. + */ +- (NSArray *)getCardsInCategories:(ABKCardCategory)categories; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKIDFADelegate.h b/Sources/BrazeKitCompat/include/ABKIDFADelegate.h new file mode 100644 index 0000000000..44e220e2ad --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKIDFADelegate.h @@ -0,0 +1,32 @@ +#import +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKAppboyIDFADelegate + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("use 'Braze.set(identifierForAdvertiser:)' and 'Braze.set(adTrackingEnabled:)' instead.") +@protocol ABKIDFADelegate +/*! + * Asks the delegate to return a valid IDFA for the current user. + * + * Use this delegate to pass the IDFA to Braze. Braze does not collect IDFA automatically. + * + * @return The current users's IDFA UUID. + */ +- (NSString *)advertisingIdentifierString; + +/*! + * Asks the delegate to return whether advertising tracking is enabled for the current user. + * + * Your delegate implementation should use ATTrackingManager on iOS 14+ and ASIdentifierManager on earlier iOS versions. + * + * An example implementation is available here: + * https://github.com/Appboy/appboy-ios-sdk/blob/master/Example/Stopwatch/Sources/Utils/IDFADelegate.m + * + * @return YES if advertising tracking is enabled for iOS 14 and earlier or if AppTrackingTransparency (ATT) is authorized with iOS 14+, NO otherwise + */ +- (BOOL)isAdvertisingTrackingEnabledOrATTAuthorized; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKImageDelegate.h b/Sources/BrazeKitCompat/include/ABKImageDelegate.h new file mode 100644 index 0000000000..e68f15c185 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKImageDelegate.h @@ -0,0 +1,44 @@ +#import +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN +/* + * This delegate protocol gives the Braze iOS SDK access to the image framework. + */ + +typedef NS_OPTIONS(NSUInteger, ABKImageOptions ) { + ABKImageOptionsRetryFailed = 1 << 0, + ABKImageOptionsLowPriority = 1 << 1, + ABKImageOptionsCacheMemoryOnly = 1 << 2, + ABKImageOptionsProgressiveDownload = 1 << 3, + ABKImageOptionsRefreshCached = 1 << 4, + ABKImageOptionsContinueInBackground = 1 << 5, + ABKImageOptionsHandleCookies = 1 << 6, +}; + +BRZ_DEPRECATED("ABKImageDelegate is not needed anymore") +@protocol ABKImageDelegate + +- (void)setImageForView:(UIImageView *)imageView + showActivityIndicator:(BOOL)showActivityIndicator + withURL:(nullable NSURL *)imageURL + imagePlaceHolder:(nullable UIImage *)placeHolder + completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion; + +- (void)loadImageWithURL:(nullable NSURL *)url + options:(ABKImageOptions)options + completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion; + +- (void)diskImageExistsForURL:(nullable NSURL *)url + completed:(nullable void (^)(BOOL isInCache))completion; + +- (nullable UIImage *)imageFromCacheForURL:(nullable NSURL *)url; + +/*! + * @discussion Returns a class that is UIImageView or a subclass of UIImageView to allow the implementor to bring their own + * implementation of animated image support. + */ +- (Class)imageViewClass; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessage+Compat.h b/Sources/BrazeKitCompat/include/ABKInAppMessage+Compat.h new file mode 100644 index 0000000000..7f62867726 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessage+Compat.h @@ -0,0 +1,11 @@ +#import "ABKInAppMessage.h" + +@class BRZInAppMessageRaw; + +@interface ABKInAppMessage () + +@property(strong, nonatomic) BRZInAppMessageRaw *inAppMessage; + +- (instancetype)initWithInAppMessage:(BRZInAppMessageRaw *)inAppMessage; + +@end diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessage.h b/Sources/BrazeKitCompat/include/ABKInAppMessage.h new file mode 100644 index 0000000000..3cd4ee7d28 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessage.h @@ -0,0 +1,248 @@ +#import +#import +#import "BrazePreprocessor.h" + +@class ABKInAppMessageDarkTheme; + +/*! + * The ABKInAppMessageClickActionType defines the action that will be performed when the in-app message is clicked. + * + * ABKInAppMessageDisplayNewsFeed - This is the default behavior. It will open a modal view of Braze news feed. + * + * ABKInAppMessageRedirectToURI - The in-app message will try to redirect to the uri defined by the uri property. Only when the uri + * is an HTTP URL, a modal web view will be displayed. If the uri is a protocol uri, the in-app message will redirect to the + * protocol uri. + * + * ABKInAppMessageNoneClickAction - The in-app message will do nothing but dismiss itself. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageClickActionType) { + ABKInAppMessageDisplayNewsFeed, + ABKInAppMessageRedirectToURI, + ABKInAppMessageNoneClickAction +}; + +/*! + * The ABKInAppMessageDismissType defines how the in-app message can be dismissed. + * + * ABKInAppMessageDismissAutomatically - This is the default behavior for ABKInAppMessageSlideup. + * It will dismiss after the length of time defined by the duration property. + * ABKInAppMessageSlideup of this type can also be dismissed by swiping. + * + * ABKInAppMessageDismissManually - This is the default behavior for ABKInAppMessageImmersive. The + * in-app message will stay on the screen indefinitely unless dismissed by swiping or a click on + * the close button. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageDismissType) { + ABKInAppMessageDismissAutomatically, + ABKInAppMessageDismissManually +}; + +/*! + * The ABKInAppMessageOrientation defines preferred screen orientation for the in-app message. + * + * ABKInAppMessageOrientationAny - This is the default value for an in-app message's orientation. This + * value allows the in-app message display in any orientation. + * + * ABKInAppMessageOrientationPortrait - This value will limit the in-app message to only display in + * protrait and portrait upside down orientation. + * + * ABKInAppMessageOrientationLandscape - This value will limit the in-app message to only display in + * landscape orientation, including landscape left and landscape right. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageOrientation) { + ABKInAppMessageOrientationAny, + ABKInAppMessageOrientationPortrait, + ABKInAppMessageOrientationLandscape +}; + +/*! + * Default icon and in-app message button background colors. + * These are used in the in-app message view controllers. + */ +static CGFloat const RedValueOfDefaultIconColorAndButtonBgColor = (CGFloat)0.0f; +static CGFloat const GreenValueOfDefaultIconColorAndButtonBgColor = (CGFloat)(115.0f / 255.0f); +static CGFloat const BlueValueOfDefaultIconColorAndButtonBgColor = (CGFloat)(213.0f / 255.0f); +static CGFloat const AlphaValueOfDefaultIconColorAndButtonBgColor = (CGFloat)1.0f; + +/* + * Braze Public API: ABKInAppMessage + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage'") +@interface ABKInAppMessage : NSObject + +/*! + * This property defines the message displayed within the in-app message. + */ +@property (copy) NSString *message; + +/*! + * This property carries extra data in the form of an NSDictionary which can be sent down via the Braze Dashboard. + * You may want to design and implement a custom handler to access this data depending on your use-case. + */ +@property (strong, nullable) NSDictionary *extras; + +/*! + * This property defines the number of seconds before the in-app message is automatically dismissed. + */ +@property (nonatomic) NSTimeInterval duration; + +/*! + * This property defines the action that will be performed when the in-app message is clicked. + * See the ABKInAppMessageClickActionType enum documentation above offers additional details. + */ +@property (readonly) ABKInAppMessageClickActionType inAppMessageClickActionType; + +/*! + * When the in-app message's inAppMessageClickActionType is ABKInAppMessageRedirectToURI, clicking on the in-app message will redirect to the uri defined + * in this property. + * + * This property can be a HTTP URI or a protocol URI. + */ +@property (readonly, copy, nullable) NSURL *uri; + +/*! + * When the in-app message's inAppMessageClickActionType is ABKInAppMessageRedirectToURI, if the property is set to YES, + * the URI will be opened in a modal WKWebView inside the app. If this property is set to NO, the URI will be opened by + * the OS and web URIs will be opened in an external web browser app. + * + * This property defaults to YES on ABKInAppMessageHTML subclasses and NO on all other ABKInAppMessage subclasses. + */ +@property BOOL openUrlInWebView; + +/*! + * inAppMessageDismissType defines the dismissal behavior of the in-app message. + * See the above documentation for ABKInAppMessageDismissType for additional details. + */ +@property ABKInAppMessageDismissType inAppMessageDismissType; + +/*! + * backgroundColor defines the background color of the in-app message. The default background color is black with 0.9 alpha for + * ABKInAppMessageSlideup, and white with 1.0 alpha for ABKInAppMessageModal and ABKInAppMessageFull. + */ +@property (nonatomic, strong, nullable) UIColor *backgroundColor; + +/*! + * textColor defines the message text color of the in-app message. The default text color is black. + */ +@property (nonatomic, strong, nullable) UIColor *textColor; + +/*! + * icon the unicode string of the Font Awesome icon for this in-app message. + * + * You may add Font Awesome icons to in-app messages from the Braze dashboard. + */ +@property (nonatomic, copy, nullable) NSString *icon; + +/*! + * iconColor defines the font color of icon property. + * The default font color is white. + */ +@property (nonatomic, strong, nullable) UIColor *iconColor; + +/*! + * iconBackgroundColor defines the background color of icon property. + * * The default background color's RGB values are R:0 G:115 B:213. + */ +@property (nonatomic, strong, nullable) UIColor *iconBackgroundColor; + +/*! + * This boolean determines if the in-app message will attempt to use dark theme colors, granted the device + * is in dark mode and the fields are present in the response. + * + * @discussion The default of this value is YES but can be overriden in `beforeInAppMessageDisplayed:` + * to ensure that the dark theme is disabled for any given in-app message. + */ +@property (nonatomic, assign) BOOL enableDarkTheme; + +/*! + * Data model that contains all the dark theme color info for any visible views, including any buttons + * that may be present. + */ +@property (nonatomic, strong, nullable) ABKInAppMessageDarkTheme *darkTheme; + +/*! + * An optional UIUserInterfaceStyle that can be used to force dark or light mode. + * + * @discussion The default value will not override OS settings but can + * be overriden in `beforeInAppMessageDisplayed:` + * to ensure that the dark or light theme is used for any given in-app message. + * This property is of type NSInteger to avoid any iOS version dependencies. + */ +@property (nonatomic) NSInteger overrideUserInterfaceStyle; + +/*! + * imageURI defines the URI of the image icon on in-app message. + * When there is a iconImage defined, the iconImage will be used and the value of property icon will + * be ignored. + */ +@property (copy, nullable) NSURL *imageURI; + +/*! + * imageContentMode defines the content mode of the image on in-app message. + * For immersive in-app messages, the imageContentMode defines both the image icon and the graphic + * image's content mode. + * + * The imageContentMode default values are: + * Slideup: UIViewContentModeScaleAspectFit + * Modal: UIViewContentModeScaleAspectFit + * Full: UIViewContentModeScaleAspectFill + */ +@property UIViewContentMode imageContentMode; + +/*! + * orientation defines the preferred screen orientation for the in-app message. + * In-app messages will only display if the preferred orientation matches the current status bar + * orientation. However, there is an important exception for iPads. For in-app messages that + * have a preferred orientation and are being displayed on an iPad, the in-app message will appear + * in the style of the preferred orientation regardless of actual screen orientation. + */ +@property ABKInAppMessageOrientation orientation; + +/*! + * messageTextAlignment defines the preferred text alignment of the message label. + * The default values are: + * Slideup: NSTextAlignmentNatural + * Modal: NSTextAlignmentCenter + * Full: NSTextAlignmentCenter + */ +@property NSTextAlignment messageTextAlignment; + +/* + * animateIn/animateOut define if the in-app message should be animated in/out on the screen when + * displaying/dismissing. The default value is YES. + */ +@property BOOL animateIn; +@property BOOL animateOut; + +/*! + * isControl defines whether this in-app message is a control. Control in-app messages should not be displayed to users. + */ +@property BOOL isControl; + +/*! + * If you're handling in-app messages completely on your own, you should still report + * impressions and clicks on the in-app message back to Braze with these methods so that your campaign reporting features + * still work in the dashboard. + * + * Note: Each in-app message can log at most one impression and at most one click. + */ +- (void)logInAppMessageImpression; +- (void)logInAppMessageClicked; + +/*! + * This method will set the inAppMessageClickActionType property. + * + * When clickActionType is ABKInAppMessageRedirectToURI, the parameter uri cannot be nil. When clickActionType is + * ABKInAppMessageDisplayNewsFeed or ABKInAppMessageNoneClickAction, the parameter uri will be ignored, and property uri + * will be set to nil. + */ +- (void)setInAppMessageClickAction:(ABKInAppMessageClickActionType)clickActionType withURI:(nullable NSURL *)uri; + +/*! + * Serializes the in-app message to binary data for use by wrappers such as Braze's Unity SDK for iOS. + */ +- (nullable NSData *)serializeToData; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageButton.h b/Sources/BrazeKitCompat/include/ABKInAppMessageButton.h new file mode 100644 index 0000000000..0f60b35fcf --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageButton.h @@ -0,0 +1,79 @@ +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageButton + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Button'") +@interface ABKInAppMessageButton : NSObject + +/*! + * This property defines the button title text in UIControlStateNormal. Setting this property will also change the button + * title text. + */ +@property (copy, nullable) NSString *buttonText; + +/*! + * This property defines the button's background color. + */ +@property (strong, nullable) UIColor *buttonBackgroundColor; + +/*! + * This property defines the button's border color. + * If this property is not sent from the server, the background color is used. + */ +@property (strong, nullable) UIColor *buttonBorderColor; + +/*! + * This property defines the button's title color in UIControlStateNormal. Setting this property will also change the + * button title color. + */ +@property (strong, nullable) UIColor *buttonTextColor; + +/*! + * This property defines the button title font in UIControlStateNormal. Please set this property before the in-app message + * is displayed, or the displayed in-app message will not apply the font. + */ +@property (copy, nullable) UIFont *buttonTextFont; + +/*! + * This property defines the action that will be performed when the button is clicked. + * See the ABKInAppMessageClickActionType enum documentation in ABKInAppMessage.h offers additional details. + */ +@property (readonly) ABKInAppMessageClickActionType buttonClickActionType; + +/*! + * When the button's buttonClickActionType is ABKInAppMessageRedirectToURI, clicking on the button will redirect to the uri + * defined in this property. + * + * This property can be a HTTP URI or a protocol URI. + */ +@property (readonly, copy, nullable) NSURL *buttonClickedURI; + +/*! + * When the button's buttonClickActionType is ABKInAppMessageRedirectToURI, if the property is set to YES, + * the URI will be opened in a modal WKWebView inside the app. If this property is set to NO, the URI will be opened by + * the OS and web URIs will be opened in an external web browser app. + * + * This property defaults to NO. + */ +@property BOOL buttonOpenUrlInWebView; + +/*! + * This property defines the button's ID. Button's ID is used to track user's clicking action and used for corresponding + * data analytics. + */ +@property (readonly) NSInteger buttonID; + +/*! + * This method will set the buttonClickActionType property. + * + * When clickActionType is ABKInAppMessageRedirectToURI, the parameter uri cannot be nil, and the value will be passed to + * buttonClickedURI. When clickActionType is ABKInAppMessageDisplayNewsFeed or ABKInAppMessageNoneClickAction, the + * parameter uri will be ignored, and property uri will be set to nil. + */ +- (void)setButtonClickAction:(ABKInAppMessageClickActionType)clickActionType withURI:(nullable NSURL *)uri; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageControl.h b/Sources/BrazeKitCompat/include/ABKInAppMessageControl.h new file mode 100644 index 0000000000..a5bbc5104a --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageControl.h @@ -0,0 +1,10 @@ +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageControl + */ +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Control'") +@interface ABKInAppMessageControl : ABKInAppMessage + +@end diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageController.h b/Sources/BrazeKitCompat/include/ABKInAppMessageController.h new file mode 100644 index 0000000000..6dec091c02 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageController.h @@ -0,0 +1,83 @@ +#import +#import "ABKInAppMessage.h" +#import "ABKInAppMessageControllerDelegate.h" +#import "ABKInAppMessageUIControlling.h" +#import "BrazePreprocessor.h" + +/*! Note: This class is not thread safe and all class methods should be called from the main thread.*/ + +/* + * Braze Public API: ABKInAppMessageController + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'BrazeInAppMessageUI' (BrazeUI module)") +@interface ABKInAppMessageController : NSObject + +/*! + * Setting the delegate allows your app to control how, when, and if in-app messages are displayed. + * Your app can set the delegate to override the default behavior of the ABKInAppMessageController. See + * ABKInAppMessageControllerDelegate.h for more information. + */ +@property (weak, nonatomic, nullable) id delegate; + +/*! + * If you have implemented the In-App Message subspec, you can use the ABKInAppMessageUIController to control + * in-app message behavior. See ABKInAppMessageUIController for more information. + */ +@property (strong, nonatomic, nullable) id inAppMessageUIController; + +/*! + * This boolean determines if modal in-app messages will be dismissed when the user taps outside of the + * in-app message. + * + * @discussion The default of this value is NO but can be overriden by setting the value of ABKEnableDismissModalOnOutsideTapKey in + * appboyOptions or in the Braze dictionary in your Info.plist file. + */ +@property BOOL enableDismissModalOnOutsideTap; + +/*! + * @param delegate The in-app message delegate that implements the ABKInAppMessageControllerDelegate methods. If the delegate is + * nil, it acts as one which always returns ABKDisplayInAppMessageNow and doesn't implement all other delegate methods. + * + * @discussion This method grabs the next in-app message from the in-app message stack, if there is one, and displays it with + * the provided delegate. The delegate must return a ABKInAppMessageDisplayChoice that defines how the in-app message will be + * handled. Please refer to the ABKInAppMessageDisplayChoice enum documentation for more detailed information. + * + * If there are no in-app messages available this returns immediately having taken no action. + */ +- (void)displayNextInAppMessageWithDelegate:(nullable id)delegate __deprecated_msg("Please use 'displayNextInAppMessage' instead."); + +/*! + * Displays the next in-app message from the in-app message stack. + * + * This method pops the next in-app message from the in-app message stack and tries to displays it. + * When defined, the current delegate methods are executed to respect any custom behavior. + */ +- (void)displayNextInAppMessage; + +/*! + * @return The number of in-app messages that are locally waiting to be displayed. + * + * @discussion Use this method to check how many in-app messages are waiting to be displayed and call + * displayNextInAppMessageWithDelegate: at to display it. If an in-app message is currently being displayed, it will not be included + * in the count. + * + * Note: Returning ABKDisplayInAppMessageLater in the beforeInAppMessageDisplayed: delegate method will put the in-app message back onto + * the stack and this will be reflected in inAppMessagesRemainingOnStack. + */ +- (NSInteger)inAppMessagesRemainingOnStack; + +/*! + * @discussion This method allows you to request display of an in-app message. It adds the in-app message object to the top of the in-app message stack + * and tries to display it immediately. + * + * If you add an ABKInAppMessage instance that you received through a Braze delegate method - i.e. one that is associated with a campaign or Canvas, + * then impression and click analytics will work automatically. If you add an ABKInAppMessage instance that you instantiated yourself programmatically + * (uncommon), then analytics will not be available. + * + * @param newInAppMessage the in-app message to add. + */ +- (void)addInAppMessage:(ABKInAppMessage *)newInAppMessage; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageControllerDelegate.h b/Sources/BrazeKitCompat/include/ABKInAppMessageControllerDelegate.h new file mode 100644 index 0000000000..6e6bc692ea --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageControllerDelegate.h @@ -0,0 +1,88 @@ +#import +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN +/*! + * Possible values for in-app message handling after a in-app message is offered to an ABKInAppMessageControllerDelegate + * ABKDisplayInAppMessageNow - The in-app message will be displayed immediately. + * ABKDisplayInAppMessageLater - The in-app message will be not be displayed and will be placed back onto the top of the stack. + * ABKDiscardInAppMessage - The in-app message will be discarded and will not be displayed. + * + * The following conditions can cause a in-app message to be offered to the delegate defined by the delegate property on + * [Appboy sharedInstance].inAppMessageController: + * - A in-app message is received from the Braze server. + * - A in-app message is waiting to display when an UIApplicationDidBecomeActiveNotification event occurs. + * - A in-app message is added by ABKInAppMessageController method addInAppMessage:. + * + * You can choose to manually display any in-app messages that are waiting locally to be displayed by calling: + * [[Appboy sharedInstance].inAppMessageController displayNextInAppMessage]. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageDisplayChoice) { + ABKDisplayInAppMessageNow, + ABKDisplayInAppMessageLater, + ABKDiscardInAppMessage +}; + +typedef NS_ENUM(NSInteger, ABKTriggerEventType) { + ABKTriggerEventTypeSessionStart, + ABKTriggerEventTypeCustomEvent, + ABKTriggerEventTypePurchase, + ABKTriggerEventTypeOther +}; + +/*! + * The in-app message delegate allows you to control the display of the Braze in-app message. For more detailed + * information on in-app message behavior, including when and how the delegate is used, see the documentation for the + * ABKInAppMessageDisplayChoice enum above for more detailed information. + * + * This delegate is for those who are using the Core subspec and not integrating the In-App Message subspec. If + * you are using the In-App Message subspec, please use ABKInAppMessageUIDelegate. + */ + +/*! + * Braze Public API: ABKInAppMessageControllerDelegate + */ +BRZ_DEPRECATED("renamed to 'BrazeInAppMessageUIDelegate' (BrazeUI module)") +@protocol ABKInAppMessageControllerDelegate + +@optional + +/*! + * @param inAppMessage The in-app message object being offered to the delegate method. + * @return ABKInAppMessageDisplayChoice The in-app message display choice. For details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice + * above. + * + * This delegate method defines whether the in-app message will be displayed now, displayed later, or discarded. + * + * If there are situations where you would not want the in-app message to appear (such as during a full screen + * game or on a loading screen), you can use this delegate to delay or discard pending in-app message messages. + */ +- (ABKInAppMessageDisplayChoice)beforeInAppMessageDisplayed:(ABKInAppMessage *)inAppMessage; + +/*! + * @param inAppMessage The control in-app message object being offered to the delegate method. + * @return ABKInAppMessageDisplayChoice The control in-app message impression logging choice. + * For details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice above. + * Logging a control message impression is an equivalent of displaying the message, except that no actual display occurs. + * + * This delegate method defines the timing of when the control in-app message impression event should be logged: now, later, or discarded. + * Logging a control message impression is an equivalent of displaying the message, except that no actual display occurs. + * + * If there are situations where you would not want the control in-app message impression to be logged, you can use this delegate to delay + * or discard it. + */ +- (ABKInAppMessageDisplayChoice)beforeControlMessageImpressionLogged:(ABKInAppMessage *)inAppMessage; + +/*! + * Executed when no trigger matches the Braze event. + * + * @param eventType The type of event that failed to match the user's triggers. + * @param name The event name of a custom event, the product identifier for a purchase + * event, or `nil` for a session start event. + */ +- (void)noMatchingTriggerForEvent:(ABKTriggerEventType)eventType + name:(nullable NSString *)name; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageDarkButtonTheme.h b/Sources/BrazeKitCompat/include/ABKInAppMessageDarkButtonTheme.h new file mode 100644 index 0000000000..f83ce7391f --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageDarkButtonTheme.h @@ -0,0 +1,32 @@ +#import +#import +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.ButtonTheme'") +@interface ABKInAppMessageDarkButtonTheme : NSObject + +/*! + * Dark theme of the button's background color. + */ +@property (strong) UIColor *buttonBackgroundColor; + +/*! + * Dark theme of the button's border color. + */ +@property (strong) UIColor *buttonBorderColor; + +/*! + * Dark theme of the button's text color. + */ +@property (strong) UIColor *buttonTextColor; + +/*! + * Creates a model containing the dark theme colors for buttons by parsing the dictionary `darkButtonFields` + */ +- (instancetype)initWithFields:(NSDictionary *)darkButtonFields; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageDarkTheme.h b/Sources/BrazeKitCompat/include/ABKInAppMessageDarkTheme.h new file mode 100644 index 0000000000..e00a51ad23 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageDarkTheme.h @@ -0,0 +1,47 @@ +#import +#import +#import "BrazePreprocessor.h" + +@class ABKInAppMessageButton; +@class ABKInAppMessageDarkButtonTheme; + +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Theme'") +@interface ABKInAppMessageDarkTheme : NSObject + +/* Properties of all ABKInAppMessages */ +@property (nonatomic, strong, nullable) UIColor *backgroundColor; + +@property (nonatomic, strong, nullable) UIColor *textColor; + +@property (nonatomic, strong, nullable) UIColor *iconColor; + +@property (nonatomic, strong, nullable) UIColor *iconBackgroundColor; + +/* ABKInAppMessageImmersive only */ +@property (nonatomic, strong, nullable) UIColor *headerTextColor; + +@property (nonatomic, strong, nullable) UIColor *closeButtonColor; + +@property (nonatomic, strong, nullable) UIColor *frameColor; + +/*! + * An array of all the button color properties, in the same order as the buttons object in ABKInAppImmersive + */ +@property (nonatomic, strong, nullable) NSArray *buttons; + +/*! + * Data model storing all the Dark Theme values passed down from the server for an in-app message. + * This only gets initalized if the campaign is set up to support Dark Theme and has the fields populated. + */ +- (instancetype)initWithFields:(NSDictionary *)darkThemeFields; + +/*! + * Returns the dark color variant given a valid key. If the key isn't found, returns nil. + */ +- (UIColor *)getColorForKey:(NSString *)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageFull.h b/Sources/BrazeKitCompat/include/ABKInAppMessageFull.h new file mode 100644 index 0000000000..8b502d06ac --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageFull.h @@ -0,0 +1,12 @@ +#import "ABKInAppMessageImmersive.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageFull + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Full' / 'Braze.InAppMessage.FullImage'") +@interface ABKInAppMessageFull : ABKInAppMessageImmersive + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageHTML.h b/Sources/BrazeKitCompat/include/ABKInAppMessageHTML.h new file mode 100644 index 0000000000..f6eefe4ce4 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageHTML.h @@ -0,0 +1,28 @@ +#import +#import "ABKInAppMessageHTMLBase.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageHTML + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Html'") +@interface ABKInAppMessageHTML : ABKInAppMessageHTMLBase + +/*! + * This property indicates whether the content was built by our platform. + */ +@property (nonatomic) BOOL trusted; + +/*! + * This property is an array of asset URLs that are used when generating the HTML. + */ +@property (strong, nonatomic, nullable) NSArray *assetUrls; + +/*! + * This property is a dictionary of other structured data that can be included with the in-app message. + */ +@property (strong, nonatomic, nullable) NSDictionary *messageFields; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLBase.h b/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLBase.h new file mode 100644 index 0000000000..b0d27b0091 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLBase.h @@ -0,0 +1,28 @@ +#import +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageHTMLBase + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Html'") +@interface ABKInAppMessageHTMLBase : ABKInAppMessage + +/*! + * This is the local URL of the assets directory for the HTML in-app message. Please note that the + * value of this property can be overridden by Braze at the time of displaying, so please don't set + * it as the value will be discarded. + */ +@property (strong, nonatomic) NSURL *assetsLocalDirectoryPath; + +/*! + * Log a click on the in-app message with a buttonId. HTMLFull in-app messages have the limitation of only + * handling a single button click, but we allow HTML in-app messages to handle multiple button clicks. + * + * @param buttonId the id of the click + */ +- (void)logInAppMessageHTMLClickWithButtonID:(NSString *)buttonId; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLFull.h b/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLFull.h new file mode 100644 index 0000000000..1cb510173b --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageHTMLFull.h @@ -0,0 +1,18 @@ +#import +#import "ABKInAppMessageHTMLBase.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageHTMLFull + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Html'") +@interface ABKInAppMessageHTMLFull : ABKInAppMessageHTMLBase + +/*! + * This property is the remote URL of the assets zip file. + */ +@property (strong, nonatomic, nullable) NSURL *assetsZipRemoteUrl; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageImmersive.h b/Sources/BrazeKitCompat/include/ABKInAppMessageImmersive.h new file mode 100644 index 0000000000..fed295b7cd --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageImmersive.h @@ -0,0 +1,95 @@ +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +@class ABKInAppMessageButton; + +/* + * Braze Public API: ABKInAppMessageImmersive + */ +NS_ASSUME_NONNULL_BEGIN + +/*! + * The ABKInAppMessageImmersiveImageStyle defines the image style of the in-app message + * + * ABKInAppMessageGraphic - The image will make up the entire in-app message, with buttons on the + * image(buttons are optional). No icons, headers or message will be displayed in this style. + * + * + * ABKInAppMessageTopImage - This is the default image style. The image will be on upper top of the + * in-app message if there is one, with all other in-app message elements. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageImmersiveImageStyle) { + ABKInAppMessageGraphic, + ABKInAppMessageTopImage +}; + +BRZ_DEPRECATED("ABKInAppMessageImmersive is not supported by Braze anymore. Use Modal, ModalImage, Full or FullImage in the Braze.InAppMessage namespace") +@interface ABKInAppMessageImmersive : ABKInAppMessage + +/*! + * header defines the header text of the in-app message. + * The header will only be displayed in one line on the default Braze in-app messages. If the header is more than one + * line, it will be truncated at the end. + */ +@property (copy, nullable) NSString *header; + +/*! + * headerTextColor defines the header text color, when there is a header string in the in-app message. The default text color + * is black. + */ +@property (nonatomic, strong, nullable) UIColor *headerTextColor; + +/*! + * closeButtonColor defines the close button color of the in-app message. + * When this property is nil, the close button's default color is black. + */ +@property (nonatomic, strong, nullable) UIColor *closeButtonColor; + +/*! + * buttons defines the buttons of the in-app message. + * Each button must be an instance of ABKInAppMessageButton. + * When there are more than two buttons in the array, only the first two buttons will be displayed in the in-app message. + * For more information and setting of ABKInAppMessageButton, please see the documentation in ABKInAppMessageButton.h for additional details. + */ +@property (readonly, copy, nullable) NSArray *buttons; + +/*! + * frameColor defines the frame color of an immersive in-app message. This color will fill the + * screen outside of the in-app message. When the property is nil, the color will be + * set to the default color, which is black with 90% opacity. + */ +@property (nonatomic, strong, nullable) UIColor *frameColor; + +/*! + * headerTextAlignment defines the preferred text alignment of the header label. + * The default value is NSTextAlignmentCenter. + */ +@property NSTextAlignment headerTextAlignment; + +/*! + * imageStyle defines the image style of a immersive in-app message. + * For more information about the possible image styles, please check the documentation of + * ABKInAppMessageImmersiveImageStyle above. + */ +@property ABKInAppMessageImmersiveImageStyle imageStyle; + +/*! + * @param buttonId The clicked button's button ID for the in-app message. This number can't be negative. + * If you're handling in-app messages completely on your own, you should still report + * clicks on the in-app message button back to Braze with this method so that your campaign reporting features + * still work in the dashboard. + * + * Note: Each in-app message can log at most one button click. + */ +- (void)logInAppMessageClickedWithButtonID:(NSInteger)buttonId; + +/*! + * @param buttonArray The button array for the in-app message. This array should NOT be nil nor empty. Every object in the array + * must be an instance of ABKInAppMessageButton. + * + * This method will set the in-app message buttons. + */ +- (void)setInAppMessageButtons:(NSArray *)buttonArray; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageModal.h b/Sources/BrazeKitCompat/include/ABKInAppMessageModal.h new file mode 100644 index 0000000000..86b7cbc4fa --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageModal.h @@ -0,0 +1,12 @@ +#import "ABKInAppMessageImmersive.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKInAppMessageModal + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Modal' / 'Braze.InAppMessage.ModalImage'") +@interface ABKInAppMessageModal : ABKInAppMessageImmersive + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageSlideup.h b/Sources/BrazeKitCompat/include/ABKInAppMessageSlideup.h new file mode 100644 index 0000000000..4b256d9862 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageSlideup.h @@ -0,0 +1,45 @@ +#import "ABKInAppMessage.h" +#import "BrazePreprocessor.h" + +/*! + * There are two possible values which control where the in-app message will enter the view. + * + * ABKInAppMessageSlideupFromBottom - This is the default behavior. + * The in-app message will slide onto the screen from the bottom edge of the view and will hide by sliding back down off + * the bottom of the screen. + * + * ABKInAppMessageSlideupFromTop - The in-app message will slide onto the screen from the top edge of the view and will hide by sliding + * back up off the top of the screen. + */ +typedef NS_ENUM(NSInteger, ABKInAppMessageSlideupAnchor) { + ABKInAppMessageSlideupFromTop, + ABKInAppMessageSlideupFromBottom +}; + +/* + * Braze Public API: ABKInAppMessageSlideup + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.InAppMessage.Slideup'") +@interface ABKInAppMessageSlideup : ABKInAppMessage + +/*! + * If hideChevron equals YES, the in-app message will not render the chevron on the right side of the in-app message. + * The chevron is a useful visual cue for the user that more content may be reached by tapping the in-app message. + */ +@property BOOL hideChevron; + +/*! + * inAppMessageSlideupAnchor defines the position of the in-app message on screen. + * See the above documentation for ABKInAppMessageAnchor enum documentation above offers additional details. + */ +@property ABKInAppMessageSlideupAnchor inAppMessageSlideupAnchor; + +/*! + * chevronColor defines the chevron arrow color of the in-app message. + * When this property is nil, the chevron's default color is white. + */ +@property (strong, nullable) UIColor *chevronColor; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageUIControlling.h b/Sources/BrazeKitCompat/include/ABKInAppMessageUIControlling.h new file mode 100644 index 0000000000..f50703b3d3 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageUIControlling.h @@ -0,0 +1,66 @@ +#import +#import "ABKInAppMessage.h" +#import "ABKInAppMessageControllerDelegate.h" +#import "BrazePreprocessor.h" + +BRZ_DEPRECATED("renamed to 'BrazeInAppMessagePresenter'") +@protocol ABKInAppMessageUIControlling + +@optional + +/*! + * @discussion This method sets the optional ABKInAppMessageUIDelegate. + * + * To set this delegate, call [[Appboy sharedInstance].inAppMessageController.inAppMessageUIController + * setInAppMessageUIDelegate: ] after initializing Braze. + */ +- (void)setInAppMessageUIDelegate:(id)uiDelegate; + +/*! + * @discussion This method will hide the in-app message that is currently being displayed. + * The animated parameter controls whether or not the in-app message will be animated + * away. This method does nothing if no in-app + * message is currently being displayed. + * + * Note: This will not fire the onInAppMessageDismissed: delegate method. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)hideCurrentInAppMessage:(BOOL)animated; + +/*! + * @discussion This method will return the ABKInAppMessageDisplayChoice (see ABKInAppMessageControllerDelegate + * for more information) based on whether or not the keyboard is showing. + * If you have implemented the beforeInAppMessageDisplayed:withKeyboardIsUp: in + * ABKInAppMessageUIDelegate, the choice returned there will override the default choice. + * + * For customization, please use a subclass or category to override this method. + */ +- (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForInAppMessage:(ABKInAppMessage *)inAppMessage; + +/*! + * @discussion This method will return the ABKInAppMessageDisplayChoice (see ABKInAppMessageControllerDelegate + * for more information) based on whether or not the keyboard is showing. + * + * For customization, please use a subclass or category to override this method. + */ +- (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForControlInAppMessage:(ABKInAppMessage *)controlInAppMessage; + +/*! + * @discussion This method displays the in-app message. We call it when the in-app message has no + * image URL, or there is an image URL, and it has already been downloaded. If you call + * this method directly and the image hasn't been downloaded, there will be a spinner + * animating in the image view. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)showInAppMessage:(ABKInAppMessage *)inAppMessage; + +/*! + * @discussion This method returns whether or not an in-app message is currently being shown. + * + * For customization, please use a subclass or category to override this method. + */ +- (BOOL)inAppMessageCurrentlyVisible; + +@end diff --git a/Sources/BrazeKitCompat/include/ABKInAppMessageWebViewBridge.h b/Sources/BrazeKitCompat/include/ABKInAppMessageWebViewBridge.h new file mode 100644 index 0000000000..b109d451c8 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKInAppMessageWebViewBridge.h @@ -0,0 +1,68 @@ +@import Foundation; + +#if !TARGET_OS_TV + +#import +#import "ABKInAppMessageHTML.h" +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN + +@class Appboy; +@class ABKInAppMessageHTML; +@protocol ABKInAppMessageWebViewBridgeDelegate; + +#pragma mark - ABKInAppMessageWebViewBridge + +/*! + * The webview bridge + * @discussion The bridge is automatically setup on initialization and destroyed on dealloc. The bridge + * needs to be retained to stay enabled. Keep a strong instance of the bridge in a property to do so + */ +BRZ_DEPRECATED("renamed to 'Braze.WebViewBridge.ScriptMessageHandler'") +@interface ABKInAppMessageWebViewBridge : NSObject + +/*! + * The delegate instance + */ +@property (nonatomic, weak) id delegate; + +/*! + * Initialize an instance of ABKInAppMessageWebViewBridge + * @param webView The WKWebView in which the bridge needs to be setup + * @param inAppMessage The InAppMessage being displayed + * @param appboy The Appboy instance + */ +- (instancetype)initWithWebView:(WKWebView *)webView + inAppMessage:(ABKInAppMessageHTML *)inAppMessage + appboyInstance:(Appboy *)appboy; + +@end + +#pragma mark - ABKInAppMessageWebViewBridgeDelegate + +/*! + * Methods for managing bridge related actions + */ +BRZ_DEPRECATED("ABKInAppMessageWebViewBridgeDelegate is not supported by Braze anymore") +@protocol ABKInAppMessageWebViewBridgeDelegate + +/*! + * Tells the delegate that the bridge has received a click action to execute + * @param webViewBridge The bridge informing the delegate + * @param clickAction The clickAction performed + */ +- (void)webViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge + receivedClickAction:(ABKInAppMessageClickActionType)clickAction; + +/*! + * Tells the delegate that a close message action was received + * @param webViewBridge The bridge informing the delegate + */ +- (void)closeMessageWithWebViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge; + +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/Sources/BrazeKitCompat/include/ABKLocationManager.h b/Sources/BrazeKitCompat/include/ABKLocationManager.h new file mode 100644 index 0000000000..ae264b42a6 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKLocationManager.h @@ -0,0 +1,37 @@ +#import +#import +#import "BrazePreprocessor.h" + +@class ABKServerConfig; + +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("use 'Braze.Configuration' to access the location analytics configuration") +@interface ABKLocationManager : NSObject + +/*! + * Use ABKEnableAutomaticLocationCollectionKey to enable automatic location tracking. + * For more information, please refer to Appboy.h. + */ +@property (readonly) BOOL enableLocationTracking; + +/*! + * Use ABKEnableGeofencesKey to enable geofences. + * For more information, please refer to Appboy.h. + */ +@property (readonly) BOOL enableGeofences; + +/*! + * Use ABKDisableAutomaticGeofenceRequestsKey to disable automatic geofence requests. + * For more information, please refer to requestGeofencesWithLongitude:latitude: in Appboy.h + */ +@property (readonly) BOOL disableAutomaticGeofenceRequests; + +/*! + * Calling this method will log a location using the regular location provider if a location is reported in under + * 60 seconds. After 60 seconds expires the regular location provider will stop collecting location. + */ +- (void)logSingleLocation; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKLocationManagerProvider.h b/Sources/BrazeKitCompat/include/ABKLocationManagerProvider.h new file mode 100644 index 0000000000..7e5c730206 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKLocationManagerProvider.h @@ -0,0 +1,26 @@ +#import +#import "BrazePreprocessor.h" + +/*! + * Do not call these methods within your code. They are meant for Braze internal use only. + */ + +/*! + * ABKLocationManagerProvider.h and ABKLocationManagerProvider.m must be added to your project + * regardless of whether or not you enable location services. This occurs automatically if you integrate/update via the CocoaPod. + */ + +/* + * Braze Public API: ABKLocationManagerProvider + */ + +@class CLLocationManager; + +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("ABKLocationManagerProvider is not supported by Braze anymore.") +@interface ABKLocationManagerProvider : NSObject + ++ (BOOL)locationServicesEnabled; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKModalWebViewController.h b/Sources/BrazeKitCompat/include/ABKModalWebViewController.h new file mode 100644 index 0000000000..0d2d420673 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKModalWebViewController.h @@ -0,0 +1,30 @@ +#import +#import "BrazePreprocessor.h" + +#if !TARGET_OS_TV + +#import + +BRZ_DEPRECATED("renamed to 'Braze.WebViewController'") +@interface ABKModalWebViewController : UINavigationController + +/*! + * The url the modal web view controller should open. Please note that this is the initial url and + * won't be updated if the initial url re-directs to another url. + */ +@property NSURL *url; + +/*! + * The WKWebView which displays the web view. + */ +@property (nonatomic) IBOutlet WKWebView *webView; + +/*! + * The UIProgressView which shows the web view loading process. It will be on top of the web view and + * will disappear as soon as the page is loaded. + */ +@property (nonatomic) IBOutlet UIProgressView *progressBar; + +@end + +#endif diff --git a/Sources/BrazeKitCompat/include/ABKNoConnectionLocalization.h b/Sources/BrazeKitCompat/include/ABKNoConnectionLocalization.h new file mode 100644 index 0000000000..9c6347af82 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKNoConnectionLocalization.h @@ -0,0 +1,9 @@ +#import +#import "BrazePreprocessor.h" + +BRZ_DEPRECATED("ABKNoConnectionLocalization is not supported by Braze anymore.") +@interface ABKNoConnectionLocalization : NSObject + ++ (NSString *)getNoConnectionLocalizedString; + +@end diff --git a/Sources/BrazeKitCompat/include/ABKPushUtils.h b/Sources/BrazeKitCompat/include/ABKPushUtils.h new file mode 100644 index 0000000000..72bb346727 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKPushUtils.h @@ -0,0 +1,124 @@ +#import + +#if !TARGET_OS_TV + +#import +#import +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN + +/* + * Braze Public API: ABKPushUtils + */ +BRZ_DEPRECATED("ABKPushUtils is not needed anymore.") +@interface ABKPushUtils : NSObject + +/*! + * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. + * + * @return YES if the user notification was sent from Braze servers. + */ ++ (BOOL)isAppboyUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetch​Completion​Handler: + * or application:didReceiveRemoteNotification:. + * + * @return YES if the push notification was sent from Braze servers. + */ ++ (BOOL)isAppboyRemoteNotification:(NSDictionary *)userInfo; + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: + * or application:didReceiveRemoteNotification:. + * + * @return YES if the push notification was sent by Braze for an internal feature. + * + * @discussion Braze uses content-available silent notifications for internal features. You can use this method to ensure + * your app doesn't take any undesired or unnecessary actions upon receiving Braze's internal content-available notifications + * (e.g., pinging your server for content). + */ ++ (BOOL)isAppboyInternalRemoteNotification:(NSDictionary *)userInfo BRZ_DEPRECATED("renamed to 'Braze.Notifications.isInternalNotification(_:)'"); + +/*! + * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. + * + * @return YES if the user notification was sent by Braze for uninstall tracking. + * + * @discussion Uninstall tracking notifications are content-available silent notifications. You can use this method to ensure + * your app doesn't take any undesired or unnecessary actions upon receiving Braze's uninstall tracking notifications + * (e.g., pinging your server for content). + */ ++ (BOOL)isUninstallTrackingUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: + * or application:didReceiveRemoteNotification:. + * + * @return YES if the push notification was sent by Braze for uninstall tracking. + * + * @discussion Uninstall tracking notifications are content-available silent notifications. You can use this method to ensure + * your app doesn't take any undesired or unnecessary actions upon receiving Braze's uninstall tracking notifications + * (e.g., pinging your server for content). + */ ++ (BOOL)isUninstallTrackingRemoteNotification:(NSDictionary *)userInfo; + +/*! + * @param response The UNNotificationResponse passed to userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:. + * + * @return YES if the user notification was sent by Braze for syncing geofences. + * + * @discussion Geofence sync notifications are content-available silent notifications. You can use this method to ensure + * your app doesn't take any undesired or unnecessary actions upon receiving Braze's geofence sync notifications + * (e.g., pinging your server for content). + */ ++ (BOOL)isGeofencesSyncUserNotification:(UNNotificationResponse *)response API_AVAILABLE(ios(10.0), macCatalyst(14.0)); + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: + * or application:didReceiveRemoteNotification:. + * + * @return YES if the push notification was sent by Braze for syncing geofences. + * + * @discussion Geofence sync notifications are content-available silent notifications. You can use this method to ensure + * your app doesn't take any undesired or unnecessary actions upon receiving Braze's geofence sync notifications + * (e.g., pinging your server for content). + */ ++ (BOOL)isGeofencesSyncRemoteNotification:(NSDictionary *)userInfo; + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetch​Completion​Handler: + * + * @return YES if the push notification was sent by Braze and is silent. + */ ++ (BOOL)isAppboySilentRemoteNotification:(NSDictionary *)userInfo; + +/*! + * @param userInfo The userInfo dictionary passed to application:didReceiveRemoteNotification:fetchCompletionHandler: + * or application:didReceiveRemoteNotification:. + * + * @return YES if the push notification was sent by Braze for push stories. + */ ++ (BOOL)isPushStoryRemoteNotification:(NSDictionary *)userInfo; + ++ (BOOL)notificationContainsContentCard:(NSDictionary *)userInfo; + +/*! + * @param userInfo The userInfo dictionary payload. + * + * @return YES if the notification contains an a flag that inticates the device should fetch test triggers from the server. + * + */ ++ (BOOL)shouldFetchTestTriggersFlagContainedInPayload:(NSDictionary *)userInfo __deprecated; + +/*! + * @return A set of the default UNNotificationCategories used by Braze. + */ ++ (NSSet *)getAppboyUNNotificationCategorySet API_AVAILABLE(ios(10.0), macCatalyst(14.0)) BRZ_DEPRECATED("renamed to 'Braze.Notifications.categories'."); + ++ (NSSet *)getAppboyUIUserNotificationCategorySet __deprecated_msg("Please use `getAppboyUNNotificationCategorySet` instead."); + +@end +NS_ASSUME_NONNULL_END +#endif diff --git a/Sources/BrazeKitCompat/include/ABKSDWebImageProxy.h b/Sources/BrazeKitCompat/include/ABKSDWebImageProxy.h new file mode 100644 index 0000000000..1afbca7bf4 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKSDWebImageProxy.h @@ -0,0 +1,34 @@ +#import +#import "BrazePreprocessor.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const CORE_VERSION_WARNING = @"Attempting to download image but Braze image utilities not found. Make sure you chose the UI Subspec if you want to use Braze's UI."; + +/* + * This proxy class gives the Braze iOS SDK access to the SDWebImage framework. + * + * NOTE: + * This class requires SDWebImage version 4.0*. + */ +BRZ_DEPRECATED("ABKSDWebImageProxy is not needed anymore") +@interface ABKSDWebImageProxy : NSObject + ++ (void)setImageForView:(UIImageView *)imageView + showActivityIndicator:(BOOL)showActivityIndicator + withURL:(nullable NSURL *)imageURL + imagePlaceHolder:(nullable UIImage *)placeHolder + completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion; ++ (void)loadImageWithURL:(nullable NSURL *)url + options:(NSInteger)options + completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion; ++ (void)diskImageExistsForURL:(nullable NSURL *)url + completed:(nullable void (^)(BOOL isInCache))completion; ++ (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url; ++ (void)removeSDWebImageForKey:(nullable NSString *)key; ++ (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; ++ (void)clearSDWebImageCache; ++ (BOOL)isSupportedSDWebImageVersion; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKSdkAuthenticationDelegate.h b/Sources/BrazeKitCompat/include/ABKSdkAuthenticationDelegate.h new file mode 100644 index 0000000000..778ae871b1 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKSdkAuthenticationDelegate.h @@ -0,0 +1,27 @@ +#import +#import "ABKSdkAuthenticationError.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKSdkAuthenticationDelegate + */ +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("renamed to 'BrazeDelegate'") +@protocol ABKSdkAuthenticationDelegate + +/*! + * This method is fired when an SDK Authentication error is returned by the server, for example, if + * the signature is expired or invalid. + * + * You are responsible for providing the Braze SDK a valid signature when this delegate method is + * called. + * SDK requests will retry periodically using an exponential backoff approach. After 50 consecutive + * failed attempts, retries will be paused until the next session start. + * + * @param authError The SDK Authentication error returned by the server + */ +- (void)handleSdkAuthenticationError:(ABKSdkAuthenticationError *)authError; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKSdkAuthenticationError.h b/Sources/BrazeKitCompat/include/ABKSdkAuthenticationError.h new file mode 100644 index 0000000000..3abe6bb23a --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKSdkAuthenticationError.h @@ -0,0 +1,37 @@ +#import +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKSdkAuthenticationError + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("renamed to 'Braze.SDKAuthenticationError'") +@interface ABKSdkAuthenticationError : NSObject + +/*! + * The error code for the SDK Authentication failure. + */ +@property (readonly) NSInteger code; + +/*! + * The reason for the SDK Authentication failure. + */ +@property (nullable, readonly) NSString *reason; + +/*! + * The user ID associated with the request that failed due to SDK Authentication failure. + */ +@property (nullable, readonly) NSString *userId; + +/*! + * The signature that was sent with the request that failed due to SDK Authentication failure. + */ +@property (readonly) NSString *signature; + +- (instancetype)initWithCode:(NSInteger)code + reason:(NSString *)reason + userId:(NSString *)userId + signature:(NSString *)signature NS_UNAVAILABLE; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKSdkMetadata.h b/Sources/BrazeKitCompat/include/ABKSdkMetadata.h new file mode 100644 index 0000000000..42d1d3aa08 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKSdkMetadata.h @@ -0,0 +1,42 @@ +@import Foundation; +#import "BrazePreprocessor.h" + +/*! + * Enum representing the accepted SDK Metatadata. + * See addSdkMetadata for more details. + */ +typedef NSString *ABKSdkMetadata NS_TYPED_EXTENSIBLE_ENUM BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata'"); +extern ABKSdkMetadata const ABKSdkMetadataAdjust BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.adjust'"); +extern ABKSdkMetadata const ABKSdkMetadataAirBridge BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.airbridge'"); +extern ABKSdkMetadata const ABKSdkMetadataAppsFlyer BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.appsflyer'"); +extern ABKSdkMetadata const ABKSdkMetadataBluedot BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.bluedot'"); +extern ABKSdkMetadata const ABKSdkMetadataBranch BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.branch'"); +extern ABKSdkMetadata const ABKSdkMetadataCordova BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.cordova'"); +extern ABKSdkMetadata const ABKSdkMetadataCarthage BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.carthage'"); +extern ABKSdkMetadata const ABKSdkMetadataCocoaPods BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.cocoapods'"); +extern ABKSdkMetadata const ABKSdkMetadataCordovaPM BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.cordovapm'"); +extern ABKSdkMetadata const ABKSdkMetadataExpo BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.expo'"); +extern ABKSdkMetadata const ABKSdkMetadataFoursquare BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.foursquare'"); +extern ABKSdkMetadata const ABKSdkMetadataFlutter BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.flutter'"); +extern ABKSdkMetadata const ABKSdkMetadataGoogleTagManager BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.googletagmanager'"); +extern ABKSdkMetadata const ABKSdkMetadataGimbal BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.gimbal'"); +extern ABKSdkMetadata const ABKSdkMetadataGradle BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.gradle'"); +extern ABKSdkMetadata const ABKSdkMetadataIonic BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.ionic'"); +extern ABKSdkMetadata const ABKSdkMetadataKochava BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.kochava'"); +extern ABKSdkMetadata const ABKSdkMetadataManual BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.manual'"); +extern ABKSdkMetadata const ABKSdkMetadataMParticle BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.mparticle'"); +extern ABKSdkMetadata const ABKSdkMetadataNativeScript BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.nativescript'"); +extern ABKSdkMetadata const ABKSdkMetadataNPM BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.npm'"); +extern ABKSdkMetadata const ABKSdkMetadataNuGet BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.nuget'"); +extern ABKSdkMetadata const ABKSdkMetadataPub BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.pub'"); +extern ABKSdkMetadata const ABKSdkMetadataRadar BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.radar'"); +extern ABKSdkMetadata const ABKSdkMetadataReactNative BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.reactnative'"); +extern ABKSdkMetadata const ABKSdkMetadataSegment BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.segment'"); +extern ABKSdkMetadata const ABKSdkMetadataSingular BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.singular'"); +extern ABKSdkMetadata const ABKSdkMetadataSwiftPM BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.swiftpm'"); +extern ABKSdkMetadata const ABKSdkMetadataTealium BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.tealium'"); +extern ABKSdkMetadata const ABKSdkMetadataUnreal BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.unreal'"); +extern ABKSdkMetadata const ABKSdkMetadataUnityPM BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.unitypm'"); +extern ABKSdkMetadata const ABKSdkMetadataUnity BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.unity'"); +extern ABKSdkMetadata const ABKSdkMetadataVizbee BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.vizbee'"); +extern ABKSdkMetadata const ABKSdkMetadataXamarin BRZ_DEPRECATED("renamed to 'Braze.Configuration.Api.SDKMetadata.xamarin'"); diff --git a/Sources/BrazeKitCompat/include/ABKTextAnnouncementCard.h b/Sources/BrazeKitCompat/include/ABKTextAnnouncementCard.h new file mode 100644 index 0000000000..c3a6324856 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKTextAnnouncementCard.h @@ -0,0 +1,26 @@ +#import "ABKCard.h" + +/* + * Braze Public API: ABKTextAnnouncementCard + */ +NS_ASSUME_NONNULL_BEGIN +@interface ABKTextAnnouncementCard : ABKCard + +/* + * The title text for the card. + */ +@property (copy) NSString *title; + +/* + * The description text for the card. + */ +@property (copy) NSString *cardDescription; + +/* + * The link text for the property url, like @"blog.appboy.com". It can be displayed on the card's + * UI to indicate the action/direction of clicking on the card. + */ +@property (copy, nullable) NSString *domain; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKTwitterUser.h b/Sources/BrazeKitCompat/include/ABKTwitterUser.h new file mode 100644 index 0000000000..6ffe29ff5e --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKTwitterUser.h @@ -0,0 +1,60 @@ +#import +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKTwitterUser + */ +NS_ASSUME_NONNULL_BEGIN +BRZ_DEPRECATED("Twitter user data is not supported by Braze anymore.") +@interface ABKTwitterUser : NSObject + +/*! + * The value returned from Twitter's Users API with key "description". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property (copy, nullable) NSString* userDescription; + +/*! + * The value returned from Twitter's Users API with key "name". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property (copy, nullable) NSString* twitterName; + +/*! + * The value returned from Twitter's Users API with key "profile_image_url". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property (copy, nullable) NSString* profileImageUrl; + +/*! + * The value returned from Twitter's Users API with key "screen_name". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property (copy, nullable) NSString* screenName; + +/*! + * The value returned from Twitter's Users API with key "followers_count". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property NSInteger followersCount; + +/*! + * The value returned from Twitter's Users API with key "friends_count". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property NSInteger friendsCount; + +/*! + * The value returned from Twitter's Users API with key "statuses_count". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property NSInteger statusesCount; + +/*! + * The value returned from Twitter's Users API with key "id". Please refer to + * https://dev.twitter.com/overview/api/users for more information. + */ +@property NSInteger twitterID; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKURLDelegate.h b/Sources/BrazeKitCompat/include/ABKURLDelegate.h new file mode 100644 index 0000000000..aa756e9788 --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKURLDelegate.h @@ -0,0 +1,29 @@ +#import +#import "Appboy.h" +#import "BrazePreprocessor.h" + +/* + * Braze Public API: ABKURLDelegate + */ +NS_ASSUME_NONNULL_BEGIN + +BRZ_DEPRECATED("renamed to 'BrazeDelegate'") +@protocol ABKURLDelegate + +/*! + * @param url The deep link or web URL being offered to the delegate method. + * @param channel An enum representing the URL's associated messaging channel. + * @param extras The extras dictionary associated with the campaign or messaging object that the URL originated from. + Extras may be specified as key-value pairs on the Braze dashboard. + * @return Boolean value which controls whether or not Braze will handle opening the URL. Returning YES will + * prevent Braze from opening the URL. Returning NO will cause Braze to handle opening the URL. + * + * This delegate method is fired whenever the user attempts to open a URL sent by Braze. You can use this delegate + * to customize Braze's URL handling. + */ +- (BOOL)handleAppboyURL:(NSURL * _Nullable)url + fromChannel:(ABKChannel)channel + withExtras:(NSDictionary * _Nullable)extras; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/ABKUser.h b/Sources/BrazeKitCompat/include/ABKUser.h new file mode 100644 index 0000000000..d3fcdd304e --- /dev/null +++ b/Sources/BrazeKitCompat/include/ABKUser.h @@ -0,0 +1,324 @@ +// +// ABKUser.h +// AppboySDK + +#import +#import "BrazePreprocessor.h" + +@class ABKFacebookUser; +@class ABKTwitterUser; +@class ABKAttributionData; + +NS_ASSUME_NONNULL_BEGIN +/* ------------------------------------------------------------------------------------------------------ + * Enums + */ + +/*! + * Genders recognized by the SDK. + */ +typedef NS_ENUM(NSInteger, ABKUserGenderType) { + ABKUserGenderMale, + ABKUserGenderFemale, + ABKUserGenderOther, + ABKUserGenderUnknown, + ABKUserGenderNotApplicable, + ABKUserGenderPreferNotToSay +}; + +/*! + * Convenience enum to represent notification status, for email and push notifications. + * + * OPTED_IN: subscribed, and explicitly opted in. + * SUBSCRIBED: subscribed, but not explicitly opted in. + * UNSUBSCRIBED: unsubscribed and/or explicitly opted out. + */ +typedef NS_ENUM(NSInteger, ABKNotificationSubscriptionType) { + ABKOptedIn, + ABKSubscribed, + ABKUnsubscribed +}; + +/*! + * When setting the custom attributes with custom keys: + * 1. The maximum key length is 255 characters; longer keys are truncated. + * 2. The maximum length for a string value in a custom attribute is 255 characters; longer values are truncated. + */ + +/* + * Braze Public API: ABKUser + */ +BRZ_DEPRECATED("renamed to 'Braze.User'") +@interface ABKUser : NSObject + +/*! + * The User's first name (String) + */ +@property (nonatomic, copy, nullable) NSString *firstName; + +/*! + * The User's last name (String) + */ +@property (nonatomic, copy, nullable) NSString *lastName; + +/*! + * The User's email (String) + */ +@property (nonatomic, copy, nullable) NSString *email; + +/*! + * The User's date of birth (NSDate) + */ +@property (nonatomic, copy, nullable) NSDate *dateOfBirth; + +/*! + * The User's country (String) + */ +@property (nonatomic, copy, nullable) NSString *country; + +/*! + * The User's home city (String) + */ +@property (nonatomic, copy, nullable) NSString *homeCity; + +/*! + * The User's language (String) + * + * Language Strings should be valid ISO 639-1 language codes. + * See https://www.loc.gov/standards/iso639-2/php/code_list.php. + * + * If not set here, user language will be inferred from the device language. + */ +@property (nonatomic, copy, nullable) NSString *language; + +/*! + * The User's phone number (String) + */ +@property (nonatomic, copy, nullable) NSString *phone; + +@property (nonatomic, copy, nullable, readonly) NSString *userID; + +/*! + * The User's avatar image URL. This URL will be processed by the server and used in their user profile on the + * dashboard. (String) + */ +@property (nonatomic, copy, nullable) NSString *avatarImageURL; + +/*! + * The User's Facebook account information. For more detail, please refer to ABKFacebookUser.h. + */ +@property (strong, nullable) ABKFacebookUser *facebookUser; + +/*! + * The User's Twitter account information. For more detail, please refer to ABKTwitterUser.h. + */ +@property (strong, nullable) ABKTwitterUser *twitterUser; + +/*! + * Sets the attribution information for the user. For in apps that have an install tracking integration. + * For more information, please refer to ABKAttributionData.h. + */ +@property (strong, nullable) ABKAttributionData *attributionData; + +/*! + * Adds an an alias for the current user. Individual (alias, label) pairs can exist on one and only one user. + * If a different user already has this alias or external user id, the alias attempt will be rejected + * on the server. + * + * @param alias The alias of the current user. + * @param label The label of the alias; used to differentiate it from other aliases for the user. + * @return Whether or not the alias and label are valid. Does not guarantee they won't collide with + * an existing pair. + */ +- (BOOL)addAlias:(NSString *)alias withLabel:(NSString *)label; + +/*! + * @param gender ABKUserGender enum representing the user's gender. + * @return YES if the user gender is set properly + */ +- (BOOL)setGender:(ABKUserGenderType)gender; + +/*! + * Sets whether or not the user should be sent email campaigns. Setting it to unsubscribed opts the user out of + * an email campaign that you create through the Braze dashboard. + * + * @param emailNotificationSubscriptionType enum representing the user's email notifications subscription type. + * @return YES if the field is set successfully, else NO. + */ +- (BOOL)setEmailNotificationSubscriptionType:(ABKNotificationSubscriptionType)emailNotificationSubscriptionType; + +/*! + * Sets the push notification subscription status of the user. Used to collect information about the user. + * + * @param pushNotificationSubscriptionType enum representing the user's push notifications subscription type. + * @return YES if the field is set successfully, else NO. + */ +- (BOOL)setPushNotificationSubscriptionType:(ABKNotificationSubscriptionType)pushNotificationSubscriptionType; + +/*! + * Adds the user to a Subscription Group. + * + * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. + * @return YES if the user was successfully added, else NO. If not, the groupId might have been nil or invalid. + */ +- (BOOL)addToSubscriptionGroupWithGroupId:(NSString *)groupId; + +/*! + * Removes the user from a Subscription Group. + * + * @param groupId The string UUID corresponding to the subscription group, provided by the Braze dashboard. + * @return YES if the user was successfully removed, else NO. If not, the groupId might have been nil or invalid. + */ +- (BOOL)removeFromSubscriptionGroupWithGroupId:(NSString *)groupId; + +/*! + * @param key The String name of the custom user attribute + * @param value A boolean value to set as a custom attribute + * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, + * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for + * one of the reserved keys. Please check the log for more details about the specific failure you encountered. + */ +- (BOOL)setCustomAttributeWithKey:(NSString *)key andBOOLValue:(BOOL)value; + +/*! + * @param key The String name of the custom user attribute + * @param value An integer value to set as a custom attribute + * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, + * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for + * one of the reserved keys. Please check the log for more details about the specific failure you encountered. + */ +- (BOOL)setCustomAttributeWithKey:(NSString *)key andIntegerValue:(NSInteger)value; + +/*! + * @param key The String name of the custom user attribute + * @param value A double value to set as a custom attribute + * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, + * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for + * one of the reserved keys. Please check the log for more details about the specific failure you encountered. + */ +- (BOOL)setCustomAttributeWithKey:(NSString *)key andDoubleValue:(double)value; + +/*! + * @param key The String name of the custom user attribute + * @param value An NSString value to set as a custom attribute + * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, + * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for + * one of the reserved keys. Please check the log for more details about the specific failure you encountered. + */ +- (BOOL)setCustomAttributeWithKey:(NSString *)key andStringValue:(NSString *)value; + +/*! + * @param key The String name of the custom user attribute + * @param value An NSDate value to set as a custom attribute + * @return whether or not the custom user attribute was set successfully; If not, your key might have been nil or empty, + * your value might have been invalid (either nil, or not of the correct type), or you tried to set a value for + * one of the reserved keys. Please check the log for more details about the specific failure you encountered. + */ +- (BOOL)setCustomAttributeWithKey:(NSString *)key andDateValue:(NSDate *)value; + +/*! + * @param key The String name of the custom user attribute to unset + * @return whether or not the custom user attribute was unset successfully + */ +- (BOOL)unsetCustomAttributeWithKey:(NSString *)key; + +/** + * Increments the value of an custom attribute by one. Only integer and long custom attributes can be incremented. + * Attempting to increment a custom attribute that is not an integer or a long will be ignored. If you increment a + * custom attribute that has not previously been set, a custom attribute will be created and assigned a value of one. + * + * @param key The identifier of the custom attribute + * @return YES if the increment for the custom attribute of given key is saved + */ +- (BOOL)incrementCustomUserAttribute:(NSString *)key; + +/** + * Increments the value of an custom attribute by a given amount. Only integer and long custom attributes can be + * incremented. Attempting to increment a custom attribute that is not an integer or a long will be ignored. If + * you increment a custom attribute that has not previously been set, a custom attribute will be created and assigned + * the value of incrementValue. To decrement the value of a custom attribute, use a negative incrementValue. + * + * @param key The identifier of the custom attribute + * @param incrementValue The amount by which to increment the custom attribute + * @return YES if the increment for the custom attribute of given key is saved + */ +- (BOOL)incrementCustomUserAttribute:(NSString *)key by:(NSInteger)incrementValue; + +/** + * Adds the string value to a custom attribute string array specified by the key. If you add a key that has not + * previously been set, a custom attribute string array will be created containing the value. + * + * @param key The custom attribute key + * @param value A string to be added to the custom attribute string array + * @return YES if the operation was successful + */ +- (BOOL)addToCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; + +/** + * Removes the string value from a custom attribute string array specified by the key. If you remove a key that has not + * previously been set, nothing will be changed. + * + * @param key The custom attribute key + * @param value A string to be removed from the custom attribute string array + * @return YES if the operation was successful + */ +- (BOOL)removeFromCustomAttributeArrayWithKey:(NSString *)key value:(NSString *)value; + +/** + * Sets a string array from a custom attribute specified by the key. + * + * @param key The custom attribute key + * @param valueArray A string array to set as a custom attribute. If this value is nil, then Braze will unset the custom + * attribute and remove the corresponding array if there is one. + * @return YES if the operation was successful + */ +- (BOOL)setCustomAttributeArrayWithKey:(NSString *)key array:(nullable NSArray *)valueArray; + +/*! +* Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES +* when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this +* method will be contending with automatic location update events. +* +* @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] +* @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] +* @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative +*/ +- (BOOL)setLastKnownLocationWithLatitude:(double)latitude longitude:(double)longitude horizontalAccuracy:(double)horizontalAccuracy; + +/*! +* Sets the last known location for the user. Intended for use with ABKDisableLocationAutomaticTrackingOptionKey set to YES +* when starting Braze, so that the only locations being set are by the integrating app. Otherwise, calls to this +* method will be contending with automatic location update events. +* +* @param latitude The latitude of the User's location in degrees, the number should be in the range of [-90, 90] +* @param longitude The longitude of the User's location in degrees, the number should be in the range of [-180, 180] +* @param horizontalAccuracy The accuracy of the User's horizontal location in meters, the number should not be negative +* @param altitude The altitude of the User's location in meters +* @param verticalAccuracy The accuracy of the User's vertical location in meters, the number should not be negative +*/ +- (BOOL)setLastKnownLocationWithLatitude:(double)latitude + longitude:(double)longitude + horizontalAccuracy:(double)horizontalAccuracy + altitude:(double)altitude + verticalAccuracy:(double)verticalAccuracy; + +/*! + * Adds the location custom attribute for the user. + * + * @param key The custom attribute key + * @param latitude The latitude of the location in degrees, the number should be in the range of [-90, 90] + * @param longitude The longitude of the location in degrees, the number should be in the range of [-180, 180] + */ +- (BOOL)addLocationCustomAttributeWithKey:(NSString *)key + latitude:(double)latitude + longitude:(double)longitude; + +/*! + * Removes the location custom attribute for the user. + * + * @param key The custom attribute key + */ +- (BOOL)removeLocationCustomAttributeWithKey:(NSString *)key; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/Appboy.h b/Sources/BrazeKitCompat/include/Appboy.h new file mode 100644 index 0000000000..3382d19eb2 --- /dev/null +++ b/Sources/BrazeKitCompat/include/Appboy.h @@ -0,0 +1,708 @@ +// +// Appboy.h +// AppboySDK + +/*! + \mainpage + This site contains technical documentation for the %Braze iOS SDK. Click on the "Classes" link above to + view the %Braze public interface classes and start integrating the SDK into your app! +*/ + +#import +#import +#import + +#import "ABKSdkMetadata.h" +#import "BrazePreprocessor.h" + +#ifndef APPBOY_SDK_VERSION +#define APPBOY_SDK_VERSION _ABKBRZCompat.sdkVersion +#endif + +#if !TARGET_OS_TV +@class ABKInAppMessageController; +@class ABKInAppMessage; +@class ABKInAppMessageViewController; +#endif + +@class ABKUser; +@class ABKFeedController; +@class ABKContentCardsController; +@class ABKLocationManager; +@protocol ABKInAppMessageControllerDelegate; +@protocol ABKIDFADelegate; +@protocol ABKURLDelegate; +@protocol ABKImageDelegate; +@protocol ABKSdkAuthenticationDelegate; + +NS_ASSUME_NONNULL_BEGIN +/* ------------------------------------------------------------------------------------------------------ + * Keys for Braze startup options + */ + +/*! + * If you want to set the request policy at app startup time (useful for avoiding any automatic data requests made by + * Braze at startup if you're looking to have full manual control). You can include one of the + * ABKRequestProcessingPolicy enum values as the value for the ABKRequestProcessingPolicyOptionKey in the appboyOptions + * dictionary. + */ +extern NSString *const ABKRequestProcessingPolicyOptionKey; + +/*! + * Sets the data flush interval (in seconds). This only has an effect when the request processing mode is set to + * ABKAutomaticRequestProcessing (which is the default). Values are converted into NSTimeIntervals and must be greater + * than 1.0. + */ +extern NSString *const ABKFlushIntervalOptionKey; + +/*! + * This key can be set to YES or NO and will configure whether Braze will automatically collect location (if the user permits). + * If set to YES, Braze will collect location if authorized. + * If it is set to NO or omitted, location will not be recorded for the user unless you manually + * call setUserLastKnownLocation on ABKUser. + */ +extern NSString *const ABKEnableAutomaticLocationCollectionKey; + +/*! + * This key can be set to YES or NO and will configure whether geofences are enabled. + * If set to YES, geofences will be enabled. + * If set to NO, geofences will be disabled. + * If the field is omitted, we will use the value of ABKEnableAutomaticLocationCollectionKey. + */ +extern NSString *const ABKEnableGeofencesKey; + +/*! + * This key can be set to YES or NO and will configure whether geofence requests are made automatically. + * If set to YES, geofence requests will not be made automatically. + * If set to NO, geofence requests will be made automatically. This is the default value when you have geofences enabled. + */ +extern NSString *const ABKDisableAutomaticGeofenceRequestsKey; + +/*! + * This key can be set to an instance of a class that extends ABKIDFADelegate, which can be used to pass advertiser tracking information to to Braze. + */ +extern NSString *const ABKIDFADelegateKey; + +/*! + * This key can be set to a custom API endpoint. This gets sent in the format sdk.api.braze.eu. + */ +extern NSString *const ABKEndpointKey; + +/*! + * This key can be set to an instance of a class that conforms to the ABKURLDelegate protocol, allowing it to handle URLs in a custom way. + */ +extern NSString *const ABKURLDelegateKey; + +/*! + * This can can be set to an instance of a class that conforms to the ABKImageDelegate protocol, allowing flexibility for using custom image libraries. + */ +extern NSString *const ABKImageDelegateKey; + +/*! + * This key can be set to an instance of a class that conforms to the ABKInAppMessageControllerDelegate protocol, allowing it to handle in-app messages in a custom way. + */ +extern NSString *const ABKInAppMessageControllerDelegateKey; + +/*! + * This key can be set YES or NO and will configure whether a modal in-app message will be dismissed when the user clicks + * outside of the in-app message. + * If set to YES, the in-app message will be dismissed. + * If set to NO, the in-app message will not be dismissed. This is the default value. + */ +extern NSString *const ABKEnableDismissModalOnOutsideTapKey; + +/*! + * This key can be set YES or NO and will configure whether the SDK Authentication feature is enabled. + */ +extern NSString *const ABKEnableSDKAuthenticationKey; + +/*! + * This key can can be set to an instance of a class that conforms to the ABKSdkAuthenticationDelegate protocol, allowing it to handle + * SDK Authentication errors. Setting this delegate will cause the delegate method `handleSdkAuthenticationError:` to get called in + * the event of an SDK Authentication error. + */ +extern NSString *const ABKSdkAuthenticationDelegateKey; + +/*! + * Set the time interval for session time out (in seconds). This will affect the case when user has a session shorter than + * the set time interval. In that case, the session won't be close even though the user closed the app, but will continue until + * it times out. The value should be an integer bigger than 0. + */ +extern NSString *const ABKSessionTimeoutKey; + +/*! + * Set the minimum time interval in seconds between triggers. After a trigger happens, we will ignore any triggers until + * the minimum time interval elapses. The default value is 30s. The minimum valid value is 0s. + */ +extern NSString *const ABKMinimumTriggerTimeIntervalKey; + +/*! + * Key to report the SDK flavor currently being used. For internal use only. + */ +extern NSString *const ABKSDKFlavorKey; + +/*! + * Key to specify an allowlist for device fields that are collected by the Braze SDK. + * + * To specify allowlisted device fields, assign the bitwise `OR` of desired fields to this key. Fields are defined + * in `ABKDeviceOptions`. To turn off all fields, set the value of this key to `ABKDeviceOptionNone`. By default, + * all fields are collected. + */ +extern NSString *const ABKDeviceAllowlistKey; + +/*! + * This key is deprecated in favor of ABKDeviceAllowlistKey. See ABKDeviceAllowlistKey for more details. + */ +extern NSString *const ABKDeviceWhitelistKey __deprecated_msg("ABKDeviceWhitelistKey is deprecated. Please use ABKDeviceAllowlistKey instead."); + +extern NSString *const ABKEphemeralEventsKey; + +/*! + * This key can be set to a string value representing the app group name for the Push Story Notification + * Content extension. This is required for the SDK to fetch data from and handle user interactions + * with the Push Story app extension. + */ +extern NSString *const ABKPushStoryAppGroupKey; + +/*! + * This key can be set to an integer value to specify the level of the log statements output by the Braze SDK. + * + * The default log level is 8 and will minimally log info. To enable verbose logging for debugging, use log level 0. + * + * This selection will override any LogLevel value set in the Info.plist. + */ +extern NSString *const ABKLogLevelKey; + +/* ------------------------------------------------------------------------------------------------------ + * Enums + */ + +/*! + * Possible values for the SDK's request processing policies: + * ABKAutomaticRequestProcessing (default) - All server communication is handled automatically. This includes flushing + * analytics data to the server, updating the feed, and requesting new in-app messages. Braze's + * communication policy is to perform immediate server requests when user facing data is required (new in-app messages, + * feed refreshes, etc.), and to otherwise perform periodic flushes of new analytics data every few seconds. + * The interval between periodic flushes can be set explicitly using the ABKFlushInterval startup option. + * ABKAutomaticRequestProcessingExceptForDataFlush - Deprecated. Use ABKManualRequestProcessing. + * ABKManualRequestProcessing - The same as ABKAutomaticRequestProcessing, except that updates to + * custom attributes and triggering of custom events will not automatically flush to the server. Instead, you + * must call requestImmediateDataFlush when you want to synchronize newly updated user data with Braze. Note that + * the configuration does not turn off all networking, i.e. requests important to the proper functionality of the Braze + * SDK will still occur. + * + * Regardless of policy, Braze will intelligently combine requests on the request queue to minimize the total number of + * requests and their combined payload. + */ +typedef NS_ENUM(NSInteger, ABKRequestProcessingPolicy) { + ABKAutomaticRequestProcessing, + ABKManualRequestProcessing, + ABKAutomaticRequestProcessingExceptForDataFlush __deprecated_enum_msg("ABKAutomaticRequestProcessingExceptForDataFlush is deprecated. Use ManualRequestProcessing.") = ABKManualRequestProcessing +}; + +/*! + * Internal enum used to report the SDK flavor being used. + */ +typedef NS_ENUM(NSInteger , ABKSDKFlavor) { + UNITY = 1, + REACT, + CORDOVA, + XAMARIN, + FLUTTER, + SEGMENT, + MPARTICLE, + TEALIUM +}; + +typedef NS_OPTIONS(NSUInteger, ABKDeviceOptions) { + ABKDeviceOptionNone = 0, + ABKDeviceOptionResolution = (1 << 0), + ABKDeviceOptionCarrier = (1 << 1), + ABKDeviceOptionLocale = (1 << 2), + ABKDeviceOptionModel = (1 << 3), + ABKDeviceOptionOSVersion = (1 << 4), + // Note: The ABKDeviceOptionIDFV allowlist key currently has no effect. + // IDFV is read regardless of allowlist settings due to its + // role as the primary device identifier within the Braze system. + ABKDeviceOptionIDFV = (1 << 5), + ABKDeviceOptionIDFA = (1 << 6), + ABKDeviceOptionPushEnabled = (1 << 7), + ABKDeviceOptionTimezone = (1 << 8), + ABKDeviceOptionPushAuthStatus = (1 << 9), + ABKDeviceOptionAdTrackingEnabled = (1 << 10), + ABKDeviceOptionPushDisplayOptions = (1 << 11), + ABKDeviceOptionAll = ~ABKDeviceOptionNone +}; + +/*! + * Possible channels supported by the SDK. + */ +typedef NS_ENUM(NSInteger, ABKChannel) { + ABKPushNotificationChannel, + ABKInAppMessageChannel, + ABKNewsFeedChannel, + ABKContentCardChannel, + ABKUnknownChannel __deprecated_enum_msg("ABKUnknownChannel will be removed in a future update.") +}; + +/* + * Braze Public API: Appboy + */ +BRZ_DEPRECATED("renamed to 'Braze'") +@interface Appboy : NSObject + +/* ------------------------------------------------------------------------------------------------------ + * Initialization + */ + +/*! + * Get the Appboy singleton. Returns nil if accessed before startWithApiKey: called. + */ ++ (nullable Appboy *)sharedInstance; + +/*! + * Get the Appboy singleton. Throws an exception if accessed before startWithApiKey: is called. + */ ++ (nonnull Appboy *)unsafeInstance; + +/*! + * @param apiKey The app's API key + * @param application the current app + * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions + * + * @discussion Starts up Braze and tells it that your app is done launching. You should call this + * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, + * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from + * the Braze dashboard where you registered your app. + */ ++ (void)startWithApiKey:(NSString *)apiKey + inApplication:(UIApplication *)application + withLaunchOptions:(nullable NSDictionary *)launchOptions; + +/*! + * @param apiKey The app's API key + * @param application The current app + * @param launchOptions The options NSDictionary that you get from application:didFinishLaunchingWithOptions + * @param appboyOptions An optional NSDictionary with startup configuration values for Braze. See below + * for more information. + * + * @discussion Starts up Braze and tells it that your app is done launching. You should call this + * method in your App Delegate application:didFinishLaunchingWithOptions method before calling makeKeyAndVisible, + * accessing [Appboy sharedInstance] or otherwise rendering Braze view controllers. Your apiKey comes from + * the Braze dashboard where you registered your app. + */ ++ (void)startWithApiKey:(NSString *)apiKey + inApplication:(UIApplication *)application + withLaunchOptions:(nullable NSDictionary *)launchOptions + withAppboyOptions:(nullable NSDictionary *)appboyOptions; + +/* ------------------------------------------------------------------------------------------------------ + * Properties + */ + +/*! + * The current app user. + * See ABKUser.h and changeUser:userId below. + */ +@property (readonly) ABKUser *user; + +@property (readonly) ABKFeedController *feedController; + +@property (readonly) ABKContentCardsController *contentCardsController; + +/*! +* The policy regarding processing of network requests by the SDK. See the enumeration values for more information on +* possible options. This value can be set at runtime, or can be injected in at startup via the appboyOptions dictionary. +* +* Any time the request processing policy is set to manual, any scheduled flush of the queue is canceled, but if the +* request queue was already processing, the current queue will finish processing. If you need to cancel in flight +* requests, you need to call
[[Appboy sharedInstance] shutdownServerCommunication]
. +* +* Setting the request policy does not automatically cause a flush to occur, it just allows for a flush to be scheduled +* the next time an eligible request is enqueued. To force an immediate flush after changing the request processing +* policy, invoke
[[Appboy sharedInstance] requestImmediateDataFlush]
. +*/ +@property ABKRequestProcessingPolicy requestProcessingPolicy; + +/*! + * A class extending ABKIDFADelegate can be set to provide the IDFA to Braze. + */ +@property (nonatomic, strong, nullable) id idfaDelegate; + +/*! + * A class conforming to ABKSdkAuthenticationDelegate can be set to handle SDK Authentication errors. + */ +@property (nonatomic, strong, nullable) id sdkAuthenticationDelegate; + +#if !TARGET_OS_TV +/*! + * The current in-app message manager. + * See ABKInAppMessageController.h. + */ +@property (readonly) ABKInAppMessageController *inAppMessageController; + +/*! + * The Braze location manager provides access to location related functionality in the Braze SDK. + * See ABKLocationManager.h. + */ +@property (nonatomic, readonly) ABKLocationManager *locationManager; + +/*! + * A class conforming to the ABKURLDelegate protocol can be set to handle URLs in a custom way. + */ +@property (nonatomic, weak, nullable) id appboyUrlDelegate; + +/*! + * A class conforming to ABKImageDelegate can be set to use a custom image library. + */ +@property (nonatomic, strong, nullable) id imageDelegate; + +/*! + * Property for internal reporting of SDK flavor. + */ +@property (nonatomic) ABKSDKFlavor sdkFlavor; + +#endif + +/* ------------------------------------------------------------------------------------------------------ + * Methods + */ + +/*! + * Enqueues a data flush request for the current user and immediately starts processing the network queue. Note that if + * the queue already contains another request for the current user, that the new data flush request + * will be merged into the already existing request and only one will execute for that user. + * + * If you're using ABKManualRequestProcessing, you only need to call this when you want to force + * an immediate flush of updated user data. + */ +- (void)requestImmediateDataFlush; + +- (void)flushDataAndProcessRequestQueue __deprecated_msg("Please use `requestImmediateDataFlush` instead."); + +/*! + * Stops all in flight server communication and enables manual request processing control to ensure that no automatic + * network activity occurs. You should usually only call shutdownServerCommunication if the OS is forcing you to stop + * background tasks upon exit of your application. To continue normal operation after calling this, you will need to + * explicitly set the request processing mode back to your desired state. + */ +- (void)shutdownServerCommunication __attribute__((unavailable("use the 'enabled' boolean property on the Braze instance to disable / enable the SDK."))); + +/*! +* @param userId The new user's ID (from the host application). +* +* @discussion +* This method changes the user's ID. These user IDs should be private and not easily obtained (e.g. not a plain +* email address or username). +* +* When you first start using Braze on a device, the user is considered "anonymous". You can use this method to +* optionally identify a user with a unique ID, which enables the following: +* +* - If the same user is identified on another device, their user profile, usage history and event history will +* be shared across devices. +* +* - If your app is used by multiple people, you can assign each of them a unique identifier to track them +* separately. Only the most recent user on a particular device will receive push notifications and in-app +* messages. +* +* - If you identify a user which has never been identified on another device, the entire history of that user as +* an "anonymous" user on this device will be preserved and associated with the newly identified user. +* +* - However, if you identify a user which *has* been identified on another device, the previous anonymous +* history of the user on this device will not be added to the already existing profile for that user. +* +* - Note that switching from one an anonymous user to an identified user or from one identified user to another is +* a relatively costly operation. When you request the +* user switch, the current session for the previous user is automatically closed and a new session is started. +* Braze will also automatically make a data refresh request to get the news feed, in-app message and other information +* for the new user. +* +* Note: Once you identify a user, you cannot go back to the "anonymous" profile. The transition from anonymous +* to identified tracking only happens once because the initial anonymous user receives special treatment +* to allow for preservation of their history. We recommend against changing the user id just because your app +* has entered a "logged out" state because it separates this device from the user profile and thus you will be +* unable to target the previously logged out user with re-engagement campaigns. If you anticipate multiple +* users on the same device, but only want to target one of them when your app is in a logged out state, we recommend +* separately keeping track of the user ID you want to target while logged out and switching back to +* that user ID as part of your app's logout process. +*/ +- (void)changeUser:(NSString *)userId; + +/*! + * @param userId The new user's ID (from the host application) + * @param signature The SDK Authentication signature for the user being identified. + * + * @discussion See documantation for `changeUser:` above + */ +- (void)changeUser:(NSString *)userId sdkAuthSignature:(nullable NSString *)signature; + +/*! + * @param signature The SDK Authentication signature for the current user + * + * @discussion Sets the signature used for SDK authentication for the current user. + */ +- (void)setSdkAuthenticationSignature:(NSString *)signature; + +/*! + * @discussion Unsubscribe from SDK Authentication errors. After this method is called, + * the ABKSdkAuthenticationDelegate method `handleSdkAuthenticationError:` will not be called in the event of + * an SDK Authentication error. + */ +- (void)unsubscribeFromSdkAuthenticationErrors; + +/*! + * @param eventName The name of the event to log. + * + * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of + * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be + * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy + * Perry's Last Friday Night" so you can create more broad user segments for targeting. + * + *
+ * [[Appboy sharedInstance] logCustomEvent:@"clicked_button"];
+ * 
+ */ +- (void)logCustomEvent:(NSString *)eventName; + +/*! + * @param eventName The name of the event to log. + * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with + * <= 255 characters and no leading dollar signs. Property values can be NSNumber booleans, integers, floats < 62 bits, NSDate objects, + * NSString objects with <= 255 characters, or any JSON Encodable object including NSArray and NSDictionary of the previous data types (nested properties). Total length of encoded properties must be under 50 KB. + * + * @discussion Adds an app specific event to event tracking log that's lazily pushed up to the server. Think of + * events like counters. That is, each time you log an event, we'll update a counter for that user. Events should be + * fairly broad like "beat level 1" or "watched video" instead of something more specific like "watched Katy + * Perry's Last Friday Night" so you can create more broad user segments for targeting. + * + *
+ * [[Appboy sharedInstance] logCustomEvent:@"clicked_button" properties:@{@"key1":@"val"}];
+ * 
+ */ +- (void)logCustomEvent:(NSString *)eventName withProperties:(nullable NSDictionary *)properties; + +/*! + * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties: with a quantity of 1 and nil properties. + * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. + */ +- (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price; + +/*! + * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with a quantity of 1. + * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. + */ +- (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withProperties:(nullable NSDictionary *)properties; + +/*! + * This method is equivalent to calling logPurchase:inCurrency:atPrice:withQuantity:andProperties with nil properties. + * Please see logPurchase:inCurrency:atPrice:withQuantity:andProperties: for more information. + */ +- (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity; + +/*! + * @param productIdentifier A String indicating the product that was purchased. Usually the product identifier in the + * iTunes store. + * @param currencyCode Currencies should be represented as an ISO 4217 currency code. Prices should + * be sent in decimal format, with the same base units as are provided by the SKProduct class. Callers of this method + * who have access to the NSLocale object for the purchase in question (which can be obtained from SKProduct listings + * provided by StoreKit) can obtain the currency code by invoking: + *
[locale objectForKey:NSLocaleCurrencyCode]
+ * Supported currency symbols include: AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BHD, BIF, + * BMD, BND, BOB, BRL, BSD, BTC, BTN, BWP, BYR, BZD, CAD, CDF, CHF, CLF, CLP, CNY, COP, CRC, CUC, CUP, CVE, CZK, DJF, + * DKK, DOP, DZD, EEK, EGP, ERN, ETB, EUR, FJD, FKP, GBP, GEL, GGP, GHS, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HRK, HTG, HUF, + * IDR, ILS, IMP, INR, IQD, IRR, ISK, JEP, JMD, JOD, JPY, KES, KGS, KHR, KMF, KPW, KRW, KWD, KYD, KZT, LAK, LBP, LKR, LRD, + * LSL, LTL, LVL, LYD, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MRO, MTL, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, + * NZD, OMR, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, RUB, RWF, SAR, SBD, SCR, SDG, SEK, SGD, SHP, SLL, SOS, SRD, + * STD, SVC, SYP, SZL, THB, TJS, TMT, TND, TOP, TRY, TTD, TWD, TZS, UAH, UGX, USD, UYU, UZS, VEF, VND, VUV, WST, XAF, XAG, + * XAU, XCD, XDR, XOF, XPD, XPF, XPT, YER, ZAR, ZMK, ZMW and ZWL. Any other provided currency symbol will result in a logged + * warning and no other action taken by the SDK. + * @param price Prices should be reported as NSDecimalNumber objects. Base units are treated the same as with SKProduct + * from StoreKit and depend on the currency. As an example, USD should be reported as Dollars.Cents, whereas JPY should + * be reported as a whole number of Yen. All provided NSDecimalNumber values will have NSRoundPlain rounding applied + * such that a maximum of two digits exist after their decimal point. + * @param quantity An unsigned number to indicate the purchase quantity. This number must be greater than 0 but no larger than 100. + * @param properties An NSDictionary of properties to associate with this purchase. Property keys are non-empty NSString objects with + * <= 255 characters and no leading dollar signs. Property values can be NSNumber integers, floats, booleans < 62 bits in length, NSDate objects or + * NSString objects with <= 255 characters. + * + * @discussion Logs a purchase made in the application. + * + * Note: Braze supports purchases in multiple currencies. Purchases that you report in a currency other than USD will + * be shown in the dashboard in USD based on the exchange rate at the date they were reported. + */ +- (void)logPurchase:(NSString *)productIdentifier inCurrency:(NSString *)currencyCode atPrice:(NSDecimalNumber *)price withQuantity:(NSUInteger)quantity andProperties:(nullable NSDictionary *)properties; + +/*! + * If you're displaying cards on your own instead of using ABKFeedViewController, you should still report impressions of + * the news feed back to Braze with this method so that your campaign reporting features still work in the dashboard. + */ +- (void)logFeedDisplayed; + +/*! + * If you're displaying content cards on your own instead of using ABKContentCardsViewController, you should still report + * impressions of the content cards back to Braze with this method so that your campaign reporting features still work + * in the dashboard. + */ +- (void)logContentCardsDisplayed; + +/*! + * Enqueues a news feed request for the current user. Note that if the queue already contains another request for the + * current user, that the new feed request will be merged into the already existing request and only one will execute + * for that user. + * + * When the new cards for news feed return from Braze server, the SDK will post an ABKFeedUpdatedNotification with an + * ABKFeedUpdatedIsSuccessfulKey in the notification's userInfo dictionary to indicate if the news feed request is successful + * or not. For more detail about the ABKFeedUpdatedNotification and the ABKFeedUpdatedIsSuccessfulKey, please check ABKFeedController. + */ +- (void)requestFeedRefresh; + +/*! + * Enqueues a content cards request for the current user. + */ +- (void)requestContentCardsRefresh; + +/*! + * Manually request geofences with a specific location. + */ +- (void)requestGeofencesWithLongitude:(double)longitude latitude:(double)latitude; + +/*! + * Get the device ID - the IDFV - which will reset if all apps for a given vendor are removed from the device. + * + * @return The device ID. + */ +- (NSString *)getDeviceId; + + +#if !TARGET_OS_TV + +/*! + * @param deviceToken The device's push token. + * + * @discussion This method posts a token to Braze servers to associate the token with the current device. + */ +- (void)registerDeviceToken:(NSData *)deviceToken; + +/*! + * @param application The app's UIApplication object + * @param notification An NSDictionary passed in from the didReceiveRemoteNotification call + * + * @discussion This method forwards remote notifications to Braze. Call it from the application:didReceiveRemoteNotification + * method of your App Delegate. + */ +- (void)registerApplication:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification __attribute__((unavailable("BrazeKit / BrazeKitCompat doesn't provide support for this deprecated method. Push notifications support must be integrated using the 'UserNotifications' framework. See our push integration documentation for more details."))); + +/*! + * @param application The app's UIApplication object + * @param notification An NSDictionary passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call + * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call + * + * @discussion This method forwards remote notifications to Braze. If the completionHandler is passed in when + * the method is called, Braze will call the completionHandler. However, if the completionHandler is not passed in, + * it is the host app's responsibility to call the completionHandler. + * Call it from the application:didReceiveRemoteNotification:fetchCompletionHandler: method of your App Delegate. + */ +- (void)registerApplication:(UIApplication *)application +didReceiveRemoteNotification:(NSDictionary *)notification + fetchCompletionHandler:(nullable void (^)(UIBackgroundFetchResult))completionHandler; + +/*! + * @param identifier The action identifier passed in from the handleActionWithIdentifier:forRemoteNotification:. + * @param userInfo An NSDictionary passed in from the handleActionWithIdentifier:forRemoteNotification: call. + * @param completionHandler A block passed in from the didReceiveRemoteNotification:fetchCompletionHandler: call + * + * @discussion This method forwards remote notifications and the custom action chosen by user to Braze. Call it from + * the application:handleActionWithIdentifier:forRemoteNotification: method of your App Delegate. + */ +- (void)getActionWithIdentifier:(NSString *)identifier + forRemoteNotification:(NSDictionary *)userInfo + completionHandler:(nullable void (^)(void))completionHandler __attribute__((unavailable("BrazeKit / BrazeKitCompat doesn't provide support for this deprecated method. Push notifications support must be integrated using the 'UserNotifications' framework. See our push integration documentation for more details."))); +/*! + * @param center The app's current UNUserNotificationCenter object + * @param response The UNNotificationResponse object passed in from the didReceiveNotificationResponse:withCompletionHandler: call + * @param completionHandler A block passed in from the didReceiveNotificationResponse:withCompletionHandler: call. Braze will call + * it at the end of the method if one is passed in. If you prefer to handle the completionHandler youself, please pass nil to Braze. + * + * @discussion This method forwards the response of the notification to Braze after user interacted with the notification. + * Call it from the userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler: method of your App Delegate. + */ +- (void)userNotificationCenter:(UNUserNotificationCenter *)center +didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(nullable void (^)(void))completionHandler NS_AVAILABLE_IOS(10_0); + +/*! + * @param pushAuthGranted The boolean value passed in from completionHandler in UNUserNotificationCenter's + * requestAuthorizationWithOptions:completionHandler: method, which indicates if the push authorization + * was granted or not. + * + * @discussion This method forwards the push authorization result to Braze after the user interacts with + * the notification prompt. + * Call it from the UNUserNotificationCenter's requestAuthorizationWithOptions:completionHandler: method + * when you prompt users to enable push. + */ +- (void)pushAuthorizationFromUserNotificationCenter:(BOOL)pushAuthGranted; + +#endif + +/*! + * Adds SDK Metadata values to those automatically collected by the SDK. + * + * Metadata tell Braze how the SDK is integrated (e.g. wrapper, package manager, etc.) + * + * @param metadata The metadata values reflecting the current SDK integration. + */ +- (void)addSdkMetadata:(NSArray *)metadata; + +/* ------------------------------------------------------------------------------------------------------ + * Data processing configuration methods. + */ + +/*! + * @discussion This method immediately wipes all data from the Braze iOS SDK. After this method is + * called, the sharedInstance singleton will be nulled out and Braze functionality will be disabled + * until the next call to startWithApiKey: in a subsequent app run. All references to the previous + * singleton should be released. + * + * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK + * within the same app run after calling this method is not supported. + * + * The SDK will automatically re-enable itself when startWithApiKey: is called. There is + * no need to call requestEnableSDKOnNextAppRun: to re-enable the SDK. wipeDataAndDisableForAppRun: + * may be used at any time, including while the SDK is otherwise disabled. + * + * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this + * method will cause an uncaught exception to be thrown. We do not recommend using this method in + * concert with unsafeInstance:. + */ ++ (void)wipeDataAndDisableForAppRun; + +/*! + * @discussion This method immediately disables the Braze iOS SDK. After this method is called, the + * sharedInstance singleton will be nulled out and Braze functionality will be disabled until the + * SDK is re-enabled via a call to requestEnableSDKOnNextAppRun: and re-initialized in a subsequent + * app run via a call to startWithApiKey:. All references to the previous singleton should be released. + * + * Note that the next call to startWithApiKey: must take place in a subsequent app run. Initializing the SDK + * within the same app run after calling this method is not supported. + * + * Unlike with wipeDataAndDisableForAppRun:, calling requestEnableSDKOnNextAppRun: is required to + * re-enable the SDK after the method is called. + * + * Note that if you are using unsafeInstance:, further calls to unsafeInstance: after using this + * method will cause an exception to be thrown. We do not recommend using this method in concert + * with unsafeInstance:. + */ ++ (void)disableSDK; + +/*! + * @discussion This method requests the Braze iOS SDK to be re-enabled on the next app run. + * After this method is called, the following call to startWithApiKey: will successfully + * re-enable the SDK. Braze functionality will remain disabled until that point. + * + * Note that this method does not re-initialize the Appboy singleton on its own nor re-enable + * Braze functionality immediately. + */ ++ (void)requestEnableSDKOnNextAppRun; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeKitCompat/include/AppboyKit.h b/Sources/BrazeKitCompat/include/AppboyKit.h new file mode 100644 index 0000000000..d0b20ccd6f --- /dev/null +++ b/Sources/BrazeKitCompat/include/AppboyKit.h @@ -0,0 +1,67 @@ +#import "Appboy.h" +#import "ABKUser.h" +#import "ABKFacebookUser.h" +#import "ABKTwitterUser.h" +#import "ABKAttributionData.h" + +// Cards +#import "ABKCard.h" +#import "ABKBannerCard.h" +#import "ABKCaptionedImageCard.h" +#import "ABKClassicCard.h" +#import "ABKTextAnnouncementCard.h" + +// Content Card +#import "ABKContentCard.h" +#import "ABKBannerContentCard.h" +#import "ABKCaptionedImageContentCard.h" +#import "ABKClassicContentCard.h" + +// SDK Authentication +#import "ABKSdkAuthenticationError.h" +#import "ABKSdkAuthenticationDelegate.h" + +#if !TARGET_OS_TV +// In-app Message +#import "ABKInAppMessage.h" +#import "ABKInAppMessageSlideup.h" +#import "ABKInAppMessageImmersive.h" +#import "ABKInAppMessageModal.h" +#import "ABKInAppMessageFull.h" +#import "ABKInAppMessageHTML.h" +#import "ABKInAppMessageHTMLFull.h" +#import "ABKInAppMessageHTMLBase.h" +#import "ABKInAppMessageControl.h" +#import "ABKInAppMessageControllerDelegate.h" +#import "ABKInAppMessageController.h" +#import "ABKInAppMessageButton.h" +#import "ABKInAppMessageWebViewBridge.h" +#import "ABKInAppMessageUIControlling.h" +#import "ABKInAppMessageDarkTheme.h" +#import "ABKInAppMessageDarkButtonTheme.h" + +// News Feed +#import "ABKFeedController.h" + +// Content Cards Feed +#import "ABKContentCardsController.h" + +// IDFA +#import "ABKIDFADelegate.h" + +// SDWebImage +#import "ABKSDWebImageProxy.h" + +// ABKImageDelegate +#import "ABKImageDelegate.h" + +// Location +#import "ABKLocationManager.h" +#import "ABKLocationManagerProvider.h" + +#import "ABKURLDelegate.h" +#import "ABKPushUtils.h" +#import "ABKModalWebViewController.h" +#import "ABKNoConnectionLocalization.h" + +#endif diff --git a/Sources/BrazeKitCompat/include/BrazePreprocessor.h b/Sources/BrazeKitCompat/include/BrazePreprocessor.h new file mode 100644 index 0000000000..55eb558829 --- /dev/null +++ b/Sources/BrazeKitCompat/include/BrazePreprocessor.h @@ -0,0 +1,7 @@ +//#define BRAZE_DISABLE_DEPRECATIONS 1 + +#if BRAZE_DISABLE_DEPRECATIONS + #define BRZ_DEPRECATED(msg) +#else + #define BRZ_DEPRECATED(msg) __attribute__((deprecated((msg)))) +#endif diff --git a/Sources/BrazeKitCompat/include/_ABKBRZCompat.h b/Sources/BrazeKitCompat/include/_ABKBRZCompat.h new file mode 100644 index 0000000000..7abe54b6c2 --- /dev/null +++ b/Sources/BrazeKitCompat/include/_ABKBRZCompat.h @@ -0,0 +1,44 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +NS_ASSUME_NONNULL_BEGIN + +@class Braze; +@class Appboy; +@class ABKFeedController; +@class ABKContentCardsController; +@class ABKInAppMessageController; +@class ABKLocationManager; +@protocol ABKIDFADelegate; +@protocol ABKSdkAuthenticationDelegate; +@protocol ABKURLDelegate; +@protocol ABKImageDelegate; + +@interface _ABKBRZCompat : NSObject + +@property (class, strong, nonatomic, readonly) _ABKBRZCompat *shared; +@property (class, copy, nonatomic, readonly) NSString *sdkVersion; + +@property (strong, nonatomic, nullable) NSNumber *initialized; +@property (strong, nonatomic, nullable) Braze *braze; +@property (strong, nonatomic, nullable) Appboy *appboy; + +@property (strong, nonatomic, readonly) ABKFeedController *feedController; +@property (strong, nonatomic, readonly) ABKContentCardsController *contentCardsController; +@property (strong, nonatomic, nullable) id idfaDelegate; +@property (strong, nonatomic, nullable) id sdkAuthenticationDelegate; +@property (strong, nonatomic, readonly) ABKInAppMessageController *inAppMessageController; +@property (strong, nonatomic, readonly) ABKLocationManager *locationManager; +@property (weak, nonatomic, nullable) id appboyUrlDelegate; +@property (strong, nonatomic, nullable) id imageDelegate; + ++ (void)startWithApiKey:(NSString *)apiKey + appboyOptions:(nullable NSDictionary *)appboyOptions; + +@end + +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift index 3c781b5753..304cf68e3e 100644 --- a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift @@ -284,4 +284,15 @@ open class BrazeInAppMessageUI: return true } + // ***** COMPAT ***** + + /// Provided for compatibility purposes. + @objc(_compat_tryPushOnStack:) + @available(swift, obsoleted: 0.0.1) + public func _compat_tryPushOnStack(message: Braze.InAppMessageRaw) { + guard let message = try? Braze.InAppMessage(message) else { return } + stack.append(message) + } + + // ****************** } diff --git a/Sources/BrazeUICompat/ABKContentCards/AppboyContentCards.h b/Sources/BrazeUICompat/ABKContentCards/AppboyContentCards.h new file mode 100644 index 0000000000..843ee360e1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/AppboyContentCards.h @@ -0,0 +1,9 @@ +// Braze Content Cards View Controllers +#import "ABKContentCardsViewController.h" +#import "ABKContentCardsTableViewController.h" + +// Braze Content Cards Cells +#import "ABKBannerContentCardCell.h" +#import "ABKBaseContentCardCell.h" +#import "ABKCaptionedImageContentCardCell.h" +#import "ABKClassicContentCardCell.h" diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/Base.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/Base.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..d9b7e93145 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/Base.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Done"; +"Appboy.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; +"Appboy.content-cards.no-connection.title" = "Connection Error"; +"Appboy.content-cards.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/ar.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/ar.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..57f0509de8 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/ar.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "تم"; +"Appboy.content-cards.no-card.text" = "ليس لدينا أي تحديث\n يرجى التحقق مرة أخرى لاحقاً"; +"Appboy.content-cards.no-connection.title" = "خلل في الاتصال"; +"Appboy.content-cards.no-connection.message" = "لا يمكن إجراء الاتصال بالشبكة\n يرجى تكرار المحاولة لاحقا "; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/cs.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/cs.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..dc9a5f33de --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/cs.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Hotovo"; +"Appboy.content-cards.no-card.text" = "Nemáme žádné aktualizace.\nZkontrolujte prosím znovu později."; +"Appboy.content-cards.no-connection.title" = "Chyba připojení"; +"Appboy.content-cards.no-connection.message" = "Nelze navázat síťové připojení.\nProsím zkuste to znovu později."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/da.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/da.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..33ad64ccb3 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/da.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Afsluttet"; +"Appboy.content-cards.no-card.text" = "Vi har ingen updates.\nPrøv venligst senere"; +"Appboy.content-cards.no-connection.title" = "Netværksfejl"; +"Appboy.content-cards.no-connection.message" = "Kan ikke etablere netværksforbindelse.\nPrøv venligst senere."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/de.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/de.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..08908e93fa --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/de.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Fertig"; +"Appboy.content-cards.no-card.text" = "Derzeit sind keine Updates verfügbar.\nBitte später noch einmal versuchen."; +"Appboy.content-cards.no-connection.title" = "Verbindungsfehler"; +"Appboy.content-cards.no-connection.message" = "Netzwerkverbindung kann nicht aufgebaut werden.\nBitte später noch einmal versuchen."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/en.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/en.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..d9b7e93145 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/en.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Done"; +"Appboy.content-cards.no-card.text" = "We have no updates.\nPlease check again later."; +"Appboy.content-cards.no-connection.title" = "Connection Error"; +"Appboy.content-cards.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/es-419.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/es-419.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..b1432e0001 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/es-419.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Listo"; +"Appboy.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; +"Appboy.content-cards.no-connection.title" = "Error de conexión"; +"Appboy.content-cards.no-connection.message" = "No se puede establecer conexión con la red.\nPor favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/es-MX.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/es-MX.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..b1432e0001 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/es-MX.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Listo"; +"Appboy.content-cards.no-card.text" = "No tenemos ninguna actualización.\nVuelva a verificar más tarde."; +"Appboy.content-cards.no-connection.title" = "Error de conexión"; +"Appboy.content-cards.no-connection.message" = "No se puede establecer conexión con la red.\nPor favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/es.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/es.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..2b44c78f06 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/es.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Finalizado"; +"Appboy.content-cards.no-card.text" = "No tenemos actualizaciones.\nPor favor compruébelo más tarde."; +"Appboy.content-cards.no-connection.title" = "Error de conexión"; +"Appboy.content-cards.no-connection.message" = "No se puede establecer conexión de red.\nPor favor inténtelo más tarde."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/et.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/et.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..538a53e089 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/et.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Valmis"; +"Appboy.content-cards.no-card.text" = "Uuendusi pole praegu saadaval.\nProovige hiljem uuesti."; +"Appboy.content-cards.no-connection.title" = "Üheduse viga"; +"Appboy.content-cards.no-connection.message" = "Võrguühenduse loomine ebaõnnestus.\nProovige hiljem uuesti."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/fi.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/fi.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..ad4b66df7e --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/fi.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Valmis"; +"Appboy.content-cards.no-card.text" = "Päivityksiä ei ole saatavilla.\nTarkista myöhemmin uudelleen."; +"Appboy.content-cards.no-connection.title" = "Yhteysvirhe"; +"Appboy.content-cards.no-connection.message" = "Verkkoyhteyttä ei voida luoda.\nYritä myöhemmin uudelleen."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/fil.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/fil.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..a25bbcbf89 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/fil.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Gawa na"; +"Appboy.content-cards.no-card.text" = "Wala kaming mga update.\nMangyaring suriin muli sa ibang pagkakataon."; +"Appboy.content-cards.no-connection.title" = "May Error sa Koneksyon"; +"Appboy.content-cards.no-connection.message" = "Hindi makapagtatag ng koneksyon sa network.\nMangyaring subukan muli mamaya."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/fr.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/fr.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..718ea34512 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/fr.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Fini"; +"Appboy.content-cards.no-card.text" = "Aucune mise à jour disponible.\nVeuillez vérifier ultérieurement."; +"Appboy.content-cards.no-connection.title" = "Erreur de connexion."; +"Appboy.content-cards.no-connection.message" = "Impossible d'établir la connexion réseau.\nVeuillez réessayer ultérieurement."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/he.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/he.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..b63fb70dc3 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/he.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "סיום"; +"Appboy.content-cards.no-card.text" = "אין לנו עדכונים\nבבקשה בדוק שוב בקרוב"; +"Appboy.content-cards.no-connection.title" = "שגיאת חיבור רשת"; +"Appboy.content-cards.no-connection.message" = "לא ניתן לקבוע חיבור רשת\nבבקשה נסה שוב בקרוב"; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/hi.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/hi.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..4d595612b4 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/hi.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "कर दिया गया"; +"Appboy.content-cards.no-card.text" = "हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।"; +"Appboy.content-cards.no-connection.title" = "कनेक्शन की त्रुटि"; +"Appboy.content-cards.no-connection.message" = "नेटवर्क कनेक्शन स्थापित नहीं हो रहा है। कृपया बाद में दोबारा प्रयास करें।."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/id.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/id.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..8db06632f5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/id.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Selesai"; +"Appboy.content-cards.no-card.text" = "Kami tidak memiliki pembaruan.\nCoba lagi nanti."; +"Appboy.content-cards.no-connection.title" = "Kesalahan Koneksi"; +"Appboy.content-cards.no-connection.message" = "Tidak bisa melakukan koneksi jaringan.\nCoba lagi nanti."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned.png b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned.png new file mode 100644 index 0000000000..c94da28ec2 Binary files /dev/null and b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned.png differ diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@2x.png b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@2x.png new file mode 100644 index 0000000000..34ab7da777 Binary files /dev/null and b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@2x.png differ diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@3x.png b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@3x.png new file mode 100644 index 0000000000..fd59677f67 Binary files /dev/null and b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_icon_pinned@3x.png differ diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg.png b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg.png new file mode 100644 index 0000000000..0968979999 Binary files /dev/null and b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg.png differ diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg@2x.png b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg@2x.png new file mode 100644 index 0000000000..7d34f620ef Binary files /dev/null and b/Sources/BrazeUICompat/ABKContentCards/Resources/images/appboy_cc_noimage_lrg@2x.png differ diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/it.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/it.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..abead6344d --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/it.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Fatto"; +"Appboy.content-cards.no-card.text" = "Non ci sono aggiornamenti.\nRicontrollare più tardi."; +"Appboy.content-cards.no-connection.title" = "Errore di connessione"; +"Appboy.content-cards.no-connection.message" = "Impossibile stabilire una connessione di rete.\nRiprovare più tardi."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/ja.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/ja.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..db4b5b1cb2 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/ja.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完了"; +"Appboy.content-cards.no-card.text" = "アップデートはありません。\n後でもう一度確認してください。"; +"Appboy.content-cards.no-connection.title" = "接続エラー"; +"Appboy.content-cards.no-connection.message" = "ネットワークに接続できません。\n後でもう一度試してください。"; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/km.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/km.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..fe4d5285ef --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/km.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "បានសម្រេច"; +"Appboy.content-cards.no-card.text" = "យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ."; +"Appboy.content-cards.no-connection.title" = "កំហុសឆ្គងក្នុងការតភ្ជាប់"; +"Appboy.content-cards.no-connection.message" = "មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/ko.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/ko.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..6aedefc229 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/ko.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "완료 "; +"Appboy.content-cards.no-card.text" = "업데이트가 없습니다.\n다음에 다시 확인해 주십시오."; +"Appboy.content-cards.no-connection.title" = "연결 오류"; +"Appboy.content-cards.no-connection.message" = "네트워크 연결을 할 수 없습니다.\n나중에 다시 시도해 주십시오."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/lo.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/lo.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..698c1ed2de --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/lo.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "ສຳ​ເລັດ"; +"Appboy.content-cards.no-card.text" = "ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ.\nກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; +"Appboy.content-cards.no-connection.title" = "ການ​ເຊື່ອມ​ຕໍ່​ຜິດ​ພາດ"; +"Appboy.content-cards.no-connection.message" = "ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້.\nກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/ms.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/ms.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..7a0bbd4865 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/ms.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Selesai"; +"Appboy.content-cards.no-card.text" = "Tiada kemas kini.\nSila periksa kemudian."; +"Appboy.content-cards.no-connection.title" = "Ralat Sambungan"; +"Appboy.content-cards.no-connection.message" = "Tidak boleh membuat sambungan rangkaian.\nSila cuba kemudian."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/my.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/my.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..587eaf3209 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/my.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "ျပီးျပီ"; +"Appboy.content-cards.no-card.text" = "ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ ."; +"Appboy.content-cards.no-connection.title" = "ဆက္သြယ္ေရး အမွား"; +"Appboy.content-cards.no-connection.message" = "ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။ ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/nb.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/nb.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..b3da18bd52 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/nb.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Ferdig"; +"Appboy.content-cards.no-card.text" = "Vi har ingen oppdateringer.\nVennligst sjekk igjen senere."; +"Appboy.content-cards.no-connection.title" = "Tilkoblingsfeil"; +"Appboy.content-cards.no-connection.message" = "Kan ikke etablere nettverkstilkobling.\nVennligst prøv igjen senere."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/nl.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/nl.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..f89fb0551b --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/nl.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Gereed"; +"Appboy.content-cards.no-card.text" = "Er zijn geen updates.\nProbeer het later opnieuw."; +"Appboy.content-cards.no-connection.title" = "Verbindingsfout"; +"Appboy.content-cards.no-connection.message" = "Kan geen netwerkverbinding maken.\nProbeer het later opnieuw."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/pl.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/pl.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..07ef91ace9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/pl.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Gotowe"; +"Appboy.content-cards.no-card.text" = "Brak aktualizacji.\nProszę sprawdzić ponownie później."; +"Appboy.content-cards.no-connection.title" = "Błąd połączenia"; +"Appboy.content-cards.no-connection.message" = "Nie można ustanowić połączenia z siecią.\nProszę spróbować ponownie później."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/pt-PT.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/pt-PT.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..f35fa8e025 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/pt-PT.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Concluído"; +"Appboy.content-cards.no-card.text" = "Não temos atualizações.\nPor favor, verifique mais tarde."; +"Appboy.content-cards.no-connection.title" = "Erro de Ligação"; +"Appboy.content-cards.no-connection.message" = "Não é possível estabelecer a ligação à rede.\nPor favor, tente mais tarde."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/pt.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/pt.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..f2e5bf1071 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/pt.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Concluído"; +"Appboy.content-cards.no-card.text" = "Não temos nenhuma atualização.\nVerifique novamente mais tarde."; +"Appboy.content-cards.no-connection.title" = "Erro de conexão"; +"Appboy.content-cards.no-connection.message" = "Não é possível estabelecer uma conexão de rede.\nTente novamente mais tarde."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/ru.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/ru.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..b5524d5471 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/ru.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Готово"; +"Appboy.content-cards.no-card.text" = "Обновления недоступны.\nПожалуйста, проверьте снова позже."; +"Appboy.content-cards.no-connection.title" = "Ошибка подключения"; +"Appboy.content-cards.no-connection.message" = "Невозможно установить сетевое подключение.\nПожалуйста, повторите попытку позже."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/sv.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/sv.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..e798bd80c9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/sv.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Klar"; +"Appboy.content-cards.no-card.text" = "Det finns inga uppdateringar.\nFörsök igen senare."; +"Appboy.content-cards.no-connection.title" = "Anslutningsfel"; +"Appboy.content-cards.no-connection.message" = "Det gick inte att skapa en nätverksanslutning.\nFörsök igen senare."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/th.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/th.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..faa46e87b4 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/th.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "เสร็จสิ้น"; +"Appboy.content-cards.no-card.text" = "เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง."; +"Appboy.content-cards.no-connection.title" = "ผิดพลาดการเชื่อมต่อ"; +"Appboy.content-cards.no-connection.message" = "ไม่สามารถสร้างการเชื่อมต่อเครือข่าย กรุณาลองใหม่ภายหลัง."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/uk.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/uk.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..cc95ac19ed --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/uk.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Готово"; +"Appboy.content-cards.no-card.text" = "Оновлення недоступні.\nБудь ласка, перевірте знову пізніше."; +"Appboy.content-cards.no-connection.title" = "Помилка підключення"; +"Appboy.content-cards.no-connection.message" = "неможливо встановити з'єднання з мережею.\nБудь ласка, спробуйте ще раз пізніше."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/vi.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/vi.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..de42a65df5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/vi.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "Hoàn tất"; +"Appboy.content-cards.no-card.text" = "Chúng tôi không có cập nhật nào.\nVui lòng kiểm tra lại sau."; +"Appboy.content-cards.no-connection.title" = "Lỗi Kết Nối"; +"Appboy.content-cards.no-connection.message" = "Không thể thiết lập kết nối mạng.\nVui lòng thử lại sau."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/zh-HK.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-HK.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..66ea056752 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-HK.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完成"; +"Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.content-cards.no-connection.title" = "連線錯誤"; +"Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hans.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hans.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..d9e2da2816 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hans.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完成"; +"Appboy.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试."; +"Appboy.content-cards.no-connection.title" = "连接错误"; +"Appboy.content-cards.no-connection.message" = "无法建立网络连接.\n请稍候再试."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hant.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hant.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..66ea056752 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-Hant.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完成"; +"Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.content-cards.no-connection.title" = "連線錯誤"; +"Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/zh-TW.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-TW.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..66ea056752 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/zh-TW.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完成"; +"Appboy.content-cards.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.content-cards.no-connection.title" = "連線錯誤"; +"Appboy.content-cards.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKContentCards/Resources/zh.lproj/AppboyContentCardsLocalizable.strings b/Sources/BrazeUICompat/ABKContentCards/Resources/zh.lproj/AppboyContentCardsLocalizable.strings new file mode 100644 index 0000000000..d9e2da2816 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/Resources/zh.lproj/AppboyContentCardsLocalizable.strings @@ -0,0 +1,5 @@ +/* Content Cards Context Labels */ +"Appboy.content-cards.done-button.title" = "完成"; +"Appboy.content-cards.no-card.text" = "暂时没有更新.\n请稍后再试."; +"Appboy.content-cards.no-connection.title" = "连接错误"; +"Appboy.content-cards.no-connection.message" = "无法建立网络连接.\n请稍候再试."; diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.h new file mode 100644 index 0000000000..f50d95c94f --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.h @@ -0,0 +1,177 @@ +#import +#import "ABKBaseContentCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKContentCard; +@protocol ABKContentCardsTableViewControllerDelegate; + +@interface ABKContentCardsTableViewController : UITableViewController + +/*! + * UI elements which are used in the Content Cards table view. You can find them in the Content Cards Storyboard. + */ +@property (nonatomic, strong) IBOutlet UIView *emptyFeedView; +@property (nonatomic, strong) IBOutlet UILabel *emptyFeedLabel; + +/*! + * The ABKContentCardsTableViewController delegate + */ +@property (weak, nonatomic) id delegate; + +/*! + * This property stores the cards displayed in the Content Cards feed. By default, the view controller + * updates this value when it receives an ABKContentCardsProcessedNotification notification from the Braze SDK. + * + * This field's value should not be set directly from a subclass; instead, it should be set from within a populateContentCards: + * implementation. + */ +@property (nonatomic) NSMutableArray *cards; + +/*! + * This property allows you to enable or disable the unread indicator on the cards. The default + * value is NO, which will enable the displaying of the unread indicator on cards. + */ +@property (assign, nonatomic) BOOL disableUnreadIndicator; + +/*! + * This property defines the timeout for stored Content Cards in the Braze SDK. If the cards in the + * Braze SDK are older than this value, the Content Cards view controller will request a Content Cards update. + * + * The default value is 60 seconds. + */ +@property NSTimeInterval cacheTimeout; + +/*! + * If set, this property overrides the maximum width of Content Cards set by the storyboard. + */ +@property (assign, nonatomic) CGFloat maxContentCardWidth; + +/*! + * This boolean determines if the Content Card will attempt to use dark theme colors, granted the device + * is in dark mode. + * + * @discussion The default of this value is YES but can be overriden before the view controller is presented + * to ensure that the dark theme is disabled for any Content Card displayed. + */ +@property (assign, nonatomic) BOOL enableDarkTheme; + +/*! + * @discussion This method returns an instance of ABKContentCardsTableViewController. You can call it + * to get a Content Cards view controller for your navigation controller. + * @warning To use a custom Content Card view controller, instantiate your own subclass instead + * (e.g. via alloc / init). + */ ++ (instancetype)getNavigationContentCardsViewController; + +/*! + * @discussion This method returns the localized string from AppboyContentCardsLocalizable.strings file. + * You can easily override the localized string by adding the keys and the translations to your own + * Localizable.strings file. + * + * To do custom handling with the Appboy localized string, you can override this method in a + * subclass. + */ +- (NSString *)localizedAppboyContentCardsString:(NSString *)key; + +/*! + * @discussion initialization that always occurs for the Content Cards table view controller + */ +- (void)setUp; + +/*! + * @discussion Initialization that is in place of Storyboard or XIB initialization. + * This method should call all the property specific setUp methods. + */ +- (void)setUpUI; + +/*! + * @discussion specific view property initialization that is in place of Storyboard or XIB initialization. + * Called by the setUpUI method and is exposed here to allow overriding. + */ +- (void)setUpEmptyFeedLabel; +- (void)setUpEmptyFeedView; + +/*! + * @discussion Registers Content Card type identifiers with the cell classes + * that implement their view. + */ +- (void)registerTableViewCellClasses; + +/*! + * @discussion Given a Content Card, return its type identifier + */ +- (NSString *)findCellIdentifierWithCard:(ABKContentCard *)card; + +/*! + * @param tableView The table view which need the cell to diplay the card UI. + * @param indexPath The index path of the card UI in the table view. + * @param card The card model for the cell. + * + * @discussion This method dequeues and returns the corresponding card cell based on card type from + * the given table view. + */ +- (ABKBaseContentCardCell *)dequeueCellFromTableView:(UITableView *)tableView + forIndexPath:(NSIndexPath *)indexPath + forCard:(ABKContentCard *)card; + +/*! + * @discussion This method handles the user's click on the card. + * + * If you wish to handle card clicks yourself, refer to ABKContentCardsTableViewControllerDelegate's + * contentCardTableViewController:shouldHandleCardClick: method. + * + * @warning Overriding handleCardClick: yourself might prevent + * ABKContentCardsTableViewControllerDelegate's contentCardTableViewController:shouldHandleCardClick: + * and contentCardTableViewController:didHandleCardClick: from firing properly. + * + * If you decide to override this method, you must call [card logContentCardClicked] manually inside of your + * new method to send the click event to the Braze server. + */ +- (void)handleCardClick:(ABKContentCard *)card; + +- (void)requestNewCardsIfTimeout; + +/*! + * @discussion This method is called when the cards stored in the cards property should be refreshed. + */ +- (void)populateContentCards; + +@end + + +@protocol ABKContentCardsTableViewControllerDelegate + +@optional + +/*! + * Asks the delegate if the Braze SDK should handle the content card click action. + * + * @warning This method might not be called if you overrode handleCardClick: + * + * @param viewController The view controller displaying the content card. + * @param url The content card's url. + * @return YES to let the Braze SDK handle the click action, NO if you wish to handle the click action + * yourself. + */ +- (BOOL)contentCardTableViewController:(ABKContentCardsTableViewController *)viewController + shouldHandleCardClick:(NSURL *)url; + +/*! + * Informs the delegate that the content card click action was handled by the Braze SDK. + * + * This method is not called if the delegate method `contentCardTableViewController:shouldHandleCardClick:` + * returns NO. + * + * @warning This method might not be called if you overrode handleCardClick: + * + * @param viewController The view controller displaying the content card. + * @param url The content card's url. + */ +- (void)contentCardTableViewController:(ABKContentCardsTableViewController *)viewController + didHandleCardClick:(NSURL *)url; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.m new file mode 100644 index 0000000000..9501937dee --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsTableViewController.m @@ -0,0 +1,483 @@ +#import "ABKContentCardsTableViewController.h" +#import "ABKContentCardsWebViewController.h" + +#import "ABKBannerContentCardCell.h" +#import "ABKCaptionedImageContentCardCell.h" +#import "ABKClassicContentCardCell.h" +#import "ABKClassicImageContentCardCell.h" +#import "ABKControlTableViewCell.h" + +#import "ABKUIUtils.h" +#import "ABKUIURLUtils.h" + +@import BrazeKit; +@import BrazeKitCompat; + +static double const ABKContentCardsCacheTimeout = 1 * 60; // 1 minute +static CGFloat const ABKContentCardsCellEstimatedHeight = 400.0f; + +@interface ABKContentCardsTableViewController () + +/*! + * This set stores the content cards IDs for which the impressions have been logged. + */ +@property (nonatomic) NSMutableSet *cardImpressions; + +/*! + * This set stores IDs for the content cards that are unviewed and on the screen right now. + */ +@property (nonatomic) NSMutableSet *unviewedOnScreenCards; + +/*! + * There is some initialization such as associating which cell class to use in the table view that + * is the responsibility of the storyboard if one is provided. If no story board is used then + * the code in viewDidLoad will handle it. We can tell based on which init method is used. + */ +@property (nonatomic) BOOL usesStoryboard; + +- (void)logCardImpressionIfNeeded:(ABKContentCard *)card; +- (void)requestContentCardsRefresh; +- (void)contentCardsUpdated:(NSNotification *)notification; + +@end + +@implementation ABKContentCardsTableViewController + +#pragma mark - Initialization + +- (instancetype)init { + self = [super init]; + if (self) { + self.usesStoryboard = NO; + [self setUp]; + [self setUpUI]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + self.usesStoryboard = YES; + [self setUp]; + } + return self; +} + +#pragma mark - SetUp + +- (void)setUp { + _cacheTimeout = ABKContentCardsCacheTimeout; + _cardImpressions = [NSMutableSet set]; + _unviewedOnScreenCards = [NSMutableSet set]; + _enableDarkTheme = YES; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(contentCardsUpdated:) + name:ABKContentCardsProcessedNotification + object:nil]; +} + +- (void)setUpUI { + [self setUpEmptyFeedLabel]; + [self setUpEmptyFeedView]; +} + +- (void)setUpEmptyFeedLabel { + self.emptyFeedLabel = [[UILabel alloc] init]; + self.emptyFeedLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleBody weight:UIFontWeightRegular]; + self.emptyFeedLabel.adjustsFontSizeToFitWidth = YES; + self.emptyFeedLabel.adjustsFontForContentSizeCategory = YES; + self.emptyFeedLabel.textAlignment = NSTextAlignmentCenter; + self.emptyFeedLabel.numberOfLines = 0; + self.emptyFeedLabel.translatesAutoresizingMaskIntoConstraints = NO; +} + +- (void)setUpEmptyFeedView { + self.emptyFeedView = [[UIView alloc] init]; + self.emptyFeedView.backgroundColor = [UIColor clearColor]; + [self.emptyFeedView addSubview:self.emptyFeedLabel]; + self.edgesForExtendedLayout = UIRectEdgeNone; + + NSLayoutConstraint *centerXConstraint = [self.emptyFeedLabel.centerXAnchor constraintEqualToAnchor:self.emptyFeedView.centerXAnchor]; + NSLayoutConstraint *centerYConstraint = [self.emptyFeedLabel.centerYAnchor constraintEqualToAnchor:self.emptyFeedView.centerYAnchor]; + NSLayoutConstraint *leadingConstraint = [self.emptyFeedLabel.leadingAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.leadingAnchor]; + NSLayoutConstraint *trailingConstraint = [self.emptyFeedLabel.trailingAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.trailingAnchor]; + NSLayoutConstraint *topConstraint = [self.emptyFeedLabel.topAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.topAnchor]; + NSLayoutConstraint *bottomConstraint = [self.emptyFeedLabel.bottomAnchor constraintEqualToAnchor:self.emptyFeedView.layoutMarginsGuide.bottomAnchor]; + [NSLayoutConstraint activateConstraints:@[centerXConstraint, centerYConstraint, + leadingConstraint, trailingConstraint, + topConstraint, bottomConstraint]]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)registerTableViewCellClasses { + [self.tableView registerClass:[ABKCaptionedImageContentCardCell class] + forCellReuseIdentifier:@"ABKCaptionedImageContentCardCell"]; + [self.tableView registerClass:[ABKBannerContentCardCell class] + forCellReuseIdentifier:@"ABKBannerContentCardCell"]; + [self.tableView registerClass:[ABKClassicContentCardCell class] + forCellReuseIdentifier:@"ABKClassicCardCell"]; + [self.tableView registerClass:[ABKControlTableViewCell class] + forCellReuseIdentifier:@"ABKControlCardCell"]; + [self.tableView registerClass:[ABKClassicImageContentCardCell class] + forCellReuseIdentifier:@"ABKClassicImageCardCell"]; +} + +# pragma mark - View Controller Life Cycle Methods + +- (void)viewDidLoad { + [super viewDidLoad]; + if (@available(iOS 13.0, *)) { + if (self.enableDarkTheme) { + // This value will respect the system UI style of dark or light mode + self.overrideUserInterfaceStyle = UIUserInterfaceStyleUnspecified; + } else { + self.overrideUserInterfaceStyle = UIUserInterfaceStyleLight; + } + } + + if (!self.usesStoryboard) { + self.emptyFeedLabel.text = [self localizedAppboyContentCardsString:@"Appboy.content-cards.no-card.text"]; + + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + + if (@available(iOS 13.0, *)) { + self.view.backgroundColor = [UIColor systemGroupedBackgroundColor]; + } else { + self.view.backgroundColor = [UIColor groupTableViewBackgroundColor]; + } + + [self registerTableViewCellClasses]; + + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + [refreshControl addTarget:self action:@selector(refreshContentCards:) + forControlEvents:UIControlEventValueChanged]; + self.refreshControl = refreshControl; + } +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [self requestNewCardsIfTimeout]; + [self updateAndDisplayCardsFromCache]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self.tableView reloadData]; + }); +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [[Appboy sharedInstance] logContentCardsDisplayed]; +} + +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:animated]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [[Braze sharedInstance] _contentCardsApplyLocalCards]; +} + +- (void)viewWillTransitionToSize:(CGSize)size + withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + [coordinator animateAlongsideTransition:nil completion:^(id _Nonnull context) { + [self.tableView reloadData]; + }]; +} + +#pragma mark - Update And Display Cached Cards + +- (void)populateContentCards { + self.cards = [NSMutableArray arrayWithArray:[Appboy.sharedInstance.contentCardsController getContentCards]]; +} + +- (void)requestContentCardsRefresh { + [Appboy.sharedInstance requestContentCardsRefresh]; +} + +- (IBAction)refreshContentCards:(UIRefreshControl *)sender { + // Remove visible cards from unviewedOnScreenCards + NSArray *visibleIndexPaths = [self.tableView indexPathsForVisibleRows]; + for (NSIndexPath *indexPath in visibleIndexPaths) { + ABKContentCard *card = self.cards[indexPath.row]; + [self.unviewedOnScreenCards removeObject:card.idString]; + } + + [self requestContentCardsRefresh]; +} + +- (void)requestNewCardsIfTimeout { + NSTimeInterval passedTime = fabs(Appboy.sharedInstance.contentCardsController.lastUpdate.timeIntervalSinceNow); + if (passedTime > self.cacheTimeout) { + [self requestContentCardsRefresh]; + } else { + // timeout is not passed, so we don't send a request for new content cards + [self.refreshControl endRefreshing]; + } +} + +- (void)contentCardsUpdated:(NSNotification *)notification { + BOOL isSuccessful = [notification.userInfo[ABKContentCardsProcessedIsSuccessfulKey] boolValue]; + if (isSuccessful) { + [self updateAndDisplayCardsFromCache]; + } + [self.refreshControl endRefreshing]; +} + +- (void)updateAndDisplayCardsFromCache { + [self populateContentCards]; + if (self.cards == nil || self.cards.count == 0) { + [self hideTableViewAndShowViewInBackground:self.emptyFeedView]; + } else { + [self showTableViewAndHideBackgroundViews]; + } + [self.tableView reloadData]; +} + +- (void)logCardImpressionIfNeeded:(ABKContentCard *)card { + if ([self.cardImpressions containsObject:card.idString]) { + // do nothing if we have already logged an impression + return; + } + + if (![card isControlCard]) { + if (card.viewed == NO) { + [self.unviewedOnScreenCards addObject:card.idString]; + } + } + [card logContentCardImpression]; + [self.cardImpressions addObject:card.idString]; +} + +#pragma mark - Table view header view + +- (void)hideTableViewAndShowViewInBackground:(UIView *)view { + view.hidden = NO; + view.frame = self.view.bounds; + [view layoutIfNeeded]; + self.tableView.backgroundView = view; +} + +- (void)showTableViewAndHideBackgroundViews { + self.emptyFeedView.hidden = YES; + self.tableView.backgroundView = nil; +} + +#pragma mark - Configuration Update + +- (void)setDisableUnreadIndicator:(BOOL)disableUnreadIndicator { + if (disableUnreadIndicator != _disableUnreadIndicator) { + _disableUnreadIndicator = disableUnreadIndicator; + [self updateAndDisplayCardsFromCache]; + } +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.cards.count; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { + if ([self.cards[indexPath.row] isControlCard]) { + return 0; + } + return UITableViewAutomaticDimension; +} + +// Overrides the storyboard to get accurate cell height estimates to prevent from having +// the scrollView jump if a cell needs to resize itself +- (CGFloat)tableView:(UITableView *)tableView + estimatedHeightForRowAtIndexPath:(nonnull NSIndexPath *)indexPath { + return ABKContentCardsCellEstimatedHeight; +} + +- (void)tableView:(UITableView *)tableView + willDisplayCell:(UITableViewCell *)cell +forRowAtIndexPath:(NSIndexPath *)indexPath { + ABKContentCard *card = self.cards[indexPath.row]; + BOOL cellVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; + if (cellVisible) { + [self logCardImpressionIfNeeded:card]; + } +} + +- (void)tableView:(UITableView *)tableView + didEndDisplayingCell:(UITableViewCell *)cell + forRowAtIndexPath:(NSIndexPath *)indexPath { + // We mark a cell as read only if it's not visible already. + // But this method might be called for visible cells too because of dynamic heights. + BOOL cellIsVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; + if (!cellIsVisible && indexPath.row < self.cards.count) { + // indexPath.row is out of bounds if the card did end displaying due to its deletion + + ABKContentCard *card = self.cards[indexPath.row]; + [self.unviewedOnScreenCards removeObject:card.idString]; + } +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + ABKContentCard *card = self.cards[indexPath.row]; + ABKBaseContentCardCell *cell = [self dequeueCellFromTableView:tableView + forIndexPath:indexPath + forCard:card]; + if (self.maxContentCardWidth > 0.0) { + cell.cardWidthConstraint.constant = self.maxContentCardWidth; + } + + BOOL viewedSetting = card.viewed; + if ([self.unviewedOnScreenCards containsObject:card.idString]) { + card.viewed = NO; + } + cell.delegate = self; + [cell applyCard:card]; + card.viewed = viewedSetting; + cell.hideUnreadIndicator = self.disableUnreadIndicator; + return cell; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + ABKContentCard *card = self.cards[indexPath.row]; + [self handleCardClick:card]; + + // Remove card from unviewedOnScreenCards + [self.unviewedOnScreenCards removeObject:card.idString]; + // Hide unviewed indicator + ABKBaseContentCardCell *cell = [tableView cellForRowAtIndexPath:indexPath]; + cell.unviewedLineView.hidden = YES; +} + +- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { + ABKContentCard *card = self.cards[indexPath.row]; + return card.dismissible; +} + +- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { + return UITableViewCellEditingStyleDelete; +} + +- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { + if (editingStyle == UITableViewCellEditingStyleDelete) { + ABKContentCard *card = self.cards[indexPath.row]; + [card logContentCardDismissed]; + [self.cards removeObjectAtIndex:indexPath.row]; + [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; + + if (self.cards.count == 0) { + [self hideTableViewAndShowViewInBackground:self.emptyFeedView]; + } + } +} + +#pragma mark - Dequeue cells + +- (ABKBaseContentCardCell *)dequeueCellFromTableView:(UITableView *)tableView + forIndexPath:(NSIndexPath *)indexPath + forCard:(ABKContentCard *)card { + return [tableView dequeueReusableCellWithIdentifier:[self findCellIdentifierWithCard:card] + forIndexPath:indexPath]; +} + +- (NSString *)findCellIdentifierWithCard:(ABKContentCard *)card { + if ([card isControlCard]) { + return @"ABKControlCardCell"; + } + if ([card isKindOfClass:[ABKBannerContentCard class]]) { + return @"ABKBannerContentCardCell"; + } else if ([card isKindOfClass:[ABKCaptionedImageContentCard class]]) { + return @"ABKCaptionedImageContentCardCell"; + } else if ([card isKindOfClass:[ABKClassicContentCard class]]) { + NSString *imageURL = [((ABKClassicContentCard *)card) image]; + if (imageURL.length > 0) { + return @"ABKClassicImageCardCell"; + } else { + return @"ABKClassicCardCell"; + } + } + return nil; +} + +#pragma mark - Card Click Actions + +- (void)handleCardClick:(ABKContentCard *)card { + // Log a card click only when the card has the url property with a valid url. + if (card.urlString.length <= 0) { + return; + } + + [card logContentCardClicked]; + NSURL *cardURL = [ABKUIURLUtils getEncodedURIFromString:card.urlString]; + + // Content Cards Delegate handles card click action + if ([self.delegate respondsToSelector:@selector(contentCardTableViewController:shouldHandleCardClick:)] && + ![self.delegate contentCardTableViewController:self shouldHandleCardClick:cardURL]) { + return; + } + + // URL Delegate + if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate + handlesURL:cardURL + fromChannel:ABKContentCardChannel + withExtras:nil]) { + return; + } + + // WebView + if ([ABKUIURLUtils URL:cardURL shouldOpenInWebView:card.openUrlInWebView]) { + [self openURLInWebView:cardURL]; + } else { + // System + [ABKUIURLUtils openURLWithSystem:cardURL]; + } + + // Delegate inform card click action + if ([self.delegate respondsToSelector:@selector(contentCardTableViewController:didHandleCardClick:)]) { + [self.delegate contentCardTableViewController:self didHandleCardClick:cardURL]; + } +} + +- (void)openURLInWebView:(NSURL *)url { + ABKContentCardsWebViewController *webVC = [ABKContentCardsWebViewController new]; + webVC.url = url; + webVC.showDoneButton = (self.navigationItem.rightBarButtonItem != nil); + [self.navigationController pushViewController:webVC animated:YES]; +} + +#pragma mark - Utility Methods + ++ (instancetype)getNavigationContentCardsViewController { + return [[ABKContentCardsTableViewController alloc] init]; +} + +- (NSString *)localizedAppboyContentCardsString:(NSString *)key { + return [ABKUIUtils getLocalizedString:key + inAppboyBundle:[ABKUIUtils bundle:[ABKContentCardsTableViewController class] channel:ABKContentCardChannel] + table:@"AppboyContentCardsLocalizable"]; +} + +#pragma mark - ABKBaseContentCardCellDelegate + +- (void)cellRequestSizeUpdate:(UITableViewCell *)cell { + NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; + if (indexPath == nil) { + return; + } + + [UIView performWithoutAnimation:^{ + [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; + }]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.h new file mode 100644 index 0000000000..dff2557614 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.h @@ -0,0 +1,12 @@ +#import +#import "ABKContentCardsTableViewController.h" + +@interface ABKContentCardsViewController : UINavigationController + +/*! + * This property is the table view controller which displays all the content cards. It's also the root view + * controller. + */ +@property (strong, nonatomic) ABKContentCardsTableViewController *contentCardsViewController; + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.m new file mode 100644 index 0000000000..b4884f9683 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsViewController.m @@ -0,0 +1,31 @@ +#import "ABKContentCardsViewController.h" +#import "ABKUIUtils.h" + +@implementation ABKContentCardsViewController + +- (instancetype)init { + self = [super initWithRootViewController:[[ABKContentCardsTableViewController alloc] init]]; + if (self) { + self.contentCardsViewController = self.viewControllers.firstObject; + [self addDoneButton]; +#if !TARGET_OS_TV + if (@available(iOS 15.0, *)) { + self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; + } +#endif + } + return self; +} + +- (void)addDoneButton { + UIBarButtonItem *closeBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(dismissContentCardsViewController:)]; + [self.contentCardsViewController.navigationItem setRightBarButtonItem:closeBarButton]; +} + +- (IBAction)dismissContentCardsViewController:(id)sender { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.h new file mode 100644 index 0000000000..5c5664e5ef --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.h @@ -0,0 +1,29 @@ +#import +#import + +@interface ABKContentCardsWebViewController : UIViewController + +/*! + * The URL the modal web view controller should open. Please note that this is the initial URL and + * won't be updated if the initial URL re-directs to another URL. + */ +@property NSURL *url; + +/*! + * The WKWebView which displays the web page. + */ +@property (nonatomic) IBOutlet WKWebView *webView; + +/*! + * The UIProgressView which shows the web view loading process. It will be on top of the web view and + * will disappear as soon as the page is loaded. + */ +@property (nonatomic) IBOutlet UIProgressView *progressBar; + +/*! + * The property tells the web view controller to add a Done button or not. The default value is NO. + * Please set this property before displaying the web view controller. + */ +@property (nonatomic) BOOL showDoneButton; + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.m new file mode 100644 index 0000000000..5871e80a60 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/ABKContentCardsWebViewController.m @@ -0,0 +1,184 @@ +#import "ABKContentCardsWebViewController.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static NSString *const EstimatedProgressKeyPath = @"estimatedProgress"; +static NSString *const LocalizedNoConnectionKey = @"Appboy.no-connection.message"; + +@implementation ABKContentCardsWebViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.webView.navigationDelegate = self; + self.webView = [self getWebView]; + self.view = self.webView; + +#if !TARGET_OS_TV + if (@available(iOS 15.0, *)) { + self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; + } +#endif + + [self setupProgressBar]; + + if (self.showDoneButton) { + UIBarButtonItem *closeBarButton = [self getDoneBarButtonItem]; + [self.navigationItem setRightBarButtonItem:closeBarButton]; + } + + [self.webView addObserver:self + forKeyPath:EstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + + [self.webView loadRequest:[NSURLRequest requestWithURL:self.url]]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([ABKUIUtils string:EstimatedProgressKeyPath isEqualToString:keyPath]) { + if (self.webView.estimatedProgress == 1.0) { + [UIView animateWithDuration:1 animations:^{ + self.progressBar.alpha = 0.0; + }]; + } else if (self.webView.estimatedProgress < 1.0) { + self.progressBar.alpha = 1.0; + [self.progressBar setProgress:self.webView.estimatedProgress animated:YES]; + } + } +} + +- (void)dealloc { + [self.webView removeObserver:self forKeyPath:EstimatedProgressKeyPath]; +} + +#pragma mark - Customization Methods + +/*! + * @discussion Returns a WKWebView object, whose navigationDelegate is this ABKContentCardsWebViewController instance. + * + * If you want to do any customization to the WKWebView, please override this method in an ABKContentCardsWebViewController + * category and return the customized WKWebView. All instances of ABKContentCardsWebViewController will then + * call the category's `getWebView` implementation instead of this method. + * + */ +- (WKWebView *)getWebView { + WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero]; + webView.navigationDelegate = self; + return webView; +} + +/*! + * + * @discussion Creates a UIProgressView and puts it on top of the web view. + * + * If you want to do any customization to the progress bar, please override this method in an ABKContentCardsWebViewController + * category and set up the progress bar. All instances of ABKContentCardsWebViewController will then + * call the category's `setupProgressBar:` implementation instead of this method. + * + */ +- (void)setupProgressBar{ + UIProgressView *progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; + progressBar.alpha = 0; + self.progressBar = progressBar; + + [self.view addSubview:self.progressBar]; + self.progressBar.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.progressBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0.0]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progressBar]|" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:@{@"progressBar" : self.progressBar}]]; +} + +/*! + * @discussion Returns the Done UIBarButtonItem, which allows the user to dismiss the modal web view. + * + * If you want to do any customization to the Done button, please override this method in an ABKContentCardsWebViewController + * category and return the customized UIBarButtonItem. All instances of ABKContentCardsWebViewController will then + * call the category's `getDoneBarButtonItem` implementation instead of this method. + * + */ +- (UIBarButtonItem *)getDoneBarButtonItem { + return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(closeButtonPressed:)]; +} + +- (void)closeButtonPressed:(id)sender { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - WKNavigationDelegate methods + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSString *urlString = [[navigationAction.request.mainDocumentURL absoluteString] lowercaseString]; + NSArray *stringComponents = [urlString componentsSeparatedByString:@":"]; + if ([stringComponents[1] hasPrefix:@"//itunes.apple.com"] || + (![stringComponents[0] isEqual:@"http"] && + ![stringComponents[0] isEqual:@"https"])) { + // Dismiss the modal web view and let the system handle the deep links + + /* + if ([[UIApplication sharedApplication] openURL:navigationAction.request.URL]) { + decisionHandler(WKNavigationActionPolicyCancel); + [self.navigationController popViewControllerAnimated:NO]; + return; + } + */ + + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { + if (success) { + decisionHandler(WKNavigationActionPolicyCancel); + [self.navigationController popViewControllerAnimated:NO]; + } else { + decisionHandler(WKNavigationActionPolicyAllow); + } + }]; + } + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + self.progressBar.alpha = 0.0; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + self.progressBar.alpha = 0.0; + + UILabel *label = [[UILabel alloc] init]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + NSString *localizedNoConectionMessage = NSLocalizedString(@"Appboy.no-connection.message", + @"No connection error message for URL loading failure"); + if (localizedNoConectionMessage.length == 0 || [ABKUIUtils string:LocalizedNoConnectionKey isEqualToString:localizedNoConectionMessage]) { + localizedNoConectionMessage = [ABKNoConnectionLocalization getNoConnectionLocalizedString]; + } + label.text = localizedNoConectionMessage; + [self.webView addSubview:label]; + label.translatesAutoresizingMaskIntoConstraints = NO; + [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[noConnectionLabel]-10-|" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:@{@"noConnectionLabel" : label}]]; + [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[noConnectionLabel]|" + options:NSLayoutFormatAlignAllCenterY + metrics:nil + views:@{@"noConnectionLabel" : label}]]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.h new file mode 100644 index 0000000000..91b5c4be1b --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.h @@ -0,0 +1,17 @@ +#import "ABKBaseContentCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKBannerContentCard; + +@interface ABKBannerContentCardCell : ABKBaseContentCardCell + +@property (strong, nonatomic) IBOutlet UIImageView *bannerImageView; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; + +- (void)applyCard:(ABKBannerContentCard *)bannerCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.m new file mode 100644 index 0000000000..bc4c2bc859 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.m @@ -0,0 +1,100 @@ +#import "ABKBannerContentCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@implementation ABKBannerContentCardCell + +#pragma mark - Properties + +- (UIImageView *)bannerImageView { + if (_bannerImageView != nil) { + return _bannerImageView; + } + + UIImageView *bannerImageView = [[[self imageViewClass] alloc] init]; + bannerImageView.contentMode = UIViewContentModeScaleAspectFit; + bannerImageView.translatesAutoresizingMaskIntoConstraints = NO; + _bannerImageView = bannerImageView; + return bannerImageView; +} + +#pragma mark - SetUp + +- (void)setUpUI { + [super setUpUI]; + + // Views + [self.rootView addSubview:self.bannerImageView]; + [self.rootView bringSubviewToFront:self.pinImageView]; + [self.rootView bringSubviewToFront:self.unviewedLineView]; + + // AutoLayout + self.imageRatioConstraint = [self.bannerImageView.heightAnchor constraintEqualToAnchor:self.bannerImageView.widthAnchor]; + self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; + + NSArray *constraints = @[ + [self.bannerImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], + [self.bannerImageView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor], + [self.bannerImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], + [self.bannerImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], + self.imageRatioConstraint + ]; + [NSLayoutConstraint activateConstraints:constraints]; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKBannerContentCard *)card { + if (![card isKindOfClass:[ABKBannerContentCard class]]) { + return; + } + + [super applyCard:card]; + [self updateImageConstraintIfNeededWithAspectRatio:card.imageAspectRatio]; + + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + typeof(self) __weak weakSelf = self; + [[Appboy sharedInstance].imageDelegate setImageForView:self.bannerImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:card.image] + imagePlaceHolder:[self getPlaceHolderImage] + completed:^(UIImage * _Nullable image, + NSError * _Nullable error, + NSInteger cacheType, + NSURL * _Nullable imageURL) { + dispatch_async(dispatch_get_main_queue(), ^{ + typeof(self) __strong strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + UIImage *finalImage = image != nil ? image : [strongSelf getPlaceHolderImage]; + strongSelf.bannerImageView.image = finalImage; + + CGFloat aspectRatio = finalImage.size.width / finalImage.size.height; + card.imageAspectRatio = aspectRatio; + [strongSelf updateImageConstraintIfNeededWithAspectRatio:aspectRatio]; + }); + }]; +} + +- (void)updateImageConstraintIfNeededWithAspectRatio:(CGFloat)aspectRatio { + if (aspectRatio == 0 || ABK_CGFLT_EQ(self.imageRatioConstraint.multiplier, 1 / aspectRatio)) { + return; + } + + self.imageRatioConstraint.active = NO; + self.imageRatioConstraint = [self.bannerImageView.heightAnchor constraintEqualToAnchor:self.bannerImageView.widthAnchor + multiplier:1 / aspectRatio]; + self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; + self.imageRatioConstraint.active = YES; + [self.delegate cellRequestSizeUpdate:self]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.h new file mode 100644 index 0000000000..95179bf44b --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.h @@ -0,0 +1,93 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKContentCard; + +@protocol ABKBaseContentCardCellDelegate + +- (void)cellRequestSizeUpdate:(UITableViewCell *)cell; + +@end + +@interface ABKBaseContentCardCell : UITableViewCell + +/*! + * This view displays the card contents and is the base view container for each card. To change or + * configure the outline of the card like card width, background color board width, etc, you can + * update this property accordingly. + */ +@property (nonatomic) IBOutlet UIView *rootView; + +/*! + * This is the triangle image which shows if a card has been viewed by the user. + */ +@property (nonatomic) IBOutlet UIImageView *pinImageView; + +/*! + * This is the blue line under unviewed cards. + */ +@property (nonatomic) IBOutlet UIView *unviewedLineView; +@property (nonatomic) UIColor *unviewedLineViewColor; + +/*! + * Card root view related constraints + */ +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewLeadingConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTrailingConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTopConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewBottomConstraint; + +@property (nonatomic) IBOutlet NSLayoutConstraint *cardWidthConstraint; + +/*! + * These are basic UI configuration for the Content Cards feed. They are set to the default values in the + * `setUp` method. + * + * It's recommended to set the values before the view is displayed. + */ +@property (nonatomic, assign) CGFloat cardSidePadding; +@property (nonatomic, assign) CGFloat cardSpacing; +@property (nonatomic, assign) BOOL hideUnreadIndicator; + +/*! + * To communicate back after any cell updates occur + */ +@property (weak, nonatomic) id delegate; + +/*! + * @param card The card model for the cell. + * + * @discussion Apply the data from the given card to the card cell. + */ +- (void)applyCard:(ABKContentCard *)card; + +/*! + * @discussion This is a utility method to return the place holder image. + */ +- (UIImage *)getPlaceHolderImage; + +- (Class)imageViewClass; + +/*! + * @discussion initialization that always occurs for the content card cells + */ +- (void)setUp; + +/*! + * @discussion Initialization that is in place of Storyboard or XIB initialization. + * This method should call all the property specific setUp methods. + */ +- (void)setUpUI; + +/*! + * @discussion This is a utility method to make text styled. + */ +- (void)applyAppboyAttributedTextStyleFrom:(NSString *)text forLabel:(UILabel *)label; + +@end + +static const UILayoutPriority ABKContentCardPriorityLayoutRequiredBelowAppleRequired = UILayoutPriorityRequired - 1; + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.m new file mode 100644 index 0000000000..8aed46c2a1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.m @@ -0,0 +1,234 @@ +#import "ABKBaseContentCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static CGFloat AppboyCardSidePadding = 10.0; +static CGFloat AppboyCardSpacing = 32.0; +static CGFloat AppboyCardBorderWidth = 0.5; +static CGFloat AppboyCardCornerRadius = 3.0; +static CGFloat AppboyCardShadowXOffset = 0.0; +static CGFloat AppboyCardShadowYOffset = -2.0; +static CGFloat AppboyCardShadowOpacity = 0.5; +static CGFloat AppboyCardLineSpacing = 1.2; + +@implementation ABKBaseContentCardCell + +#pragma mark - Properties + +- (UIView *)rootView { + if (_rootView != nil) { + return _rootView; + } + + // View + UIView *rootView = [[UIView alloc] init]; + rootView.translatesAutoresizingMaskIntoConstraints = NO; + if (@available(iOS 13.0, *)) { + rootView.backgroundColor = [UIColor systemBackgroundColor]; + } else { + rootView.backgroundColor = [UIColor whiteColor]; + } + + // - Border + UIColor *lightBorderColor = [UIColor colorWithWhite:(224.0 / 255.0) alpha:1.0]; + UIColor *darkBorderColor = [UIColor colorWithWhite:(85.0 / 255.0) alpha:1.0]; + + CALayer *rootLayer = rootView.layer; + rootLayer.masksToBounds = YES; + rootLayer.cornerRadius = AppboyCardCornerRadius; + rootLayer.borderWidth = AppboyCardBorderWidth; + rootLayer.borderColor = [ABKUIUtils dynamicColorForLightColor:lightBorderColor + darkColor:darkBorderColor].CGColor; + + // - Shadow + UIColor *shadowColor = [UIColor colorWithWhite:(178.0 / 255.0) alpha:1.0]; + rootLayer.shadowColor = shadowColor.CGColor; + rootLayer.shadowOffset = CGSizeMake(AppboyCardShadowXOffset, AppboyCardShadowYOffset); + rootLayer.shadowOpacity = AppboyCardShadowOpacity; + + _rootView = rootView; + return rootView; +} + +- (UIImageView *)pinImageView { + if (_pinImageView != nil) { + return _pinImageView; + } + + NSBundle *bundle = [ABKUIUtils bundle:[ABKBaseContentCardCell class] + channel:ABKContentCardChannel]; + UIImage *pinImage = [UIImage imageNamed:@"appboy_cc_icon_pinned" + inBundle:bundle + compatibleWithTraitCollection:nil]; + pinImage = [pinImage imageFlippedForRightToLeftLayoutDirection]; + + UIImageView *pinImageView = [[UIImageView alloc] initWithImage:pinImage]; + pinImageView.contentMode = UIViewContentModeScaleToFill; + pinImageView.translatesAutoresizingMaskIntoConstraints = NO; + _pinImageView = pinImageView; + return pinImageView; +} + +- (UIView *)unviewedLineView { + if (_unviewedLineView != nil) { + return _unviewedLineView; + } + + UIView *unviewedLineView = [[UIView alloc] init]; + unviewedLineView.backgroundColor = self.unviewedLineViewColor; + unviewedLineView.translatesAutoresizingMaskIntoConstraints = NO; + _unviewedLineView = unviewedLineView; + return unviewedLineView; +} + +#pragma mark - Initialization + +- (instancetype)initWithStyle:(UITableViewCellStyle)style + reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + [self setUp]; + [self setUpUI]; + } + + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self setUp]; + } + return self; +} + +#pragma mark - SetUp + +- (void)setUp { + self.backgroundColor = [UIColor clearColor]; + self.contentView.backgroundColor = [UIColor clearColor]; + self.selectionStyle = UITableViewCellSelectionStyleNone; + + self.unviewedLineViewColor = self.tintColor; + + self.cardSidePadding = AppboyCardSidePadding; + self.cardSpacing = AppboyCardSpacing; +} + +- (void)setUpUI { + // View Hierarchy + [self.contentView addSubview:self.rootView]; + [self.rootView addSubview:self.pinImageView]; + [self.rootView addSubview:self.unviewedLineView]; + + // AutoLayout + // - Root + self.rootViewLeadingConstraint = [self.rootView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor + constant:self.cardSidePadding]; + self.rootViewTrailingConstraint = [self.rootView.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor + constant:-self.cardSidePadding]; + self.rootViewTopConstraint = [self.rootView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor + constant:self.cardSidePadding]; + self.rootViewBottomConstraint = [self.rootView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor + constant:-self.cardSidePadding]; + self.cardWidthConstraint = [self.rootView.widthAnchor constraintLessThanOrEqualToConstant:380]; + self.rootViewLeadingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; + self.rootViewTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; + + // - All constraints + NSArray *constraints = @[ + // Root view + self.rootViewLeadingConstraint, + self.rootViewTrailingConstraint, + self.rootViewTopConstraint, + self.rootViewBottomConstraint, + self.cardWidthConstraint, + [self.rootView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor], + // PinImage + [self.pinImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], + [self.pinImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], + [self.pinImageView.widthAnchor constraintEqualToConstant:20], + [self.pinImageView.heightAnchor constraintEqualToConstant:20], + // UnviewedLine + [self.unviewedLineView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], + [self.unviewedLineView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], + [self.unviewedLineView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor], + [self.unviewedLineView.heightAnchor constraintEqualToConstant:8] + ]; + [NSLayoutConstraint activateConstraints:constraints]; +} + +# pragma mark - Cell UI Configuration + +- (void)setUnviewedLineViewColor:(UIColor*)bgColor { + _unviewedLineViewColor = bgColor; + if (self.unviewedLineView) { + self.unviewedLineView.backgroundColor = self.unviewedLineViewColor; + } +} + +- (void)setHideUnreadIndicator:(BOOL)hideUnreadIndicator { + if (_hideUnreadIndicator != hideUnreadIndicator) { + _hideUnreadIndicator = hideUnreadIndicator; + self.unviewedLineView.hidden = hideUnreadIndicator; + } +} + +- (void)setCardSidePadding:(CGFloat)sidePadding { + _cardSidePadding = sidePadding; + if (self.rootViewLeadingConstraint && self.rootViewTrailingConstraint) { + self.rootViewLeadingConstraint.constant = self.cardSidePadding; + self.rootViewTrailingConstraint.constant = self.cardSidePadding; + } +} + +- (void)setCardSpacing:(CGFloat)spacing { + _cardSpacing = spacing; + if (self.rootViewTopConstraint && self.rootViewBottomConstraint) { + self.rootViewTopConstraint.constant = self.cardSpacing / 2.0; + self.rootViewBottomConstraint.constant = self.cardSpacing / 2.0; + } +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKContentCard *)card { + if ([card isControlCard]) { + self.pinImageView.hidden = YES; + self.unviewedLineView.hidden = YES; + return; + } + + self.unviewedLineView.hidden = self.hideUnreadIndicator || card.viewed; + self.pinImageView.hidden = !card.pinned; +} + +#pragma mark - Utiliy Methods + +- (UIImage *)getPlaceHolderImage { + return [ABKUIUtils imageNamed:@"appboy_cc_noimage_lrg" + bundle:[ABKBaseContentCardCell class] + channel:ABKContentCardChannel]; +} + +- (Class)imageViewClass { + if ([Appboy sharedInstance].imageDelegate) { + return [[Appboy sharedInstance].imageDelegate imageViewClass]; + } + return [UIImageView class]; +} + +- (void)applyAppboyAttributedTextStyleFrom:(NSString *)text forLabel:(UILabel *)label { + UIColor *color = label.textColor; + NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init]; + paragraphStyle.lineSpacing = AppboyCardLineSpacing; + UIFont *font = label.font; + NSDictionary *attributes = @{NSFontAttributeName: font, + NSForegroundColorAttributeName: color, + NSParagraphStyleAttributeName: paragraphStyle}; + // Convert to empty string to fail gracefully if given null from backend + text = text ?: @""; + label.attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.h new file mode 100644 index 0000000000..ff5f0db5be --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.h @@ -0,0 +1,26 @@ +#import "ABKBaseContentCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKCaptionedImageContentCard; + +@interface ABKCaptionedImageContentCardCell : ABKBaseContentCardCell + +@property (class, nonatomic) UIColor *titleLabelColor; +@property (class, nonatomic) UIColor *descriptionLabelColor; +@property (class, nonatomic) UIColor *linkLabelColor; + +@property (strong, nonatomic) IBOutlet UIImageView *captionedImageView; +@property (strong, nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; +@property (strong, nonatomic) IBOutlet UILabel *titleLabel; +@property (strong, nonatomic) IBOutlet UILabel *descriptionLabel; +@property (strong, nonatomic) IBOutlet UILabel *linkLabel; + +@property (nonatomic, assign) CGFloat padding; + +- (void)applyCard:(ABKCaptionedImageContentCard *)captionedImageCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.m new file mode 100644 index 0000000000..31cb9e8248 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.m @@ -0,0 +1,269 @@ +#import "ABKCaptionedImageContentCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@interface ABKCaptionedImageContentCardCell () + +@property (strong, nonatomic) NSArray *descriptionConstraints; +@property (strong, nonatomic) NSArray *linkConstraints; + +@end + + +@implementation ABKCaptionedImageContentCardCell + +static UIColor *_titleLabelColor = nil; +static UIColor *_descriptionLabelColor = nil; +static UIColor *_linkLabelColor = nil; + ++ (UIColor *)titleLabelColor { + if (_titleLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _titleLabelColor = [UIColor labelColor]; + } else { + _titleLabelColor = [UIColor blackColor]; + } + } + return _titleLabelColor; +} + ++ (void)setTitleLabelColor:(UIColor *)titleLabelColor { + _titleLabelColor = titleLabelColor; +} + ++ (UIColor *)descriptionLabelColor { + if (_descriptionLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _descriptionLabelColor = [UIColor labelColor]; + } else { + _descriptionLabelColor = [UIColor blackColor]; + } + } + return _descriptionLabelColor; +} + ++ (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { + _descriptionLabelColor = descriptionLabelColor; +} + ++ (UIColor *)linkLabelColor { + if (_linkLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _linkLabelColor = [UIColor linkColor]; + } else { + _linkLabelColor = [UIColor systemBlueColor]; + } + } + return _linkLabelColor; +} + ++ (void)setLinkLabelColor:(UIColor *)linkLabelColor{ + _linkLabelColor = linkLabelColor; +} + +#pragma mark - Properties + +- (UIImageView *)captionedImageView { + if (_captionedImageView != nil) { + return _captionedImageView; + } + + UIImageView *captionedImageView = [[[self imageViewClass] alloc] init]; + captionedImageView.contentMode = UIViewContentModeScaleAspectFit; + captionedImageView.translatesAutoresizingMaskIntoConstraints = NO; + _captionedImageView = captionedImageView; + return captionedImageView; +} + +- (UILabel *)titleLabel { + if (_titleLabel != nil) { + return _titleLabel; + } + + UILabel *titleLabel = [[UILabel alloc] init]; + titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleCallout weight:UIFontWeightBold]; + titleLabel.textColor = [self class].titleLabelColor; + titleLabel.text = @"Title"; + titleLabel.numberOfLines = 0; + titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + _titleLabel = titleLabel; + return titleLabel; +} + +- (UILabel *)descriptionLabel { + if (_descriptionLabel != nil) { + return _descriptionLabel; + } + + UILabel *descriptionLabel = [[UILabel alloc] init]; + descriptionLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightRegular]; + descriptionLabel.textColor = [self class].descriptionLabelColor; + descriptionLabel.text = @"Description"; + descriptionLabel.numberOfLines = 0; + descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; + _descriptionLabel = descriptionLabel; + return descriptionLabel; +} + +- (UILabel *)linkLabel { + if (_linkLabel != nil) { + return _linkLabel; + } + + UILabel *linkLabel = [[UILabel alloc] init]; + linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightMedium]; + linkLabel.textColor = [self class].linkLabelColor; + linkLabel.text = @"Link"; + linkLabel.numberOfLines = 0; + linkLabel.lineBreakMode = NSLineBreakByCharWrapping; + linkLabel.translatesAutoresizingMaskIntoConstraints = NO; + _linkLabel = linkLabel; + return linkLabel; +} + +#pragma mark - SetUp + +- (void)setUp { + [super setUp]; + self.padding = 25; +} + +- (void)setUpUI { + [super setUpUI]; + + // Views + [self.rootView addSubview:self.captionedImageView]; + [self.rootView addSubview:self.titleLabel]; + [self.rootView addSubview:self.descriptionLabel]; + [self.rootView addSubview:self.linkLabel]; + + // - Remove / add pinImageView to reset it + [self.pinImageView removeFromSuperview]; + [self.rootView addSubview:self.pinImageView]; + + // AutoLayout + + self.imageRatioConstraint = [self.captionedImageView.heightAnchor constraintEqualToAnchor:self.captionedImageView.widthAnchor]; + self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; + + NSArray *constraints = @[ + // Captioned Image + [self.captionedImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor], + [self.captionedImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor], + [self.captionedImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], + self.imageRatioConstraint, + + // Pin Image + [self.pinImageView.topAnchor constraintEqualToAnchor:self.captionedImageView.topAnchor], + [self.pinImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor], + [self.pinImageView.widthAnchor constraintEqualToConstant:20], + [self.pinImageView.heightAnchor constraintEqualToConstant:20], + + // Title + [self.titleLabel.topAnchor constraintEqualToAnchor:self.captionedImageView.bottomAnchor + constant:17], + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor + constant:self.padding], + [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor + constant:-self.padding], + + // Description + [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor + constant:6], + [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], + + // Link + [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] + ]; + [NSLayoutConstraint activateConstraints:constraints]; + + self.descriptionConstraints = @[ + [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; + + self.linkConstraints = @[ + [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor + constant:8], + [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKCaptionedImageContentCard *)card { + if (![card isKindOfClass:[ABKCaptionedImageContentCard class]]) { + return; + } + + [super applyCard:card]; + [self applyAppboyAttributedTextStyleFrom:card.title forLabel:self.titleLabel]; + [self applyAppboyAttributedTextStyleFrom:card.cardDescription forLabel:self.descriptionLabel]; + [self applyAppboyAttributedTextStyleFrom:card.domain forLabel:self.linkLabel]; + self.linkLabel.hidden = card.domain.length == 0; + + [self updateConstraintsForCard:card]; + [self updateImageConstraintIfNeededWithAspectRatio:card.imageAspectRatio]; + + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + typeof(self) __weak weakSelf = self; + [[Appboy sharedInstance].imageDelegate setImageForView:self.captionedImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:card.image] + imagePlaceHolder:[self getPlaceHolderImage] + completed:^(UIImage * _Nullable image, + NSError * _Nullable error, + NSInteger cacheType, + NSURL * _Nullable imageURL) { + dispatch_async(dispatch_get_main_queue(), ^{ + typeof(self) __strong strongSelf = weakSelf; + if (strongSelf == nil) { + return; + } + + if (image == nil) { + strongSelf.captionedImageView.image = [strongSelf getPlaceHolderImage]; + return; + } + + CGFloat aspectRatio = image.size.width / image.size.height; + card.imageAspectRatio = aspectRatio; + [strongSelf updateImageConstraintIfNeededWithAspectRatio:aspectRatio]; + }); + }]; +} + +- (void)updateConstraintsForCard:(ABKCaptionedImageContentCard *)card { + if (card.domain.length == 0) { + [NSLayoutConstraint deactivateConstraints:self.linkConstraints]; + [NSLayoutConstraint activateConstraints:self.descriptionConstraints]; + } else { + [NSLayoutConstraint deactivateConstraints:self.descriptionConstraints]; + [NSLayoutConstraint activateConstraints:self.linkConstraints]; + } +} + +- (void)updateImageConstraintIfNeededWithAspectRatio:(CGFloat)aspectRatio { + if (aspectRatio == 0 || ABK_CGFLT_EQ(self.imageRatioConstraint.multiplier, 1 / aspectRatio)) { + return; + } + + self.imageRatioConstraint.active = NO; + self.imageRatioConstraint = [self.captionedImageView.heightAnchor constraintEqualToAnchor:self.captionedImageView.widthAnchor + multiplier:1 / aspectRatio]; + self.imageRatioConstraint.priority = UILayoutPriorityDefaultHigh; + self.imageRatioConstraint.active = YES; + [self.delegate cellRequestSizeUpdate:self]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.h new file mode 100644 index 0000000000..ce150acc38 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.h @@ -0,0 +1,27 @@ +#import "ABKBaseContentCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKClassicContentCard; + +@interface ABKClassicContentCardCell : ABKBaseContentCardCell + +@property (class, nonatomic) UIColor *titleLabelColor; +@property (class, nonatomic) UIColor *descriptionLabelColor; +@property (class, nonatomic) UIColor *linkLabelColor; + +@property (strong, nonatomic) IBOutlet UILabel *titleLabel; +@property (strong, nonatomic) IBOutlet UILabel *descriptionLabel; +@property (strong, nonatomic) IBOutlet UILabel *linkLabel; + +@property (strong, nonatomic) NSArray *descriptionConstraints; +@property (strong, nonatomic) NSArray *linkConstraints; + +@property (nonatomic, assign) CGFloat padding; + +- (void)applyCard:(ABKClassicContentCard *)classicCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.m new file mode 100644 index 0000000000..23e7329236 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.m @@ -0,0 +1,190 @@ +#import "ABKClassicContentCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@implementation ABKClassicContentCardCell + +static UIColor *_titleLabelColor = nil; +static UIColor *_descriptionLabelColor = nil; +static UIColor *_linkLabelColor = nil; + ++ (UIColor *)titleLabelColor { + if (_titleLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _titleLabelColor = [UIColor labelColor]; + } else { + _titleLabelColor = [UIColor blackColor]; + } + } + return _titleLabelColor; +} + ++ (void)setTitleLabelColor:(UIColor *)titleLabelColor { + _titleLabelColor = titleLabelColor; +} + ++ (UIColor *)descriptionLabelColor { + if (_descriptionLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _descriptionLabelColor = [UIColor labelColor]; + } else { + _descriptionLabelColor = [UIColor blackColor]; + } + } + return _descriptionLabelColor; +} + ++ (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { + _descriptionLabelColor = descriptionLabelColor; +} + ++ (UIColor *)linkLabelColor { + if (_linkLabelColor == nil) { + if (@available(iOS 13.0, *)) { + _linkLabelColor = [UIColor linkColor]; + } else { + _linkLabelColor = [UIColor systemBlueColor]; + } + } + return _linkLabelColor; +} + ++ (void)setLinkLabelColor:(UIColor *)linkLabelColor{ + _linkLabelColor = linkLabelColor; +} + +#pragma mark - Properties + +- (UILabel *)titleLabel { + if (_titleLabel != nil) { + return _titleLabel; + } + + UILabel *titleLabel = [[UILabel alloc] init]; + titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleCallout weight:UIFontWeightBold]; + titleLabel.textColor = [self class].titleLabelColor; + titleLabel.text = @"Title"; + titleLabel.numberOfLines = 0; + titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + _titleLabel = titleLabel; + return titleLabel; +} + +- (UILabel *)descriptionLabel { + if (_descriptionLabel != nil) { + return _descriptionLabel; + } + + UILabel *descriptionLabel = [[UILabel alloc] init]; + descriptionLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightRegular]; + descriptionLabel.textColor = [self class].descriptionLabelColor; + descriptionLabel.text = @"Description"; + descriptionLabel.numberOfLines = 0; + descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; + descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; + _descriptionLabel = descriptionLabel; + return descriptionLabel; +} + +- (UILabel *)linkLabel { + if (_linkLabel != nil) { + return _linkLabel; + } + + UILabel *linkLabel = [[UILabel alloc] init]; + linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleFootnote weight:UIFontWeightMedium]; + linkLabel.textColor = [self class].linkLabelColor; + linkLabel.text = @"Link"; + linkLabel.numberOfLines = 0; + linkLabel.lineBreakMode = NSLineBreakByCharWrapping; + linkLabel.translatesAutoresizingMaskIntoConstraints = NO; + _linkLabel = linkLabel; + return linkLabel; +} + +#pragma mark - SetUp + +- (void)setUp { + [super setUp]; + self.padding = 25; +} + +- (void)setUpUI { + [super setUpUI]; + + // Views + [self.rootView addSubview:self.titleLabel]; + [self.rootView addSubview:self.descriptionLabel]; + [self.rootView addSubview:self.linkLabel]; + + NSLayoutConstraint *titleTrailingConstraint = [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor + constant:-self.padding]; + titleTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; + + // AutoLayout + NSArray *constraints = @[ + // Title + // - Top + [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor + constant:17], + // - Horizontal + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor + constant:self.padding], + titleTrailingConstraint, + // Description + // - Top + [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor + constant:6], + // - Horizontal + [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], + // Link + // - Horizontal + [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] + ]; + [NSLayoutConstraint activateConstraints:constraints]; + + self.descriptionConstraints = @[ + [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; + + self.linkConstraints = @[ + [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor + constant:8], + [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKClassicContentCard *)card { + if (![card isKindOfClass:[ABKClassicContentCard class]]) { + return; + } + + [super applyCard:card]; + + [self applyAppboyAttributedTextStyleFrom:card.title forLabel:self.titleLabel]; + [self applyAppboyAttributedTextStyleFrom:card.cardDescription forLabel:self.descriptionLabel]; + [self applyAppboyAttributedTextStyleFrom:card.domain forLabel:self.linkLabel]; + self.linkLabel.hidden = card.domain.length == 0; + + [self updateConstraintsForCard:card]; +} + +- (void)updateConstraintsForCard:(ABKClassicContentCard *)card { + if (card.domain.length == 0) { + [NSLayoutConstraint deactivateConstraints:self.linkConstraints]; + [NSLayoutConstraint activateConstraints:self.descriptionConstraints]; + } else { + [NSLayoutConstraint deactivateConstraints:self.descriptionConstraints]; + [NSLayoutConstraint activateConstraints:self.linkConstraints]; + } +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.h new file mode 100644 index 0000000000..14ecd1bfe7 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.h @@ -0,0 +1,21 @@ +#import "ABKClassicContentCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKClassicContentCard; + +/*! + * The ABKClassicContentCard has an optional image property. + * Use this view controller for a classic card with an image and ABKClassicContentCardCell for a + * classic card without an image. + */ +@interface ABKClassicImageContentCardCell : ABKClassicContentCardCell + +@property (strong, nonatomic) IBOutlet UIImageView *classicImageView; + +- (void)applyCard:(ABKClassicContentCard *)classicCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.m new file mode 100644 index 0000000000..ff6d4ab255 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.m @@ -0,0 +1,104 @@ +#import "ABKClassicImageContentCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@implementation ABKClassicImageContentCardCell + +#pragma mark - Properties + +- (UIImageView *)classicImageView { + if (_classicImageView != nil) { + return _classicImageView; + } + + UIImageView *classicImageView = [[[self imageViewClass] alloc] init]; + classicImageView.contentMode = UIViewContentModeScaleAspectFit; + classicImageView.translatesAutoresizingMaskIntoConstraints = NO; + classicImageView.clipsToBounds = YES; + _classicImageView = classicImageView; + return classicImageView; +} + +#pragma mark - SetUp + +- (void)setUpUI { + [super setUpUI]; + + // Reset + [self.titleLabel removeFromSuperview]; + [self.descriptionLabel removeFromSuperview]; + [self.linkLabel removeFromSuperview]; + + // Views + [self.rootView addSubview:self.classicImageView]; + [self.rootView addSubview:self.titleLabel]; + [self.rootView addSubview:self.descriptionLabel]; + [self.rootView addSubview:self.linkLabel]; + + NSLayoutConstraint *titleTrailingConstraint = [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor + constant:-self.padding]; + titleTrailingConstraint.priority = ABKContentCardPriorityLayoutRequiredBelowAppleRequired; + + // AutoLayout + NSArray *constraints = @[ + // ClassicImage + [self.classicImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor + constant:17], + [self.classicImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor + constant:self.padding], + [self.classicImageView.heightAnchor constraintEqualToConstant:57.5], + [self.classicImageView.widthAnchor constraintEqualToConstant:57.5], + // Title + [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor + constant:17], + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.classicImageView.trailingAnchor constant:12], + titleTrailingConstraint, + // Description + // - Top + [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor + constant:6], + // - Horizontal + [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor], + // Link + // - Horizontal + [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor], + [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor] + ]; + [NSLayoutConstraint activateConstraints:constraints]; + + self.descriptionConstraints = @[ + [self.descriptionLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; + + self.linkConstraints = @[ + [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor + constant:8], + [self.linkLabel.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor + constant:-self.padding] + ]; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKClassicContentCard *)card { + if (![card isKindOfClass:[ABKClassicContentCard class]]) { + return; + } + [super applyCard:card]; + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + [[Appboy sharedInstance].imageDelegate setImageForView:self.classicImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:card.image] + imagePlaceHolder:[self getPlaceHolderImage] + completed:nil]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.h b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.h new file mode 100644 index 0000000000..7f2ee7d233 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.h @@ -0,0 +1,10 @@ +#import +#import "ABKBaseContentCardCell.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ABKControlTableViewCell : ABKBaseContentCardCell + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.m b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.m new file mode 100644 index 0000000000..3443a4f974 --- /dev/null +++ b/Sources/BrazeUICompat/ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.m @@ -0,0 +1,5 @@ +#import "ABKControlTableViewCell.h" + +@implementation ABKControlTableViewCell + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.h b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.h new file mode 100644 index 0000000000..857106f183 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.h @@ -0,0 +1,19 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKInAppMessageButton; + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageUIButton : UIButton + +/*! + * The model object for the UIButton. + */ +@property ABKInAppMessageButton *inAppButtonModel; + +@end +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.m b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.m new file mode 100644 index 0000000000..0c3495ce81 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIButton.m @@ -0,0 +1,90 @@ +#import "ABKInAppMessageUIButton.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +#define DefaultTitleSize UIFontTextStyleSubheadline +static CGFloat const ButtonCornerRadius = 5.0f; +static CGFloat const ButtonTitleSidePadding = 12.0; + +@interface ABKInAppMessageUIButton () + +@property (copy) UIColor *originalBackgroundColor; + +@end + +@implementation ABKInAppMessageUIButton + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self setUp]; + } + return self; +} + +- (instancetype)init { + if (self = [super init]) { + [self setUp]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self setUp]; + } + return self; +} + +- (void)setUp { + self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:DefaultTitleSize weight:UIFontWeightBold]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; + self.titleLabel.textAlignment = NSTextAlignmentCenter; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + self.originalBackgroundColor = self.backgroundColor; +} + +- (void)layoutSubviews { + [super layoutSubviews]; + + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonTextFont]) { + self.titleLabel.font = self.inAppButtonModel.buttonTextFont; + } + + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonTextColor]) { + [self setTitleColor:self.inAppButtonModel.buttonTextColor forState:UIControlStateNormal]; + } + + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonText]) { + [self setTitle:self.inAppButtonModel.buttonText forState:UIControlStateNormal]; + } + + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBackgroundColor]) { + self.backgroundColor = self.inAppButtonModel.buttonBackgroundColor; + } + + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBorderColor]) { + self.layer.borderColor = [self.inAppButtonModel.buttonBorderColor CGColor]; + } else if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppButtonModel.buttonBackgroundColor]) { + self.layer.borderColor = [self.inAppButtonModel.buttonBackgroundColor CGColor]; + } else { + self.layer.borderColor = [[UIColor colorWithRed:(27.0/255.0) green:(120.0/255.0) blue:(207.0)/(255.0) alpha:1.0] CGColor]; + } + + self.layer.cornerRadius = ButtonCornerRadius; + self.titleLabel.frame = CGRectMake(ButtonTitleSidePadding, 0, + self.bounds.size.width - ButtonTitleSidePadding * 2, self.bounds.size.height); +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + + if (highlighted) { + [self setBackgroundColor:[UIColor colorWithRed:0 green:0 blue:0 alpha:.08]]; + } else { + self.backgroundColor = self.originalBackgroundColor; + [self setNeedsLayout]; + } +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.h b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.h new file mode 100644 index 0000000000..36e2059993 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.h @@ -0,0 +1,50 @@ +#import +#import "ABKInAppMessageUIDelegate.h" +#import "ABKInAppMessageWindowController.h" + +@import BrazeKit; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@interface ABKInAppMessageUIController : NSObject + +/*! + * supportedOrientationMask allows you to change which orientation mask the in-app message supports. + * In-app messages will normally support the orientations specified in the app settings, but the method + * supportedInterfaceOrientations may optionally override that. The value of supportedOrientationMask will be returned + * in supportedInterfaceOrientations in the in-app message view controller. + * + * The default value of supportedOrientationMask is UIInterfaceOrientationMaskAll. + */ +@property UIInterfaceOrientationMask supportedOrientationMask; + +/*! + * preferredOrientation allows you to select which orientation should be preferred if multiple orientations are supported by the view controller. + * If set to a value other than UIInterfaceOrientationUnknown, the value of preferredOrientation will be returned by + * preferredInterfaceOrientationForPresentation in the in-app message view controller. + * Otherwise, the current status bar orientation will be returned. + * + * The default value of preferredOrientation is UIInterfaceOrientationUnknown, which means status bar orientation should be set + * for in-app message orientation. + */ +@property UIInterfaceOrientation preferredOrientation; + +/*! + * keyboardVisible will have the value YES when the keyboard is shown. + */ +@property BOOL keyboardVisible; + +/*! + * The ABKInAppMessageWindowController that is being shown. + */ +@property (nullable) ABKInAppMessageWindowController *inAppMessageWindowController; + +/*! + * The optional ABKInAppMessageUIDelegate that can be used to specify the UI behaviors of in-app messages. + */ +@property (weak, nullable) id uiDelegate; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.m b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.m new file mode 100644 index 0000000000..480ec8834b --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIController.m @@ -0,0 +1,284 @@ +#import "ABKInAppMessageUIController.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKUIUtils.h" +#import "ABKInAppMessageSlideupViewController.h" +#import "ABKInAppMessageModalViewController.h" +#import "ABKInAppMessageHTMLFullViewController.h" +#import "ABKInAppMessageHTMLViewController.h" +#import "ABKInAppMessageFullViewController.h" +#import "ABKSDWebImageImageDelegate.h" +#import "SDWebImage/SDWebImage.h" + +@import BrazeKitCompat; + +@interface ABKInAppMessageUIController () + +@property (strong, nonatomic) NSMutableArray *inAppMessageStack; + +- (void)handleExistingInAppMessagesInStack; + +@end + +@implementation ABKInAppMessageUIController + +- (instancetype)init { + if (self = [super init]) { + _supportedOrientationMask = UIInterfaceOrientationMaskAll; + _preferredOrientation = UIInterfaceOrientationUnknown; + _inAppMessageStack = [NSMutableArray array]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveKeyboardWasShownNotification:) + name:UIKeyboardWillShowNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(receiveKeyboardDidHideNotification:) + name:UIKeyboardDidHideNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(inAppMessageWindowDismissed:) + name:ABKNotificationInAppMessageWindowDismissed + object:nil]; + } + return self; +} + +#pragma mark - Show and Hide In-app Message + +- (void)showInAppMessage:(ABKInAppMessage *)inAppMessage { + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad) { + // Check the device orientation before displaying the in-app message + UIInterfaceOrientation statusBarOrientation = [ABKUIUtils getInterfaceOrientation]; + NSString *errorMessage = @"The in-app message %@ with %@ orientation shouldn't be displayed in %@, disregarding this in-app message."; + if (inAppMessage.orientation == ABKInAppMessageOrientationPortrait && + !UIInterfaceOrientationIsPortrait(statusBarOrientation)) { + NSLog(errorMessage, inAppMessage, @"portrait", @"landscape"); + return; + } + if (inAppMessage.orientation == ABKInAppMessageOrientationLandscape && + !UIInterfaceOrientationIsLandscape(statusBarOrientation)) { + NSLog(errorMessage, inAppMessage, @"landscape", @"portrait"); + return; + } + } + + if ([inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { + ABKInAppMessageImmersive *immersiveInAppMessage = (ABKInAppMessageImmersive *)inAppMessage; + if (immersiveInAppMessage.imageStyle == ABKInAppMessageGraphic && + ![ABKUIUtils objectIsValidAndNotEmpty:immersiveInAppMessage.imageURI]) { + NSLog(@"The in-app message has graphic image style but no image, discard this in-app message."); + return; + } + if ([immersiveInAppMessage isKindOfClass:[ABKInAppMessageFull class]] && + ![ABKUIUtils objectIsValidAndNotEmpty:immersiveInAppMessage.imageURI]) { + NSLog(@"The in-app message is a full in-app message without an image, discard this in-app message."); + return; + } + } + + if (inAppMessage.inAppMessageClickActionType == ABKInAppMessageNoneClickAction && + [inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { + ((ABKInAppMessageSlideup *)inAppMessage).hideChevron = YES; + } + + ABKInAppMessageViewController *inAppMessageViewController = nil; + if ([self.uiDelegate respondsToSelector:@selector(inAppMessageViewControllerWithInAppMessage:)]) { + inAppMessageViewController = [self.uiDelegate inAppMessageViewControllerWithInAppMessage:inAppMessage]; + } else { + if ([inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { + inAppMessageViewController = [[ABKInAppMessageSlideupViewController alloc] + initWithInAppMessage:inAppMessage]; + } else if ([inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { + inAppMessageViewController = [[ABKInAppMessageModalViewController alloc] + initWithInAppMessage:inAppMessage]; + } else if ([inAppMessage isKindOfClass:[ABKInAppMessageFull class]]) { + inAppMessageViewController = [[ABKInAppMessageFullViewController alloc] + initWithInAppMessage:inAppMessage]; + } else if ([inAppMessage isKindOfClass:[ABKInAppMessageHTMLFull class]]) { + inAppMessageViewController = [[ABKInAppMessageHTMLFullViewController alloc] + initWithInAppMessage:inAppMessage]; + } else if ([inAppMessage isKindOfClass:[ABKInAppMessageHTML class]]) { + inAppMessageViewController = [[ABKInAppMessageHTMLViewController alloc] + initWithInAppMessage:inAppMessage]; + } + } + if (inAppMessageViewController) { + ABKInAppMessageWindowController *windowController = [[ABKInAppMessageWindowController alloc] + initWithInAppMessage:inAppMessage + inAppMessageViewController:inAppMessageViewController + inAppMessageDelegate:self.uiDelegate]; + windowController.supportedOrientationMask = self.supportedOrientationMask; + windowController.preferredOrientation = self.preferredOrientation; + self.inAppMessageWindowController = windowController; + if (@available(iOS 13.0, *)) { + inAppMessageViewController.overrideUserInterfaceStyle = inAppMessage.overrideUserInterfaceStyle; + } + [self.inAppMessageWindowController displayInAppMessageViewWithAnimation:inAppMessage.animateIn]; + } +} + +- (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForInAppMessage:(ABKInAppMessage *)inAppMessage { + ABKInAppMessageDisplayChoice inAppMessageDisplayChoice = self.keyboardVisible ? + ABKDisplayInAppMessageLater : ABKDisplayInAppMessageNow; + if (inAppMessageDisplayChoice == ABKDisplayInAppMessageLater) { + NSLog(@"Initially setting in-app message display choice to ABKDisplayInAppMessageLater due to visible keyboard."); + } + if ([self.uiDelegate respondsToSelector:@selector(beforeInAppMessageDisplayed:withKeyboardIsUp:)]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // ignore deprecation warning to support client integrations using the deprecated method + inAppMessageDisplayChoice = [self.uiDelegate beforeInAppMessageDisplayed:inAppMessage + withKeyboardIsUp:self.keyboardVisible]; +#pragma clang diagnostic pop + } else if ([[Appboy sharedInstance].inAppMessageController.delegate + respondsToSelector:@selector(beforeInAppMessageDisplayed:)]) { + inAppMessageDisplayChoice = [[Appboy sharedInstance].inAppMessageController.delegate + beforeInAppMessageDisplayed:inAppMessage]; + } + return inAppMessageDisplayChoice; +} + +- (ABKInAppMessageDisplayChoice)getCurrentDisplayChoiceForControlInAppMessage:(ABKInAppMessage *)controlInAppMessage { + ABKInAppMessageDisplayChoice inAppMessageDisplayChoice = self.keyboardVisible ? ABKDisplayInAppMessageLater : ABKDisplayInAppMessageNow; + if (inAppMessageDisplayChoice == ABKDisplayInAppMessageLater) { + NSLog(@"Initially setting in-app message display choice to ABKDisplayInAppMessageLater due to visible keyboard."); + } + if ([[Appboy sharedInstance].inAppMessageController.delegate + respondsToSelector:@selector(beforeControlMessageImpressionLogged:)]) { + inAppMessageDisplayChoice = [Appboy.sharedInstance.inAppMessageController.delegate beforeControlMessageImpressionLogged:controlInAppMessage]; + } + return inAppMessageDisplayChoice; +} + +- (BOOL)inAppMessageCurrentlyVisible { + if (self.inAppMessageWindowController) { + return YES; + } + return NO; +} + +- (void)hideCurrentInAppMessage:(BOOL)animated { + @try { + if (self.inAppMessageWindowController) { + [self.inAppMessageWindowController hideInAppMessageViewWithAnimation:animated]; + } + } + @catch (NSException *exception) { + NSLog(@"An error occured and this in-app message couldn't be hidden."); + } +} + +- (void)inAppMessageWindowDismissed:(NSNotification *)notification { + // We listen to this notification so that we know when the screen is clear of in-app messages + // and a new in-app message can be shown. + self.inAppMessageWindowController = nil; +} + +#pragma mark - Keyboard + +- (void)receiveKeyboardDidHideNotification:(NSNotification *)notification { + self.keyboardVisible = NO; +} + +- (void)receiveKeyboardWasShownNotification:(NSNotification *)notification { + self.keyboardVisible = YES; + [self.inAppMessageWindowController keyboardWasShown]; +} + +#pragma mark - Set UIDelegate + +- (void)setInAppMessageUIDelegate:(id)uiDelegate { + _uiDelegate = uiDelegate; +} + +#pragma mark - Dealloc + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - BrazeInAppMessagePresenter + +- (void)presentMessage:(BRZInAppMessageRaw * _Nonnull)message { + ABKInAppMessage *abkMessage; + switch (message.type) { + case BRZInAppMessageRawTypeSlideup: + abkMessage = [[ABKInAppMessageSlideup alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeModal: + abkMessage = [[ABKInAppMessageModal alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeFull: + abkMessage = [[ABKInAppMessageFull alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeHtmlFull: + abkMessage = [[ABKInAppMessageHTMLFull alloc] initWithInAppMessage:message]; + break; + case BRZInAppMessageRawTypeHtml: + abkMessage = [[ABKInAppMessageHTML alloc] initWithInAppMessage:message]; + break; + default: + abkMessage = [[ABKInAppMessage alloc] initWithInAppMessage:message]; + break; + } + + // Force loading assets in SDWebImage cache, the assets needs to be directly available otherwise + // the IAM UI doesn't render properly. + SDImageCache *cache = SDImageCache.sharedImageCache; + if (message.imageURL) { + NSString *key = [SDWebImageManager.sharedManager cacheKeyForURL:message.imageURL]; + NSData *imageData = [NSData dataWithContentsOfURL:message.imageURL]; + [cache storeImageDataToDisk:imageData forKey:key]; + } + + [self.inAppMessageStack addObject:abkMessage]; + [self handleExistingInAppMessagesInStack]; +} + +- (void)handleExistingInAppMessagesInStack { + if (self.inAppMessageStack.count == 0) { + NSLog(@"%@: No in-app message found in stack.", NSStringFromSelector(_cmd)); + return; + } + + // We used to only display IAM when the state is active, but sometimes when the + // in-app message comes in early and the state is still inactive, the message won't show. + // As a result, we display the message when the application state is inactive or active. + if ([UIApplication sharedApplication].applicationState == UIApplicationStateBackground) { + // When the application is in the background, we don't want to display an in-app message in any case + NSLog(@"%@: Application state was neither active nor inactive.", NSStringFromSelector(_cmd)); + return; + } + + ABKInAppMessage *inAppMessage = [self.inAppMessageStack lastObject]; + if (inAppMessage) { + [self.inAppMessageStack removeLastObject]; + } + //suppress in-app message if another in-app message is visible if IAM subspec is available + if (self.inAppMessageCurrentlyVisible) { + NSLog(@"Another in-app message is currently visible. Putting back on stack.", nil); + [self.inAppMessageStack addObject:inAppMessage]; + return; + } + + ABKInAppMessageDisplayChoice inAppMessageDisplayChoice; + if (inAppMessage.isControl) { + inAppMessageDisplayChoice = [self getCurrentDisplayChoiceForControlInAppMessage:inAppMessage]; + } else { + inAppMessageDisplayChoice = [self getCurrentDisplayChoiceForInAppMessage:inAppMessage]; + } + if (inAppMessageDisplayChoice == ABKDiscardInAppMessage) { + NSLog(@"%@: ABKDiscardInAppMessage received.", NSStringFromSelector(_cmd)); + } else if (inAppMessageDisplayChoice == ABKDisplayInAppMessageNow) { + NSLog(@"%@: ABKDisplayInAppMessageNow received: attempting to display the in-app message", NSStringFromSelector(_cmd)); + [self showInAppMessage:inAppMessage]; + } else if (inAppMessageDisplayChoice == ABKDisplayInAppMessageLater) { + NSLog(@"%@: ABKDisplayInAppMessageLater received. Returning in-app message to the stack.", NSStringFromSelector(_cmd)); + [self.inAppMessageStack addObject:inAppMessage]; + } else { + // Developer returned a wrong in-app message return value, print it out to inform the developer + NSLog(@"Invalid value returned from beforeInAppMessageDisplayed:withKeyboardIsUp:. Please return ABKDisplayInAppMessageNow, ABKDisplayInAppMessageLater, or ABKDiscardInAppMessage"); + } +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIDelegate.h b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIDelegate.h new file mode 100644 index 0000000000..10f884e25d --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageUIDelegate.h @@ -0,0 +1,130 @@ +#import +#import +#import "ABKInAppMessageViewController.h" +#import "AppboyKit.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +typedef NS_ENUM(NSInteger, ABKInAppMessageDisplayChoice); +@class ABKInAppMessage; +@class ABKInAppMessageImmersive; +@class ABKInAppMessageButton; +@class ABKInAppMessageHTMLBase; + +NS_ASSUME_NONNULL_BEGIN +/*! + * The in-app message UI delegate allows you to control the display and behavior of the Braze in-app message. + */ +@protocol ABKInAppMessageUIDelegate + +@optional + +/*! + * @param inAppMessage The in-app message object being offered to the delegate method. + * @param keyboardIsUp This boolean indicates whether or not the keyboard is currently being displayed when this + * delegate fires. + * @return ABKInAppMessageDisplayChoice for details refer to the documentation regarding the ENUM ABKInAppMessageDisplayChoice + * above. + * + * This delegate method defines whether the in-app message will be displayed now, displayed later, or discarded. + * + * The default behavior is that the in-app message will be displayed unless the keyboard is currently active on the screen. + * However, if there are other situations where you would not want the in-app message to appear (such as during a full screen + * game or on a loading screen), you can use this delegate to delay or discard pending in-app message messages. + * + * This method is deprecated. Please use the beforeInAppMessageDisplayed: method in ABKInAppMessageControllerDelegate + * and use the methods receiveKeyboardDidHideNotification: and receiveKeyboardWasShownNotification: + * in ABKInAppMessageUIController to customize based on keyboard behavior. + */ +- (ABKInAppMessageDisplayChoice)beforeInAppMessageDisplayed:(ABKInAppMessage *)inAppMessage withKeyboardIsUp:(BOOL)keyboardIsUp __deprecated; + +/*! + * @param inAppMessage The in-app message object being offered to the delegate. + * + * This delegate method allows host applications to customize the look of an in-app message while + * maintaining the same user experience and impression/click tracking as the default Braze in-app + * message. It allows developers to pass incoming in-app messages to custom view controllers which + * they have created. + * + * The custom view controller is responsible for handling any responsive UI layout use-cases. e.g. device orientations, + * or varied message lengths. + * + * Even with a custom view, by inheriting from ABKInAppMessageViewController, the in-app message will automatically animate and + * dismiss according to the parameters of the provided ABKInAppMessage object. See ABKInAppMessage.h for more information. + * + * By default, Braze will add following functions/changes to the custom view controller, and animate + * the in-app message on and off the screen, based on the class of the given in-app message: + * * ABKInAppMessageSlideup: + * * stretch/shrink the in-app message view's width to fix the screen's width. If you wish to + * have margins between the in-app message and the edge of the screen, those must be incorporated + * into the custom view controller itself. + * * add the impression and click tracking for the in-app message + * * when user clicks on the in-app message, call the onInAppMessageClicked:, and handle the click + * behavior correspond to the in-app message's inAppMessageClickActionType property. + * * add a pan gesture to the in-app message so user can swipe it away. + * * ABKInAppMessageModal: + * * make the in-app message clickable when there is no button(s) on it. + * * put the in-app message in the center of the screen, and add a full screen background layer. + * * ABKInAppMessageFull: + * * make the in-app message clickable when there is no button(s) on it. + * * stretch/shrink the in-app message view to fix the whole screen. + * + * @returns An ABKInAppMessageViewController subclass for which the view is an ABKInAppMessageView + * instance or subclass. Returning nil will prevent the in-app message from displaying. + */ +- (nullable ABKInAppMessageViewController *)inAppMessageViewControllerWithInAppMessage:(ABKInAppMessage *)inAppMessage; + +/*! + * @param inAppMessage The in-app message object being offered to the delegate. + * + * This delegate method is fired when: + * * the user manually dismisses the in-app message. + * * the in-app message times out and expires. + * * the close button on a modal in-app message or a full in-app message is clicked. + * Use this method to perform any custom logic that should execute after the in-app message has been + * dismissed. + */ +- (void)onInAppMessageDismissed:(ABKInAppMessage *)inAppMessage; + +/*! + * @param inAppMessage The in-app message object being offered to the delegate. + * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent + * Braze from performing the click action. Returning NO will cause Braze to execute the action defined in the + * in-app message's inAppMessageClickActionType property after this delegate method is called. + * + * This delegate method is fired when the user clicks on a slideup in-app message, or a modal/full + * in-app message without button(s) on it. See ABKInAppMessage.h for more information. + */ +- (BOOL)onInAppMessageClicked:(ABKInAppMessage *)inAppMessage; + +/*! + * @param inAppMessage The in-app message object being offered to the delegate. + * @param button The clicked button being offered to the delegate. + * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent + * Braze from performing the click action. Returning NO will cause Braze to execute the action defined in the + * button's inAppMessageClickActionType property after this delegate method is called. + * + * This delegate method is fired whenever the user clicks a button on the in-app message. See + * ABKInAppMessageBlock.h for more information. + */ +- (BOOL)onInAppMessageButtonClicked:(ABKInAppMessageImmersive *)inAppMessage button:(ABKInAppMessageButton *)button; + +/*! + * @param inAppMessage The in-app message object being offered to the delegate. + * @param clickedURL The URL that is clicked by user. + * @param buttonId The buttonId within the clicked link being offered to the delegate. + * @return Boolean Value which controls whether or not Braze will execute the click action. Returning YES will prevent + * Braze from performing the click action. Returning NO will cause Braze to follow the link. + * + * This delegate method is fired whenever the user clicks a link on the HTML in-app message. See + * ABKInAppMessageHTMLBase.h for more information. + */ +- (BOOL)onInAppMessageHTMLButtonClicked:(ABKInAppMessageHTMLBase *)inAppMessage clickedURL:(nullable NSURL *)clickedURL buttonID:(NSString *)buttonId; + +- (WKWebViewConfiguration *)setCustomWKWebViewConfiguration; + +@end +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.h b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.h new file mode 100644 index 0000000000..ba1d5fdedb --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.h @@ -0,0 +1,6 @@ +#import + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageView : UIView +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.m b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.m new file mode 100644 index 0000000000..0c609f5398 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageView.m @@ -0,0 +1,5 @@ +#import "ABKInAppMessageView.h" + +@implementation ABKInAppMessageView + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.h b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.h new file mode 100644 index 0000000000..075d7c7fcc --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.h @@ -0,0 +1,17 @@ +#import + +/*! + * ABKInAppMessageWindow handles a subset of all touches. + * + * By default, touches not handled by ABKInAppMessageWindow are automatically passed to the next + * UIWindow in the view hierarchy by UIKit. + */ +@interface ABKInAppMessageWindow : UIWindow + +/*! + * ABKInAppMessageWindow handles all touch events when enabled, no touch events are passed to a next + * UIWindow. + */ +@property (nonatomic) BOOL handleAllTouchEvents; + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.m b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.m new file mode 100644 index 0000000000..b8d93281d5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ABKInAppMessageWindow.m @@ -0,0 +1,41 @@ +#import "ABKInAppMessageWindow.h" +#import "ABKInAppMessageView.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKUIUtils.h" + +@import BrazeKit; +@import BrazeKitCompat; + +@interface ABKInAppMessageWindow () + +@end + +@implementation ABKInAppMessageWindow + +// Touches handled by ABKInAppMessageWindow: +// - all if `handleAllTouchEvents == YES` +// - in `ABKInAppMessageView` or one of its subviews +// - all if displaying an HTML in-app message +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + // Get the view in the hierarchy that contains the point + UIView *hitTestResult = [super hitTest:point withEvent:event]; + + // Always returns the view for HTML in-app messages + if ([self.rootViewController isKindOfClass:[ABKInAppMessageWindowController class]]) { + ABKInAppMessageWindowController *controller = (ABKInAppMessageWindowController *)self.rootViewController; + if ([controller.inAppMessage isKindOfClass:[ABKInAppMessageHTMLBase class]]) { + return hitTestResult; + } + } + + // Handles the touch event + if (self.handleAllTouchEvents || + [ABKUIUtils responderChainOf:hitTestResult hasKindOfClass:[ABKInAppMessageView class]]) { + return hitTestResult; + } + + return nil; +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/AppboyInAppMessage.h b/Sources/BrazeUICompat/ABKInAppMessage/AppboyInAppMessage.h new file mode 100644 index 0000000000..daddc29f1d --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/AppboyInAppMessage.h @@ -0,0 +1,17 @@ +#import "ABKInAppMessageUIButton.h" +#import "ABKInAppMessageUIController.h" +#import "ABKInAppMessageUIDelegate.h" +#import "ABKInAppMessageView.h" +#import "ABKInAppMessageWindow.h" +#import "ABKInAppMessageFullViewController.h" +#import "ABKInAppMessageHTMLFullViewController.h" +#import "ABKInAppMessageHTMLViewController.h" +#import "ABKInAppMessageHTMLBaseViewController.h" +#import "ABKInAppMessageImmersiveViewController.h" +#import "ABKInAppMessageModalViewController.h" +#import "ABKInAppMessageSlideupViewController.h" +#import "ABKInAppMessageViewController.h" +#import "ABKInAppMessageWindowController.h" + +#import "ABKUIURLUtils.h" +#import "ABKUIUtils.h" diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageFullViewController.xib b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageFullViewController.xib new file mode 100644 index 0000000000..7ed152c11f --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageFullViewController.xib @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageModalViewController.xib b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageModalViewController.xib new file mode 100644 index 0000000000..01a9d52654 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageModalViewController.xib @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageSlideupViewController.xib b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageSlideupViewController.xib new file mode 100644 index 0000000000..75050e3711 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ABKInAppMessageSlideupViewController.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/Base.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/Base.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..d1453a9a83 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/Base.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "Close"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/FontAwesome.otf b/Sources/BrazeUICompat/ABKInAppMessage/Resources/FontAwesome.otf new file mode 100644 index 0000000000..f7936cc1e7 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/FontAwesome.otf differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ar.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ar.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..af00bf8a78 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ar.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "لإغلاق"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow.png new file mode 100644 index 0000000000..a59d07fb08 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@2x.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@2x.png new file mode 100644 index 0000000000..c38103e3f8 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@2x.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@3x.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@3x.png new file mode 100644 index 0000000000..9a1f8e78a5 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/arrow@3x.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon.png new file mode 100644 index 0000000000..eb7e885a01 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@2x.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@2x.png new file mode 100644 index 0000000000..e9ae7e90df Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@2x.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@3x.png b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@3x.png new file mode 100644 index 0000000000..bf75340a71 Binary files /dev/null and b/Sources/BrazeUICompat/ABKInAppMessage/Resources/com_appboy_inapp_close_icon@3x.png differ diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/cs.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/cs.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..5e2699296a --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/cs.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "zavřít"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/da.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/da.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..a0186606cb --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/da.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "at lukke"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/de.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/de.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..e70cf17290 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/de.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "schließen"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/en.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/en.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..d1453a9a83 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/en.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "Close"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-419.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-419.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..f7bf166053 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-419.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-MX.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-MX.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..f7bf166053 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es-MX.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/es.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..f7bf166053 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/es.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/et.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/et.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..4adcde76ac --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/et.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "sulgema"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/fi.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fi.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..5b21ade665 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fi.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "sulkea"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/fil.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fil.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..cf9586bd6d --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fil.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "Isara"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/fr.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fr.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..3d0adc6f2b --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/fr.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "fermer"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/he.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/he.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..ea3ea6228b --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/he.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "לִסְגוֹר"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/hi.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/hi.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..d0e97596f5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/hi.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "बंद करना"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/id.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/id.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..7c2b753eab --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/id.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "untuk menutup"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/it.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/it.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..c183a5b6d4 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/it.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "chiudere"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ja.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ja.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..e109af451e --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ja.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "閉じる"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/km.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/km.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..55568c7426 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/km.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "បិទ"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ko.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ko.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..9313a6daad --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ko.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "닫다"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/lo.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/lo.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..8338e16bd3 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/lo.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "ປິດ"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ms.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ms.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..7c2b753eab --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ms.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "untuk menutup"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/my.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/my.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..eb81a6398a --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/my.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "ပိတ်ရန်"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/nb.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/nb.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..657206e4ad --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/nb.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "å lukke"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/nl.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/nl.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..6223d5a0d8 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/nl.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "sluiten"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/pl.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pl.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..4f39922535 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pl.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "zamknąć"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt-PT.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt-PT.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..6fdffb65d8 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt-PT.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "fechar"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..6fdffb65d8 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/pt.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "fechar"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/ru.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ru.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..e808f2a8b7 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/ru.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "закрывать"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/sv.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/sv.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..d4048cc618 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/sv.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "stänga"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/th.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/th.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..12fbdaaaf1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/th.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "ปิด"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/uk.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/uk.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..e1850aebb4 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/uk.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "закрити"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/vi.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/vi.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..0396373348 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/vi.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "đóng"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-HK.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-HK.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..3d330d04fc --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-HK.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hans.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hans.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..fc834b49bd --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hans.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "关闭"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hant.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hant.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..3d330d04fc --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-Hant.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-TW.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-TW.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..3d330d04fc --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh-TW.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh.lproj/AppboyInAppMessageLocalizable.strings b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh.lproj/AppboyInAppMessageLocalizable.strings new file mode 100644 index 0000000000..fc834b49bd --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/Resources/zh.lproj/AppboyInAppMessageLocalizable.strings @@ -0,0 +1 @@ +"Appboy.in-app-message.close-button.title" = "关闭"; diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.h new file mode 100644 index 0000000000..b28a491b56 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.h @@ -0,0 +1,9 @@ +#import "ABKInAppMessageImmersiveViewController.h" + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageFullViewController : ABKInAppMessageImmersiveViewController + +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *closeXButtonTopConstraint; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.m new file mode 100755 index 0000000000..4b04544b7e --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.m @@ -0,0 +1,157 @@ +#import "ABKInAppMessageFullViewController.h" +#import "ABKInAppMessageViewController.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static const CGFloat FullViewInIPadCornerRadius = 8.0f; +static const CGFloat MaxLongEdge = 720.0f; +static const CGFloat MaxShortEdge = 450.0f; +static const CGFloat CloseXPadding = 15.0f; + +@implementation ABKInAppMessageFullViewController + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + CGFloat maxWidth = MaxShortEdge; + CGFloat maxHeight = MaxLongEdge; + if (self.inAppMessage.orientation == ABKInAppMessageOrientationLandscape) { + maxWidth = MaxLongEdge; + maxHeight = MaxShortEdge; + } + if (self.isiPad) { + NSArray *widthConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" + options:0 + metrics:@{@"max" : @(maxWidth)} + views:@{@"view" : self.view}]; + NSArray *heightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" + options:0 + metrics:@{@"max" : @(maxHeight)} + views:@{@"view" : self.view}]; + [self.view addConstraints:widthConstraints]; + [self.view addConstraints:heightConstraints]; + self.view.layer.cornerRadius = FullViewInIPadCornerRadius; + self.view.layer.masksToBounds = YES; + + [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=0)-[view]-(>=0)-|" + options:0 + metrics:nil + views:@{@"view" : self.view}]]; + } else { + NSLayoutConstraint *leadConstraint = [NSLayoutConstraint constraintWithItem:self.view + attribute:NSLayoutAttributeLeading + relatedBy:NSLayoutRelationEqual + toItem:self.view.superview + attribute:NSLayoutAttributeLeading + multiplier:1 + constant:0.0]; + NSLayoutConstraint *trailConstraint = [NSLayoutConstraint constraintWithItem:self.view.superview + attribute:NSLayoutAttributeTrailing + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeTrailing + multiplier:1 + constant:0.0]; + [self.view.superview addConstraints:@[leadConstraint, trailConstraint]]; + } + + NSString *heightVisualFormat = self.isiPad? @"V:|-(>=0)-[view]-(>=0)-|" : @"V:|[view]|"; + NSArray *heightConstraints = [NSLayoutConstraint constraintsWithVisualFormat:heightVisualFormat + options:0 + metrics:nil + views:@{@"view" : self.view}]; + [self.view.superview addConstraints:heightConstraints]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + // Close X should be equidistant from top and right in notched phones despite presence of (hidden) status bar + if (![ABKUIUtils isNotchedPhone]) { + if (!self.isiPad) { + CGSize statusBarSize = [ABKUIUtils getStatusBarSize]; + self.closeXButtonTopConstraint.constant = CloseXPadding - statusBarSize.height; + } + } else { + // Move close x button slightly higher for notched phones in portrait + BOOL isPortrait = UIInterfaceOrientationIsPortrait([ABKUIUtils getInterfaceOrientation]); + self.closeXButtonTopConstraint.constant = isPortrait ? 0.0f : CloseXPadding; + } +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.textsView flashScrollIndicators]; +} + +- (void)loadView { + NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageFullViewController class] channel:ABKInAppMessageChannel]; + [bundle loadNibNamed:@"ABKInAppMessageFullViewController" + owner:self + options:nil]; + self.inAppMessageHeaderLabel.font = HeaderLabelDefaultFont; + self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; + + if (self.inAppMessage.message) { + NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; + NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; + [messageStyle setLineSpacing:2]; + [attributedStringMessage addAttribute:NSParagraphStyleAttributeName + value:messageStyle + range:NSMakeRange(0, self.inAppMessage.message.length)]; + self.inAppMessageMessageLabel.attributedText = attributedStringMessage; + } + if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { + if (((ABKInAppMessageImmersive *)self.inAppMessage).header) { + NSMutableAttributedString *attributedStringHeader = [[NSMutableAttributedString alloc] initWithString:((ABKInAppMessageImmersive *)self.inAppMessage).header]; + NSMutableParagraphStyle *headerStyle = [[NSMutableParagraphStyle alloc] init]; + [headerStyle setLineSpacing:2]; + [attributedStringHeader addAttribute:NSParagraphStyleAttributeName + value:headerStyle + range:NSMakeRange(0, ((ABKInAppMessageImmersive *)self.inAppMessage).header.length)]; + self.inAppMessageMessageLabel.attributedText = attributedStringHeader; + } + } +} + +#pragma mark - Superclass methods + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +- (UIView *)bottomViewWithNoButton { + return self.textsView; +} + +- (void)setupLayoutForGraphic { + [super applyImageToImageView:self.graphicImageView]; + [self.iconImageView removeFromSuperview]; + [self.textsView removeFromSuperview]; + self.iconImageView = nil; + self.textsView = nil; +} + +- (void)setupLayoutForTopImage { + [self.graphicImageView removeFromSuperview]; + self.graphicImageView = nil; + self.inAppMessageMessageLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.textsView.translatesAutoresizingMaskIntoConstraints = NO; + + // When there is no header, we set following two things to 0: + // (1) the header label's height + // (2) the constraint's height between header label and the message label + // so that the space is collapsed. + if (![ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).header]) { + for (NSLayoutConstraint *constraint in self.inAppMessageHeaderLabel.constraints) { + if (constraint.firstAttribute == NSLayoutAttributeHeight) { + constraint.constant = 0.0f; + break; + } + } + self.headerBodySpaceConstraint.constant = 0.0f; + } + [super applyImageToImageView:self.iconImageView]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.h new file mode 100644 index 0000000000..a8e4fc3748 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.h @@ -0,0 +1,27 @@ +#import +#import +#import "ABKInAppMessageViewController.h" + +NS_ASSUME_NONNULL_BEGIN +static NSString *const ABKInAppMessageHTMLFileName = @"index.html"; + +@interface ABKInAppMessageHTMLBaseViewController : ABKInAppMessageViewController + +/*! + * The WKWebView used to parse and display the HTML. + */ +@property (nonatomic) WKWebView *webView; + +/*! + * The constraints for top and bottom between view and the super view. + */ +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *topConstraint; +@property (weak, nonatomic) IBOutlet NSLayoutConstraint *bottomConstraint; + +/*! + * The flag specifying if body clicks should be registered automatically. Defaults to NO. + */ +@property (assign, nonatomic, readonly) BOOL automaticBodyClicksEnabled; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.m new file mode 100644 index 0000000000..0e55150725 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.m @@ -0,0 +1,443 @@ +#import "ABKInAppMessageHTMLBaseViewController.h" +#import "ABKInAppMessageView.h" +#import "ABKUIUtils.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKUIURLUtils.h" + +@import BrazeKitCompat; + +static NSString *const ABKBlankURLString = @"about:blank"; +static NSString *const ABKHTMLInAppButtonIdKey = @"abButtonId"; +static NSString *const ABKHTMLInAppAppboyKey = @"appboy"; +static NSString *const ABKHTMLInAppCloseKey = @"close"; +static NSString *const ABKHTMLInAppFeedKey = @"feed"; +static NSString *const ABKHTMLInAppCustomEventKey = @"customEvent"; +static NSString *const ABKHTMLInAppCustomEventQueryParamNameKey = @"name"; +static NSString *const ABKHTMLInAppExternalOpenKey = @"abExternalOpen"; +static NSString *const ABKHTMLInAppDeepLinkKey = @"abDeepLink"; +static NSString *const ABKHTMLInAppJavaScriptExtension = @"js"; + +@interface ABKInAppMessageHTMLBaseViewController () + +@property (nonatomic) ABKInAppMessageWebViewBridge *webViewBridge; + +@end + +@implementation ABKInAppMessageHTMLBaseViewController + +#pragma mark - Properties + +- (BOOL)automaticBodyClicksEnabled { + return NO; +} + +#pragma mark - View Lifecycle + +- (void)loadView { + // View needs to be an ABKInAppMessageView to ensure touches register as per custom logic + // in ABKInAppMessageWindow. The frame is set in `beforeMoveInAppMessageViewOnScreen`. + self.view = [[ABKInAppMessageView alloc] initWithFrame:CGRectZero]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.view.translatesAutoresizingMaskIntoConstraints = NO; + + NSLayoutConstraint *leadConstraint = [self.view.leadingAnchor constraintEqualToAnchor:self.view.superview.leadingAnchor]; + NSLayoutConstraint *trailConstraint = [self.view.trailingAnchor constraintEqualToAnchor:self.view.superview.trailingAnchor]; + + // Top and bottom constants will be populated with the actual frame sizes after + // the HTML content is fully loaded in `beforeMoveInAppMessageViewOnScreen` +#if TARGET_OS_MACCATALYST + // Within safe zone + self.topConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.topAnchor]; + self.bottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.bottomAnchor]; +#else + // Extends to the edges of the screen + self.topConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.topAnchor]; + self.bottomConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.bottomAnchor]; +#endif + + [self.view.superview addConstraints:@[leadConstraint, trailConstraint, self.topConstraint, self.bottomConstraint]]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + self.edgesForExtendedLayout = UIRectEdgeNone; + WKWebViewConfiguration *webViewConfiguration = [[WKWebViewConfiguration alloc] init]; + webViewConfiguration.allowsInlineMediaPlayback = YES; + webViewConfiguration.suppressesIncrementalRendering = YES; + /* + if (@available(iOS 10.0, *)) { + webViewConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + } else { + webViewConfiguration.requiresUserActionForMediaPlayback = YES; + } + */ + webViewConfiguration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + + ABKInAppMessageWindowController *parentViewController = + (ABKInAppMessageWindowController *)self.parentViewController; + if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(setCustomWKWebViewConfiguration)]) { + webViewConfiguration = [parentViewController.inAppMessageUIDelegate setCustomWKWebViewConfiguration]; + } + + WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:webViewConfiguration]; + self.webView = webView; + + self.webViewBridge = [[ABKInAppMessageWebViewBridge alloc] initWithWebView:webView + inAppMessage:(ABKInAppMessageHTML *)self.inAppMessage appboyInstance:[Appboy sharedInstance]]; + self.webViewBridge.delegate = self; + + self.webView.allowsLinkPreview = NO; + self.webView.navigationDelegate = self; + self.webView.UIDelegate = self; + self.webView.scrollView.bounces = NO; + + // Handle resizing during orientation changes + self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + + if (@available(iOS 11.0, *)) { + // Cover status bar when showing HTML IAMs + [self.webView.scrollView setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever]; + } + if (((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath != nil) { + NSFileManager *fileManager = NSFileManager.defaultManager; + NSURL *localPath = ((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath; + [fileManager createDirectoryAtPath:localPath.path withIntermediateDirectories:YES attributes:nil error:nil]; + // Here we must use fileURLWithPath: to add the "file://" scheme, otherwise the webView won't recognize the + // base URL and won't load the zip file resources. + NSURL *html = [localPath URLByAppendingPathComponent:ABKInAppMessageHTMLFileName]; + [self.inAppMessage.message writeToURL:html atomically:NO encoding:NSUTF8StringEncoding error:nil]; + if (![fileManager fileExistsAtPath:html.path]) { + NSLog(@"Can't find HTML at path %@, with file name %@. Aborting display.", localPath, ABKInAppMessageHTMLFileName); + [self hideInAppMessage:NO]; + } + [self.webView loadFileURL:html allowingReadAccessToURL:localPath]; + } else { + [self.webView loadHTMLString:self.inAppMessage.message baseURL:nil]; + } + [self.view addSubview:self.webView]; + + // Sets an observer for UIKeyboardWillHideNotification. This is a workaround for the + // keyboard dismissal bug in iOS 12+ WKWebView filed here + // https://bugs.webkit.org/show_bug.cgi?id=192564. The workaround is also from the post. + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide) name:UIKeyboardWillHideNotification object:nil]; +} + +#pragma mark - Superclass methods + +- (BOOL)prefersStatusBarHidden { + return YES; +} + +#pragma mark - NSNotificationCenter selectors + +- (void)keyboardWillHide { + [self.webView setNeedsLayout]; +} + +#pragma mark - WKDelegate methods + +- (WKWebView *)webView:(WKWebView *)webView +createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration + forNavigationAction:(WKNavigationAction *)navigationAction + windowFeatures:(WKWindowFeatures *)windowFeatures { + if (navigationAction.targetFrame == nil) { + [webView loadRequest:navigationAction.request]; + } + return nil; +} + +- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction +decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSURL *url = navigationAction.request.URL; + + // Handle normal html resource loading + BOOL isSystemOpen = [ABKUIURLUtils URLHasSystemScheme:url]; + BOOL isIframeLoad = navigationAction.targetFrame != nil && ![navigationAction.sourceFrame isEqual:navigationAction.targetFrame]; + BOOL isIframeNavigation = navigationAction.targetFrame != nil && navigationAction.targetFrame.mainFrame == NO; + NSString *assetPath = ((ABKInAppMessageHTMLBase *)self.inAppMessage).assetsLocalDirectoryPath.absoluteString; + BOOL isHandledByWebView = + !isSystemOpen && + ( + !url || + isIframeLoad || + isIframeNavigation || + [ABKUIUtils string:url.absoluteString isEqualToString:ABKBlankURLString] || + [ABKUIUtils string:url.path isEqualToString:assetPath] || + [ABKUIUtils string:url.lastPathComponent isEqualToString:ABKInAppMessageHTMLFileName] + ); + + if (isHandledByWebView) { + decisionHandler(WKNavigationActionPolicyAllow); + return; + } + + // Handle Braze specific actions + NSDictionary *queryParams = [self queryParameterDictionaryFromURL:url]; + NSString *buttonId = [self parseButtonIdFromQueryParams:queryParams]; + ABKInAppMessageWindowController *parentViewController = + (ABKInAppMessageWindowController *)self.parentViewController; + + [self setClickActionBasedOnURL:url]; + parentViewController.clickedHTMLButtonId = buttonId; + + // - Delegate handling + if ([self delegateHandlesHTMLButtonClick:parentViewController.inAppMessageUIDelegate + URL:url + buttonId:buttonId]) { + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + // - Custom event handling + if ([self isCustomEventURL:url]) { + [self handleCustomEventWithQueryParams:queryParams]; + decisionHandler(WKNavigationActionPolicyCancel); + return; + } + + // - Body click handling + if (![ABKUIUtils objectIsValidAndNotEmpty:buttonId]) { + if (self.automaticBodyClicksEnabled) { + parentViewController.inAppMessageIsTapped = YES; + NSLog(@"In-app message body click registered. Automatic body clicks are enabled."); + } else { + NSLog(@"In-app message body click not registered. Automatic body clicks are disabled."); + } + } + [parentViewController inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType + URL:url + openURLInWebView:[self getOpenURLInWebView:queryParams]]; + decisionHandler(WKNavigationActionPolicyCancel); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + self.webView.backgroundColor = [UIColor clearColor]; + self.webView.opaque = NO; + if (self.inAppMessage.animateIn) { + [UIView animateWithDuration:InAppMessageAnimationDuration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + self.topConstraint.constant = 0; + self.bottomConstraint.constant = 0; + [self.view.superview layoutIfNeeded]; + } + completion:^(BOOL finished){ + }]; + } else { + self.topConstraint.constant = 0; + self.bottomConstraint.constant = 0; + [self.view.superview layoutIfNeeded]; + } + + // Disable touch callout from displaying link information + [self.webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil]; +} + +- (void)webView:(WKWebView *)webView +runJavaScriptAlertPanelWithMessage:(nonnull NSString *)message + initiatedByFrame:(nonnull WKFrameInfo *)frame + completionHandler:(nonnull void (^)(void))completionHandler { + [self presentAlertWithMessage:message + andConfiguration:^(UIAlertController *alert) { + // Action labels matches Safari implementation + // Close + [alert addAction:[UIAlertAction actionWithTitle:@"Close" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + completionHandler(); + }]]; + }]; +} + +- (void)webView:(WKWebView *)webView +runJavaScriptConfirmPanelWithMessage:(NSString *)message + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(BOOL))completionHandler { + [self presentAlertWithMessage:message andConfiguration:^(UIAlertController *alert) { + // Action labels matches Safari implementation + // Cancel + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * _Nonnull action) { + completionHandler(NO); + }]]; + + // OK + [alert addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + completionHandler(YES); + }]]; + }]; +} + +- (void)webView:(WKWebView *)webView +runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt + defaultText:(NSString *)defaultText + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(NSString * _Nullable))completionHandler { + [self presentAlertWithMessage:prompt + andConfiguration:^(UIAlertController *alert) { + // Action labels matches Safari implementation + // Text field + [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { + textField.text = defaultText; + }]; + + // Cancel + [alert addAction:[UIAlertAction actionWithTitle:@"Cancel" + style:UIAlertActionStyleCancel + handler:^(UIAlertAction * _Nonnull action) { + completionHandler(nil); + }]]; + + // OK + [alert addAction:[UIAlertAction actionWithTitle:@"OK" + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * _Nonnull action) { + completionHandler(alert.textFields[0].text); + }]]; + }]; +} + +- (BOOL)isCustomEventURL:(NSURL *)url { + return ([ABKUIUtils string:url.scheme.lowercaseString isEqualToString:ABKHTMLInAppAppboyKey] && + [ABKUIUtils string:url.host isEqualToString:ABKHTMLInAppCustomEventKey]); +} + +- (BOOL)getOpenURLInWebView:(NSDictionary *)queryParams { + if ([queryParams[ABKHTMLInAppDeepLinkKey] boolValue] | [queryParams[ABKHTMLInAppExternalOpenKey] boolValue]) { + return NO; + } + return self.inAppMessage.openUrlInWebView; +} + +#pragma mark - Delegate + +- (BOOL)delegateHandlesHTMLButtonClick:(id)delegate + URL:(NSURL *)url + buttonId:(NSString *)buttonId { + if ([delegate respondsToSelector:@selector(onInAppMessageHTMLButtonClicked:clickedURL:buttonID:)]) { + if ([delegate onInAppMessageHTMLButtonClicked:(ABKInAppMessageHTMLBase *)self.inAppMessage + clickedURL:url + buttonID:buttonId]) { + NSLog(@"No in-app message click action will be performed by Braze as in-app message delegate %@ returned YES in onInAppMessageHTMLButtonClicked:", delegate); + return YES; + } + } + return NO; +} + +#pragma mark - Custom Event Handling + +- (void)handleCustomEventWithQueryParams:(NSDictionary *)queryParams { + NSString *customEventName = [self parseCustomEventNameFromQueryParams:queryParams]; + NSMutableDictionary *eventProperties = [self parseCustomEventPropertiesFromQueryParams:queryParams]; + [[Appboy sharedInstance] logCustomEvent:customEventName withProperties:eventProperties]; +} + +- (NSString *)parseCustomEventNameFromQueryParams:(NSDictionary *)queryParams { + return queryParams[ABKHTMLInAppCustomEventQueryParamNameKey]; +} + +- (NSMutableDictionary *)parseCustomEventPropertiesFromQueryParams:(NSDictionary *)queryParams { + NSMutableDictionary *eventProperties = [queryParams mutableCopy]; + [eventProperties removeObjectForKey:ABKHTMLInAppCustomEventQueryParamNameKey]; + return eventProperties; +} + +#pragma mark - Button Click Handling + +- (NSString *)parseButtonIdFromQueryParams:(NSDictionary *)queryParams { + return queryParams[ABKHTMLInAppButtonIdKey]; +} + +// Set the inAppMessage's click action type based on given URL. It's going to be three types: +// * URL is appboy://close: set click action to be ABKInAppMessageNoneClickAction +// * URL is appboy://feed: set click action to be ABKInAppMessageDisplayNewsFeed +// * URL is anything else: set click action to be ABKInAppMessageRedirectToURI and the uri is the URL. +- (void)setClickActionBasedOnURL:(NSURL *)url { + if ([ABKUIUtils string:url.scheme.lowercaseString isEqualToString:ABKHTMLInAppAppboyKey]) { + if ([ABKUIUtils string:url.host.lowercaseString isEqualToString:ABKHTMLInAppCloseKey]) { + [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageNoneClickAction withURI:nil]; + return; + } else if ([ABKUIUtils string:url.host.lowercaseString isEqualToString:ABKHTMLInAppFeedKey]) { + [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageDisplayNewsFeed withURI:nil]; + return; + } + } + [self.inAppMessage setInAppMessageClickAction:ABKInAppMessageRedirectToURI withURI:url]; +} + +#pragma mark - Utility Methods + +- (NSDictionary *)queryParameterDictionaryFromURL:(NSURL *)url { + NSMutableDictionary *dict = [NSMutableDictionary dictionary]; + NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem *queryItem in components.queryItems) { + dict[queryItem.name] = queryItem.value; + } + + return [dict copy]; +} + +- (void)presentAlertWithMessage:(NSString *)message + andConfiguration:(void (^)(UIAlertController *alert))configure { + UIAlertController *alert = [UIAlertController alertControllerWithTitle:nil + message:message + preferredStyle:UIAlertControllerStyleAlert]; + configure(alert); + [self presentViewController:alert animated:YES completion:nil]; +} + +#pragma mark - Animation + +- (void)beforeMoveInAppMessageViewOnScreen { + self.topConstraint.constant = self.view.frame.size.height; + self.bottomConstraint.constant = self.view.frame.size.height; +} + +- (void)moveInAppMessageViewOnScreen { + // Do nothing - moving the in-app message is handled in didFinishNavigation + // though that logic should probably be gated by a call here. In a perfect world, + // ABKInAppMessageWindowController would "request" VC's to show themselves, + // and the VC's would report when they were shown so ABKInAppMessageWindowController + // could log impressions. +} + +- (void)beforeMoveInAppMessageViewOffScreen { + self.topConstraint.constant = self.view.frame.size.height; + self.bottomConstraint.constant = self.view.frame.size.height; +} + +- (void)moveInAppMessageViewOffScreen { + [self.view.superview layoutIfNeeded]; +} + +#pragma mark - ABKInAppMessageWebViewBridgeDelegate + +- (void)webViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge + receivedClickAction:(ABKInAppMessageClickActionType)clickAction { + ABKInAppMessageWindowController *parentViewController = + (ABKInAppMessageWindowController *)self.parentViewController; + + [self.inAppMessage setInAppMessageClickAction:clickAction withURI:nil]; + [parentViewController inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType + URL:nil + openURLInWebView:false]; +} + +- (void)closeMessageWithWebViewBridge:(ABKInAppMessageWebViewBridge *)webViewBridge { + ABKInAppMessageWindowController *parentViewController = + (ABKInAppMessageWindowController *)self.parentViewController; + if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { + [parentViewController.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; + } + [super hideInAppMessage:self.inAppMessage.animateOut]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.h new file mode 100644 index 0000000000..c9e95fde47 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.h @@ -0,0 +1,9 @@ +#import +#import "ABKInAppMessageHTMLBaseViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ABKInAppMessageHTMLFullViewController : ABKInAppMessageHTMLBaseViewController + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.m new file mode 100755 index 0000000000..bc338069d0 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.m @@ -0,0 +1,12 @@ +#import "ABKInAppMessageHTMLFullViewController.h" + +/*! + * Custom implementation for the zip-based HTML IAM type + */ +@implementation ABKInAppMessageHTMLFullViewController + +- (BOOL)automaticBodyClicksEnabled { + return YES; +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.h new file mode 100644 index 0000000000..4a5119be80 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.h @@ -0,0 +1,10 @@ +#import +#import "ABKInAppMessageHTMLBaseViewController.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface ABKInAppMessageHTMLViewController : ABKInAppMessageHTMLBaseViewController + + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.m new file mode 100644 index 0000000000..235b37f1c7 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.m @@ -0,0 +1,8 @@ +#import "ABKInAppMessageHTMLViewController.h" + +/*! + * Custom implementation for the file-based HTML IAM type + */ +@implementation ABKInAppMessageHTMLViewController + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.h new file mode 100644 index 0000000000..232beacd1d --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.h @@ -0,0 +1,78 @@ +#import "ABKInAppMessageViewController.h" +#import "ABKInAppMessageUIButton.h" + +// Customize this to set the font for the in-app message header. +#define HeaderLabelDefaultFont [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold] + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageImmersiveViewController : ABKInAppMessageViewController + +/*! + * The UILabel for the in-app message header. + */ +@property (weak, nonatomic) IBOutlet UILabel *inAppMessageHeaderLabel; + +/*! + * The UIImageView for the in-app message image. + */ +@property (weak, nonatomic, nullable) IBOutlet UIImageView *graphicImageView; + +/*! + * The NSLayoutConstraint that specifies the space between the header and rest of the in-app message. + */ +@property (nonatomic) IBOutlet NSLayoutConstraint *headerBodySpaceConstraint; + +/*! + * The UIButton on the left of the in-app message. + * When there is only one button in the in-app message, this left button is the one that is used. + */ +@property (retain, nonatomic, nullable) IBOutlet ABKInAppMessageUIButton *leftInAppMessageButton; + +/*! + * The UIButton on the right of the in-app message. + */ +@property (retain, nonatomic, nullable) IBOutlet ABKInAppMessageUIButton *rightInAppMessageButton; + +/*! + * The UIScrollView for the message of the in-app message. + */ +@property (nonatomic, nullable) IBOutlet UIScrollView *textsView; + +/*! + * @discussion This method is used for setting up the layout for ABKInAppMessageGraphic image style. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)setupLayoutForGraphic; + +/*! + * @discussion This method is used for setting up the layout for ABKInAppMessageTopImage image style. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)setupLayoutForTopImage; + +/*! + * @discussion This method is used for setting the color of the close button. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)changeCloseButtonColor; + +/*! + * @discussion The touch up inside action for the close button. The default behavior is to close the + * in-app message. + * + * For customization, please use a subclass or category to override this method. + */ +- (IBAction)dismissInAppMessage:(id)sender; + +/*! + * @discussion The touch up inside action for the in-app message buttons. + * + * For customization, please use a subclass or category to override this method. + */ +- (IBAction)buttonClicked:(ABKInAppMessageUIButton *)button; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.m new file mode 100644 index 0000000000..26319a9085 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.m @@ -0,0 +1,218 @@ +#import "ABKInAppMessageImmersiveViewController.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static NSInteger const CloseButtonTag = 50; + +@implementation ABKInAppMessageImmersiveViewController + +#pragma mark - Immersive In-App Message View UI Initialization + +- (void)viewDidLoad { + [super viewDidLoad]; + + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.inAppMessageMessageLabel]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + self.view.translatesAutoresizingMaskIntoConstraints = NO; + ABKInAppMessageImmersive *inAppMessage = [self getInAppMessage]; + + self.inAppMessageHeaderLabel.text = inAppMessage.header; + self.inAppMessageHeaderLabel.textAlignment = inAppMessage.headerTextAlignment; + self.graphicImageView.contentMode = self.inAppMessage.imageContentMode; + + if (inAppMessage.headerTextColor != nil) { + [self.inAppMessageHeaderLabel setTextColor:inAppMessage.headerTextColor]; + } + [self changeCloseButtonColor]; + [self setCloseButtonAccessibilityLabel]; + + if (inAppMessage.imageStyle == ABKInAppMessageGraphic) { + [self setupLayoutForGraphic]; + } else { + [self setupLayoutForTopImage]; + } + [self setupButtons]; + if (![inAppMessage isKindOfClass:[ABKInAppMessageFull class]]) { + if (inAppMessage.frameColor != nil) { + self.view.superview.backgroundColor = inAppMessage.frameColor; + } else { + self.view.superview.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:.3]; + } + } + + NSLayoutConstraint *centerYConstraint = [NSLayoutConstraint constraintWithItem:self.view + attribute:NSLayoutAttributeCenterY + relatedBy:NSLayoutRelationEqual + toItem:self.view.superview + attribute:NSLayoutAttributeCenterY + multiplier:1 + constant:0]; + centerYConstraint.priority = 999; + [self.view.superview addConstraint:centerYConstraint]; + + [self.view.superview addConstraint:[NSLayoutConstraint constraintWithItem:self.view + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view.superview + attribute:NSLayoutAttributeCenterX + multiplier:1 + constant:0]]; + + self.view.alpha = 0.0f; +} + +- (nullable UIButton *)getCloseButton { + UIView *buttonView = [self.view viewWithTag:CloseButtonTag]; + if ([buttonView isKindOfClass:[UIButton class]]) { + return (UIButton *) buttonView; + } + return nil; +} + +- (void)changeCloseButtonColor { + UIButton *closeButton = [self getCloseButton]; + if (closeButton != nil) { + UIColor *closeButtonColor = [self getInAppMessage].closeButtonColor ? + [self getInAppMessage].closeButtonColor : + [UIColor colorWithRed:(155.0/255.0) green:(155.0/255.0) blue:(155.0/255.0) alpha:1.0]; + UIImageView *closeButtonImageView = closeButton.imageView; + closeButtonImageView.image = [closeButtonImageView.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + closeButtonImageView.tintColor = closeButtonColor; + [closeButton setImage:closeButtonImageView.image forState:UIControlStateNormal]; + + // Copy of the imageView for the Selected state + UIImageView *closeButtonSelectedImageView = [[UIImageView alloc] initWithImage:closeButton.imageView.image]; + closeButtonSelectedImageView.tintColor = [closeButtonColor colorWithAlphaComponent:InAppMessageSelectedOpacity]; + [closeButton setImage:closeButtonSelectedImageView.image forState:UIControlStateSelected]; + } +} + +- (void)setCloseButtonAccessibilityLabel { + UIButton *closeButton = [self getCloseButton]; + if (closeButton != nil) { + closeButton.accessibilityLabel = [self localizedAppboyInAppMessageString:@"Appboy.in-app-message.close-button.title"]; + } +} + +- (NSString *)localizedAppboyInAppMessageString:(NSString *)key { + return [ABKUIUtils getLocalizedString:key + inAppboyBundle:[ABKUIUtils bundle:[ABKInAppMessageImmersiveViewController class] channel:ABKInAppMessageChannel] + table:@"AppboyInAppMessageLocalizable"]; +} + +- (void)setupLayoutForGraphic { + NSLog(@"Please override method setupLayoutForGraphic: to create proper layout for graphic image style."); +} + +- (void)setupLayoutForTopImage { + NSLog(@"Please override method setupLayoutForTopImage: to create proper layout for top image style."); +} + +- (void)setupButtons { + NSArray *buttons = [self getInAppMessage].buttons; + if (![ABKUIUtils objectIsValidAndNotEmpty:buttons]) { + [self.leftInAppMessageButton removeFromSuperview]; + [self.rightInAppMessageButton removeFromSuperview]; + self.leftInAppMessageButton = nil; + self.rightInAppMessageButton = nil; + if (([[self getInAppMessage] isKindOfClass:[ABKInAppMessageModal class]] + || [[self getInAppMessage] isKindOfClass:[ABKInAppMessageFull class]]) + && [self getInAppMessage].imageStyle != ABKInAppMessageGraphic) { + UIView *bottomView = [self bottomViewWithNoButton]; + if ([ABKUIUtils objectIsValidAndNotEmpty:bottomView]) { + NSArray *bottomConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view]-30-|" + options:0 + metrics:nil + views:@{@"view" : bottomView}]; + [self.view addConstraints:bottomConstraints]; + } + } + } else if (buttons.count == 1) { + [self.leftInAppMessageButton removeFromSuperview]; + self.leftInAppMessageButton = nil; + self.rightInAppMessageButton.inAppButtonModel = buttons[0]; + NSLayoutConstraint *constraintHorizontal = [NSLayoutConstraint constraintWithItem:self.rightInAppMessageButton + attribute:NSLayoutAttributeCenterX + relatedBy:NSLayoutRelationEqual + toItem:self.view + attribute:NSLayoutAttributeCenterX + multiplier:1.0f + constant:0.0f]; + [self.view addConstraint:constraintHorizontal]; + } else { + self.leftInAppMessageButton.inAppButtonModel = buttons[0]; + self.rightInAppMessageButton.inAppButtonModel = buttons[1]; + } +} + +- (void)setInAppMessage:(ABKInAppMessage *)inAppMessage { + if ([inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { + super.inAppMessage = inAppMessage; + } else { + NSLog(@"ABKInAppMessageImmersiveViewController only accepts in-app message with type ABKInAppMessageImmersive. Setting in-app message fails."); + } +} + +- (UIView *)bottomViewWithNoButton { + return nil; +} + +#pragma mark - Animation + +- (void)moveInAppMessageViewOnScreen { + self.view.alpha = 1.0f; +} + +- (void)moveInAppMessageViewOffScreen { + self.view.alpha = 0.0f; +} + +#pragma mark - Button Actions + +- (IBAction)dismissInAppMessage:(id)sender { + ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; + if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { + [parentViewController.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; + } + [super hideInAppMessage:self.inAppMessage.animateOut]; +} + +- (IBAction)buttonClicked:(ABKInAppMessageUIButton *)button { + ABKInAppMessageWindowController *parentViewController = (ABKInAppMessageWindowController *)self.parentViewController; + parentViewController.clickedButtonId = button.inAppButtonModel.buttonID; + // Calls the delegate method for button click if it has been implemented. + if ([parentViewController.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageButtonClicked:button:)]) { + if ([parentViewController.inAppMessageUIDelegate + onInAppMessageButtonClicked:(ABKInAppMessageImmersive *)self.inAppMessage + button:button.inAppButtonModel]) { + NSLog(@"No in-app message click action will be performed by Braze as inAppMessageUIDelegate %@ returned YES in onInAppMessageButtonClicked:", parentViewController.inAppMessageUIDelegate); + return; + } + } + [parentViewController inAppMessageClickedWithActionType:button.inAppButtonModel.buttonClickActionType + URL:button.inAppButtonModel.buttonClickedURI + openURLInWebView:button.inAppButtonModel.buttonOpenUrlInWebView]; +} + +#pragma mark - Get In-App Message + +- (ABKInAppMessageImmersive *)getInAppMessage { + return (ABKInAppMessageImmersive *)self.inAppMessage; +} + +#pragma mark - Dealloc + +- (void)dealloc { + if ([ABKUIUtils objectIsValidAndNotEmpty:[self getInAppMessage].buttons]) { + [self.leftInAppMessageButton removeTarget:self action:nil forControlEvents:UIControlEventAllEvents]; + [self.rightInAppMessageButton removeTarget:self action:nil forControlEvents:UIControlEventAllEvents]; + } +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.h new file mode 100644 index 0000000000..85406cdb1e --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.h @@ -0,0 +1,27 @@ +#import "ABKInAppMessageImmersiveViewController.h" + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageModalViewController : ABKInAppMessageImmersiveViewController + +/*! + * This boolean determines if the modal in-app message will be dismissed when the user taps outside of the + * in-app message. + * + * @discussion The default of this value is NO but can be overriden by setting the value of ABKEnableDismissModalOnOutsideTapKey in + * appboyOptions or in the Braze dictionary in your Info.plist file. + */ +@property (nonatomic, assign) BOOL enableDismissOnOutsideTap; + +/*! + * The NSLayoutConstraint that specifies the height of the part of the in-app message which houses + * the image. + */ +@property (retain, nonatomic) IBOutlet NSLayoutConstraint *iconImageHeightConstraint; + +@property (retain, nonatomic) IBOutlet NSLayoutConstraint *textsViewWidthConstraint; + +@property (strong, nonatomic) IBOutlet UIView *iconImageContainerView; +@property (strong, nonatomic) IBOutlet UIView *graphicImageContainerView; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.m new file mode 100755 index 0000000000..4eb11483a0 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.m @@ -0,0 +1,262 @@ +#import "ABKInAppMessageModalViewController.h" +#import "ABKUIUtils.h" +#import "ABKInAppMessageViewController.h" + +@import BrazeKitCompat; + +static const CGFloat ModalViewCornerRadius = 8.0f; +static const CGFloat MaxModalViewWidth = 450.0f; +static const CGFloat MinModalViewWidth = 320.0f; +static const CGFloat MaxModalViewHeight = 720.0f; + +@implementation ABKInAppMessageModalViewController + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.enableDismissOnOutsideTap = [Appboy sharedInstance].inAppMessageController.enableDismissModalOnOutsideTap; + + if (((ABKInAppMessageImmersive *)self.inAppMessage).imageStyle == ABKInAppMessageTopImage) { + [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=15)-[view]-(>=15)-|" + options:0 + metrics:nil + views:@{@"view" : self.view}]]; + [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=15)-[view]-(>=15)-|" + options:0 + metrics:nil + views:@{@"view" : self.view}]]; + } else { + @try { + UIImage *inAppImage = [[Appboy sharedInstance].imageDelegate imageFromCacheForURL:self.inAppMessage.imageURI]; + CGFloat imageAspectRatio = 1.0; + if (inAppImage != nil) { + imageAspectRatio = inAppImage.size.width / inAppImage.size.height; + } + NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.graphicImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.graphicImageView + attribute:NSLayoutAttributeHeight + multiplier:imageAspectRatio + constant:0]; + [self.graphicImageView addConstraint:constraint]; + NSArray *maxWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" + options:0 + metrics:@{@"max" : @(MaxModalViewWidth)} + views:@{@"view" : self.graphicImageView}]; + NSArray *maxHeightConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" + options:0 + metrics:@{@"max" : @(MaxModalViewHeight)} + views:@{@"view" : self.graphicImageView}]; + [self.graphicImageView addConstraints:maxWidthConstraint]; + [self.graphicImageView addConstraints:maxHeightConstraint]; + + [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-(>=15)-[view]-(>=15)-|" + options:0 + metrics:nil + views:@{@"view" : self.view}]]; + [self.view.superview addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(>=15)-[view]-(>=15)-|" + options:0 + metrics:nil + views:@{@"view" : self.view}]]; + } @catch (NSException *exception) { + NSLog(@"Braze cannot display this message because it has a height or width of 0. The graphic image has width %f and height %f and image URI %@.", + self.graphicImageView.image.size.width, self.graphicImageView.image.size.height, + self.inAppMessage.imageURI.absoluteString); + [self hideInAppMessage:NO]; + } + } +} + + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self.textsView flashScrollIndicators]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + if (![self isKindOfClass:[ABKInAppMessageModalViewController class]]) { + return; + } + + [self drawShadows]; + if (self.iconImageView) { + // Clips the top corners if the image is wide enough in the VC + CAShapeLayer * maskLayer = [CAShapeLayer layer]; + UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds + byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerTopRight) + cornerRadii:CGSizeMake(ModalViewCornerRadius, ModalViewCornerRadius)]; + maskLayer.path = maskPath.CGPath; + self.iconImageContainerView.layer.mask = maskLayer; + self.iconImageContainerView.clipsToBounds = YES; + } + + if (self.textsView && !self.textsViewWidthConstraint) { + [self addTextViewConstraints]; + } + [self.view layoutIfNeeded]; +} + +- (void)loadView { + NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageModalViewController class] channel:ABKInAppMessageChannel]; + [bundle loadNibNamed:@"ABKInAppMessageModalViewController" + owner:self + options:nil]; + self.view.layer.cornerRadius = ModalViewCornerRadius; + self.inAppMessageHeaderLabel.font = HeaderLabelDefaultFont; + self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; + + if (self.inAppMessage.message) { + NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; + NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; + [messageStyle setLineSpacing:2]; + [attributedStringMessage addAttribute:NSParagraphStyleAttributeName + value:messageStyle + range:NSMakeRange(0, self.inAppMessage.message.length)]; + self.inAppMessageMessageLabel.attributedText = attributedStringMessage; + } + if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { + if (((ABKInAppMessageImmersive *)self.inAppMessage).header) { + NSMutableAttributedString *attributedStringHeader = [[NSMutableAttributedString alloc] initWithString:((ABKInAppMessageImmersive *)self.inAppMessage).header]; + NSMutableParagraphStyle *headerStyle = [[NSMutableParagraphStyle alloc] init]; + [headerStyle setLineSpacing:2]; + [attributedStringHeader addAttribute:NSParagraphStyleAttributeName + value:headerStyle + range:NSMakeRange(0, ((ABKInAppMessageImmersive *)self.inAppMessage).header.length)]; + self.inAppMessageMessageLabel.attributedText = attributedStringHeader; + } + } +} + +#pragma mark - Private methods + +- (void)drawShadows { + UIBezierPath *dropShadowPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds + cornerRadius:self.view.layer.cornerRadius]; + self.view.layer.masksToBounds = NO; + self.view.layer.shadowOffset = CGSizeMake(0.0f, 0.0f); + self.view.layer.shadowRadius = InAppMessageShadowBlurRadius; + self.view.layer.shadowColor = [[UIColor blackColor] colorWithAlphaComponent:InAppMessageShadowOpacity].CGColor; + self.view.layer.shadowPath = dropShadowPath.CGPath; + + // Make opacity of shadow match opacity of the In-App Message background + CGFloat alpha = 0; + [self.view.backgroundColor getRed:nil green:nil blue:nil alpha:&alpha]; + self.view.layer.shadowOpacity = alpha; +} + +- (void)addTextViewConstraints { + [self.view layoutIfNeeded]; + NSLayoutConstraint *widthConstraint = [NSLayoutConstraint constraintWithItem:self.textsView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:self.textsView.contentSize.width]; + self.textsViewWidthConstraint = widthConstraint; + NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:self.textsView + attribute:NSLayoutAttributeHeight + relatedBy:NSLayoutRelationEqual + toItem:nil + attribute:NSLayoutAttributeNotAnAttribute + multiplier:1 + constant:self.textsView.contentSize.height]; + widthConstraint.priority = 999; + heightConstraint.priority = 999; + [self.textsView addConstraint:widthConstraint]; + [self.textsView addConstraint:heightConstraint]; +} + +#pragma mark - Superclass methods + +- (UIView *)bottomViewWithNoButton { + return self.textsView; +} + +- (void)setupLayoutForGraphic { + [super applyImageToImageView:self.graphicImageView]; + self.graphicImageContainerView.layer.cornerRadius = self.view.layer.cornerRadius; + + [self.iconImageView removeFromSuperview]; + [self.iconImageContainerView removeFromSuperview]; + [self.iconLabelView removeFromSuperview]; + [self.textsView removeFromSuperview]; + self.iconImageView = nil; + self.iconLabelView = nil; + self.inAppMessageHeaderLabel = nil; + self.inAppMessageMessageLabel = nil; + self.textsView = nil; +} + +- (void)setupLayoutForTopImage { + self.textsView.translatesAutoresizingMaskIntoConstraints = NO; + [self.graphicImageView removeFromSuperview]; + [self.graphicImageContainerView removeFromSuperview]; + self.graphicImageView = nil; + + // Set up the icon image/label view + if ([super applyImageToImageView:self.iconImageView]) { + [self.iconLabelView removeFromSuperview]; + self.iconLabelView = nil; + + @try { + UIImage *inAppImage = [[Appboy sharedInstance].imageDelegate imageFromCacheForURL:self.inAppMessage.imageURI]; + CGFloat imageAspectRatio = 1.0; + if (inAppImage != nil) { + imageAspectRatio = inAppImage.size.width / inAppImage.size.height; + } + NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:self.iconImageView + attribute:NSLayoutAttributeWidth + relatedBy:NSLayoutRelationEqual + toItem:self.iconImageView + attribute:NSLayoutAttributeHeight + multiplier:imageAspectRatio + constant:0]; + [self.iconImageView addConstraint:constraint]; + } @catch (NSException *exception) { + NSLog(@"Braze cannot display this message because the image has a height or width of 0. The image has width %f and height %f and image URI %@.", + self.iconImageView.image.size.width, self.iconImageView.image.size.height, + self.inAppMessage.imageURI.absoluteString); + [self hideInAppMessage:NO]; + } + } else { + self.iconImageView.hidden = YES; + self.iconImageHeightConstraint.constant = self.iconLabelView.frame.size.height + 20.0f; + + if (![super applyIconToLabelView:self.iconLabelView]) { + // When there is no image or icon, remove the iconLabelView to free up the space of the image view + [self.iconLabelView removeFromSuperview]; + self.iconLabelView = nil; + self.iconImageHeightConstraint.constant = 20.0f; + } + } + + if (![ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).header]) { + for (NSLayoutConstraint *constraint in self.inAppMessageHeaderLabel.constraints) { + if (constraint.firstAttribute == NSLayoutAttributeHeight) { + constraint.constant = 0.0f; + break; + } + } + self.headerBodySpaceConstraint.constant = 0.0f; + } + + NSArray *maxWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(<=max)]" + options:0 + metrics:@{@"max" : @(MaxModalViewWidth)} + views:@{@"view" : self.view}]; + NSArray *minWidthConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"H:[view(>=min)]" + options:0 + metrics:@{@"min" : @(MinModalViewWidth)} + views:@{@"view" : self.view}]; + NSArray *maxHeightConstraint = [NSLayoutConstraint constraintsWithVisualFormat:@"V:[view(<=max)]" + options:0 + metrics:@{@"max" : @(MaxModalViewHeight)} + views:@{@"view" : self.view}]; + [self.view addConstraints:maxWidthConstraint]; + [self.view addConstraints:minWidthConstraint]; + [self.view addConstraints:maxHeightConstraint]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.h new file mode 100644 index 0000000000..460366ceed --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.h @@ -0,0 +1,17 @@ +#import "ABKInAppMessageViewController.h" + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageSlideupViewController : ABKInAppMessageViewController + +/*! + * The UIImageView for the arrow of the in-app message. + */ +@property (weak, nonatomic, nullable) IBOutlet UIImageView *arrowImage; + +/*! + * The offset which controls the slideup in-app message vertical position once visible. + */ +@property (assign, nonatomic) CGFloat offset; + +@end +NS_ASSUME_NONNULL_END diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.m new file mode 100755 index 0000000000..0b1b605dfe --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.m @@ -0,0 +1,193 @@ +#import "ABKInAppMessageSlideupViewController.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static CGFloat const AssetSideMargin = 20.0f; +static CGFloat const DefaultViewRadius = 15.0f; +static CGFloat const DefaultVerticalMarginHeight = 10.0f; + +@interface ABKInAppMessageSlideupViewController() + +@property (strong, nonatomic) NSLayoutConstraint *slideConstraint; +@property (nonatomic, readonly) BOOL animatesFromTop; +@property (nonatomic, readonly) CGFloat safeAreaOffset; + +@end + +@implementation ABKInAppMessageSlideupViewController + +- (void)loadView { + NSBundle *bundle = [ABKUIUtils bundle:[ABKInAppMessageSlideupViewController class] channel:ABKInAppMessageChannel]; + [bundle loadNibNamed:@"ABKInAppMessageSlideupViewController" + owner:self + options:nil]; + self.inAppMessageMessageLabel.font = MessageLabelDefaultFont; + if (self.inAppMessage.message) { + NSMutableAttributedString *attributedStringMessage = [[NSMutableAttributedString alloc] initWithString:self.inAppMessage.message]; + NSMutableParagraphStyle *messageStyle = [[NSMutableParagraphStyle alloc] init]; + [messageStyle setLineSpacing:2]; + [messageStyle setLineBreakMode:NSLineBreakByTruncatingTail]; + [attributedStringMessage addAttribute:NSParagraphStyleAttributeName + value:messageStyle + range:NSMakeRange(0, self.inAppMessage.message.length)]; + self.inAppMessageMessageLabel.attributedText = attributedStringMessage; + } +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + self.view.translatesAutoresizingMaskIntoConstraints = NO; + + [self setupChevron]; + [self setupImageOrLabelView]; + + self.view.layer.cornerRadius = DefaultViewRadius; + self.view.layer.masksToBounds = NO; +} + +- (void)viewWillLayoutSubviews { + [super viewWillLayoutSubviews]; + + // Setup the constraints once UIKit has set the layoutMargins / safeAreaInsets + if (!self.slideConstraint) { + [self setupConstraintsWithSuperView]; + } +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + // Redraw the shadow when the layout is changed. + UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRoundedRect:self.view.bounds cornerRadius:DefaultViewRadius]; + self.view.layer.shadowColor = [[UIColor blackColor] colorWithAlphaComponent:InAppMessageShadowOpacity].CGColor; + self.view.layer.shadowOffset = CGSizeMake(0.0f, 0.0f); + self.view.layer.shadowRadius = InAppMessageShadowBlurRadius; + self.view.layer.shadowPath = shadowPath.CGPath; + + // Make opacity of shadow match opacity of the In-App Message background + CGFloat alpha = 0; + [self.view.backgroundColor getRed:nil green:nil blue:nil alpha:&alpha]; + self.view.layer.shadowOpacity = alpha; +} + +#pragma mark - Public methods + +- (CGFloat)offset { + return self.slideConstraint.constant - self.safeAreaOffset; +} + +- (void)setOffset:(CGFloat)offset { + self.slideConstraint.constant = offset + self.safeAreaOffset; +} + +#pragma mark - Private methods + +- (void)setupChevron { + if (((ABKInAppMessageSlideup *)self.inAppMessage).hideChevron) { + [self.arrowImage removeFromSuperview]; + self.arrowImage = nil; + NSLayoutConstraint *inAppMessageLabelTrailingConstraint = + [self.inAppMessageMessageLabel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor + constant:-AssetSideMargin]; + [self.view addConstraint:inAppMessageLabelTrailingConstraint]; + + } else { + if (((ABKInAppMessageSlideup *)self.inAppMessage).chevronColor != nil) { + UIColor *arrowColor = ((ABKInAppMessageSlideup *)self.inAppMessage).chevronColor; + self.arrowImage.image = [self.arrowImage.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.arrowImage.tintColor = arrowColor; + } else { + UIColor *defaultArrowColor = [UIColor colorWithRed:(155.0/255.0) green:(155.0/255.0) blue:(155.0/255.0) alpha:1.0]; + self.arrowImage.image = [self.arrowImage.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + self.arrowImage.tintColor = defaultArrowColor; + } + } +} + +- (void)setupImageOrLabelView { + if (![super applyImageToImageView:self.iconImageView]) { + [self.iconImageView removeFromSuperview]; + self.iconImageView = nil; + + if (![super applyIconToLabelView:self.iconLabelView]) { + [self.iconLabelView removeFromSuperview]; + self.iconLabelView = nil; + NSLayoutConstraint *inAppMessageLabelLeadingConstraint = + [self.inAppMessageMessageLabel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor + constant:AssetSideMargin]; + [self.view addConstraint:inAppMessageLabelLeadingConstraint]; + } + } +} + +- (void)setupConstraintsWithSuperView { + NSLayoutConstraint *leadConstraint = [self.view.leadingAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.leadingAnchor]; + NSLayoutConstraint *trailConstraint = [self.view.trailingAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.trailingAnchor]; + NSLayoutConstraint *offscreenConstraint; + + if (self.animatesFromTop) { + offscreenConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.topAnchor]; + self.slideConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.topAnchor + constant:self.safeAreaOffset]; + } else { + offscreenConstraint = [self.view.topAnchor constraintEqualToAnchor:self.view.superview.bottomAnchor]; + self.slideConstraint = [self.view.bottomAnchor constraintEqualToAnchor:self.view.superview.layoutMarginsGuide.bottomAnchor + constant:self.safeAreaOffset]; + } + + offscreenConstraint.priority = UILayoutPriorityDefaultLow; + [NSLayoutConstraint activateConstraints:@[leadConstraint, trailConstraint, offscreenConstraint]]; +} + +- (BOOL)animatesFromTop { + return ((ABKInAppMessageSlideup *)self.inAppMessage).inAppMessageSlideupAnchor == ABKInAppMessageSlideupFromTop; +} + +- (CGFloat)safeAreaOffset { + BOOL hasSafeArea = self.animatesFromTop + ? self.view.superview.layoutMargins.top != 0 + : self.view.superview.layoutMargins.bottom != 0; + + if (hasSafeArea) { + return 0; + } + + return self.animatesFromTop + ? DefaultVerticalMarginHeight + : -DefaultVerticalMarginHeight; +} + +#pragma mark - Superclass methods + +- (void)beforeMoveInAppMessageViewOnScreen { + self.slideConstraint.active = YES; +} + +- (void)moveInAppMessageViewOnScreen { + [self.view.superview layoutIfNeeded]; +} + +- (void)beforeMoveInAppMessageViewOffScreen { + self.slideConstraint.active = NO; +} + +- (void)moveInAppMessageViewOffScreen { + [self.view.superview layoutIfNeeded]; +} + +- (void)setInAppMessage:(ABKInAppMessage *)inAppMessage { + if ([inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { + super.inAppMessage = inAppMessage; + } else { + NSLog(@"ABKInAppMessageSlideupViewController only accepts in-app message with type ABKInAppMessageSlideup. Setting in-app message fails."); + } +} + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + if (self.inAppMessage.inAppMessageClickActionType != ABKInAppMessageNoneClickAction) { + self.view.alpha = InAppMessageSelectedOpacity; + } +} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.h new file mode 100644 index 0000000000..eb593d27a5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.h @@ -0,0 +1,123 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKInAppMessage; + +// Customize this to set the font for the in-app message message. +#define MessageLabelDefaultFont [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] + +static const CGFloat InAppMessageShadowBlurRadius = 4.0f; +static const CGFloat InAppMessageShadowOpacity = 0.3f; +static const CGFloat InAppMessageSelectedOpacity = 0.8f; + +NS_ASSUME_NONNULL_BEGIN +@interface ABKInAppMessageViewController : UIViewController + +/*! + * The in-app message that is being displayed in the view controller. + */ +@property (strong) ABKInAppMessage *inAppMessage; + +/*! + * The UIImageView for the in-app message image. + */ +@property (weak, nonatomic) IBOutlet UIImageView *iconImageView; + +/*! + * The UILabel for the in-app message icon. + */ +@property (weak, nonatomic) IBOutlet UILabel *iconLabelView; + +/*! + * The UILabel for the in-app message message. + */ +@property (weak, nonatomic) IBOutlet UILabel *inAppMessageMessageLabel; + +/*! + * This is YES if the device being used is an iPad, and NO if the device is not an iPad. + */ +@property BOOL isiPad; + +/*! + * @discussion This method is used for passing the in-app message property to any custom view + * controller. + */ +- (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage; + +/*! + * @discussion This method is used to decide whether the in-app message will be animated off the screen. + * If YES, the in-app message will animate off the screen. If NO, the in-app message will + * disappear immediately without animation. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)hideInAppMessage:(BOOL)animated; + +/* + * @discussion This method is called right before an in-app message view is going to be animated and + * removed from screen. You can use this method to change the in-app message view's + * constraints and call the `layoutIfNeeded` method in the `moveInAppMessageViewOffScreen` + * method to animate the constraint changes. + * + * For customization, please use a subclass or category to override this method. + * You must implement this method in a custom view controller. + * The default implementation of the method does nothing. + */ +- (void)beforeMoveInAppMessageViewOffScreen; + +/* + * @discussion This method is called when an in-app message view is going to be removed from the + * screen. You can use this method to control the in-app message view's + * animation by setting the off-screen position and status of the in-app message view, for + * example, by setting the alpha of the view to 0. + * + * For customization, please use a subclass or category to override this method. + * You must implement this method in a custom view controller. + * The default implementation of the method does nothing. + */ +- (void)moveInAppMessageViewOffScreen; + +/* + * @discussion This method is called right before the in-app message view is going to be animated and + * displayed on the screen. You can use this method to change the in-app message view's + * constraints and call the `layoutIfNeeded` method in the `moveInAppMessageViewOnScreen` + * method to animate the constraint changes. + * + * For customization, please use a subclass or category to override this method. + * You must implement this method in a custom view controller. + * The default implementation of the method does nothing. + */ +- (void)beforeMoveInAppMessageViewOnScreen; + +/* + * @discussion This method is called when in-app message view is going to displayed on the screen. You + * can use this method to control the in-app message view's animation by setting the on- + * screen position and status of the in-app message view, for example by moving the in-app + * message view to the center of the screen or setting the alpha of the view to 1. + * + * For customization, please use a subclass or category to override this method. + * You must implement this method in a custom view controller. + * The default implementation of the method does nothing. + */ +- (void)moveInAppMessageViewOnScreen; + +/* + * @discussion This method sets the image of the in-app message. + * + * For customization, please use a subclass or category to override this method. + */ +- (BOOL)applyImageToImageView:(UIImageView *)iconImageView; + +/* + * @discussion This method sets the icon of the in-app message. + * + * For customization, please use a subclass or category to override this method. + */ +- (BOOL)applyIconToLabelView:(UILabel *)iconLabelView; + +@end +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.m new file mode 100644 index 0000000000..85b014d790 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.m @@ -0,0 +1,159 @@ +#import +#import "ABKInAppMessageViewController.h" +#import "ABKInAppMessageView.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static const float InAppMessageIconLabelCornerRadius_iPhone = 10.0f; +static const float InAppMessageIconLabelCornerRadius_iPad = 15.0f; +static NSString *const FontAwesomeName = @"FontAwesome"; + +@implementation ABKInAppMessageViewController + +- (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage { + if (self = [super init]) { + _inAppMessage = inAppMessage; + _isiPad = [UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad; + return self; + } else { + return nil; + } +} + +#pragma mark - Lifecycle Methods + +- (void)viewDidLoad { + [super viewDidLoad]; + + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.inAppMessageMessageLabel]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [ABKInAppMessageView class]; + + // Set colors of the IAM view at display time + self.inAppMessageMessageLabel.text = self.inAppMessage.message; + self.inAppMessageMessageLabel.textAlignment = self.inAppMessage.messageTextAlignment; + if (self.inAppMessage.backgroundColor != nil) { + self.view.backgroundColor = self.inAppMessage.backgroundColor; + } + if (self.inAppMessage.textColor != nil) { + [self.inAppMessageMessageLabel setTextColor:self.inAppMessage.textColor]; + } + self.iconImageView.contentMode = self.inAppMessage.imageContentMode; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, + self.inAppMessageMessageLabel); +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, + nil); +} + +- (BOOL)prefersStatusBarHidden { + return ABKUIUtils.applicationStatusBarHidden; +} + +- (UIStatusBarStyle)preferredStatusBarStyle { + return ABKUIUtils.applicationStatusBarStyle; +} + +#pragma mark - UIViewController Methods + +// Inherit the supported orientations from the currently active application view +// controller (the one immediately under the in-app message window) +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return ABKUIUtils.activeApplicationViewController.supportedInterfaceOrientations; +} + +#pragma mark - In-app Message Initialization + +- (BOOL)applyIconToLabelView:(UILabel *)iconLabelView { + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppMessage.icon]) { + // Check if font awesome is already registered in the application. If not, register it. + // The size can be any number here. + if ([UIFont fontWithName:FontAwesomeName size:30] == nil) { + NSString *fontPath = [[ABKUIUtils bundle:[ABKInAppMessageViewController class] channel:ABKInAppMessageChannel] + pathForResource:FontAwesomeName + ofType:@"otf"]; + NSData *fontData = [NSData dataWithContentsOfFile:fontPath]; + CFErrorRef error; + CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)fontData); + CGFontRef font = CGFontCreateWithDataProvider(provider); + BOOL failedToRegisterFont = NO; + if (!CTFontManagerRegisterGraphicsFont(font, &error)) { + CFStringRef errorDescription = CFErrorCopyDescription(error); + NSLog(@"Error: Cannot load Font Awesome"); + CFBridgingRelease(errorDescription); + failedToRegisterFont = YES; + } + CFRelease(font); + CFRelease(provider); + if (failedToRegisterFont) { + return NO; + } + } + iconLabelView.font = [UIFont fontWithName:FontAwesomeName size:self.iconLabelView.font.pointSize]; + // The icon here is a Unicode string, so we use a text label instead of an image view + iconLabelView.text = self.inAppMessage.icon; + iconLabelView.textColor = self.inAppMessage.iconColor == nil ? [UIColor whiteColor] : self.inAppMessage.iconColor; + iconLabelView.backgroundColor = self.inAppMessage.iconBackgroundColor == nil ? + [UIColor colorWithRed:RedValueOfDefaultIconColorAndButtonBgColor + green:GreenValueOfDefaultIconColorAndButtonBgColor + blue:BlueValueOfDefaultIconColorAndButtonBgColor + alpha:AlphaValueOfDefaultIconColorAndButtonBgColor] + : self.inAppMessage.iconBackgroundColor; + iconLabelView.layer.cornerRadius = self.isiPad ? InAppMessageIconLabelCornerRadius_iPad : + InAppMessageIconLabelCornerRadius_iPhone; + iconLabelView.layer.masksToBounds = YES; + return YES; + } + return NO; +} + +// Here we try to find the icon image and set it to the given image view. We will first try to find if the icon image +// is one of the default Braze icon images. If not, we try to check the icon URI and download the image +// asynchronously. +// This method returns YES if we can find a default icon image, or there is a valid icon image URL. It returns NO when +// we cannot find any icon from the in-app message, and won't do anything to the given image view. +- (BOOL)applyImageToImageView:(UIImageView *)iconImageView { + if ([ABKUIUtils objectIsValidAndNotEmpty:self.inAppMessage.imageURI]) { + if ([Appboy sharedInstance].imageDelegate) { + [[Appboy sharedInstance].imageDelegate setImageForView:iconImageView + showActivityIndicator:NO + withURL:self.inAppMessage.imageURI + imagePlaceHolder:nil + completed:nil]; + return YES; + } else { + [self hideInAppMessage:NO]; + return NO; + } + } + return NO; +} + +#pragma mark - Animation + +- (void)hideInAppMessage:(BOOL)animated { + ABKInAppMessageWindowController *parentInAppMessageWindowController = (ABKInAppMessageWindowController *)self.parentViewController; + [parentInAppMessageWindowController hideInAppMessageViewWithAnimation:animated]; +} + +- (void)beforeMoveInAppMessageViewOnScreen {} + +- (void)moveInAppMessageViewOnScreen {} + +- (void)beforeMoveInAppMessageViewOffScreen {} + +- (void)moveInAppMessageViewOffScreen {} + +@end diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.h b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.h new file mode 100644 index 0000000000..ac9cc12a28 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.h @@ -0,0 +1,161 @@ +#import +#import "ABKInAppMessageUIDelegate.h" +#import "ABKInAppMessageWindowController.h" +#import "ABKInAppMessageWindow.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +typedef NS_ENUM(NSInteger, ABKInAppMessageClickActionType); +@class ABKInAppMessage; + +NS_ASSUME_NONNULL_BEGIN + +// This notification is used to let the InAppMessageUIController know that the InAppMessageWindowController +// was dismissed. +static NSString * const ABKNotificationInAppMessageWindowDismissed = @"inAppMessageWindowDismissedNotification"; + +static double const InAppMessageAnimationDuration = 0.4; + +/*! + * ABKInAppMessageWindowController is the view controller responsible for housing and displaying + * ABKInAppMessageViewControllers and performing actions after the in-app message is clicked. Instances + * of ABKInAppMessageWindowController are deallocated after the in-app message is dismissed. + * + * It will display the given in-app message view controller by animating it onto the screen, and + * dismiss it by animating it off the screen, by calling the ABKInAppMessageViewController's + * moveInAppMessageViewOffScreen: and moveInAppMessageViewOnScreen: methods, and log an impression + * of the in-app message. + * If the in-app message view controller is an instance of ABKInAppMessageSlideupViewController, + * ABKInAppMessageModalViewController, or ABKInAppMessageFullViewController, it'll also handle the + * following behaviors: + * * For ABKInAppMessageSlideupViewController: + * * set the width of the view controller based on slideup UI style and iPhone devices. + * * add a tap gesture recognizer to the in-app message view controller, and handle the clicks on it. + * * add a pan gesture recognizer to the in-app message view controller, and handle the panning on it. + * * For ABKInAppMessageModalViewController: + * * set the background color to be black with alpha 0.9. + * * move the in-app view controller to the center. + * * when the in-ap message has no buttons, add a tap gesture recognizer to the in-app message + * view controller, and handle the clicks on it. + * * block the clicks outside of the in-app message view. + * * For ABKInAppMessageFullViewController: + * * set the in-app message's frame to be full screen. + * * when the in-app message has no buttons, add a tap gesture recognizer to the in-app message + * view controller, and handle the clicks on it. + * + * Additionally, the view controller is responsible for executing that in-app message's specified + * behavior on click or performing a "custom action", which can be specified through a delegate for + * the in-app message. + * + * After the in-app message is dismissed, ABKInAppMessageWindowController will set the inAppMessageWindow + * property to nil, and inform ABKInAppMessageUIController to set it's windowController property to + * nil as well. At that point, the in-app message window's retainer count will drop to 0 and the + * system will clean it out from the UIApplication's windows array. + */ +@interface ABKInAppMessageWindowController : UIViewController + +/*! + * The UI window used to display the in-app message. + */ +@property (nonatomic, nullable) IBOutlet ABKInAppMessageWindow *inAppMessageWindow; + +/*! + * The timer used to know when to slide the in-app message off the screen. + */ +@property (nullable) NSTimer *slideAwayTimer; + +/*! + * The in-app message that is being displayed. + */ +@property ABKInAppMessage *inAppMessage; + +/*! + * The optional ABKInAppMessageUIDelegate that can be used to customize display and behavior of the + * in-app message. + */ +@property (weak, nullable) id inAppMessageUIDelegate; + +/*! + * The view controller used to display the in-app message. + */ +@property ABKInAppMessageViewController *inAppMessageViewController; + +/*! + * Properties used to properly place the slideup in-app messages with pan gestures. + */ +@property CGFloat slideupConstraintMaxValue; +@property CGPoint inAppMessagePreviousPanPosition; + +/*! + * The orientation mask that the in-app message supports. + * The default value is UIInterfaceOrientationMaskAll + */ +@property UIInterfaceOrientationMask supportedOrientationMask; + +/*! + * The preferred orientation for in-app message display. + * The default is unknown, which means the orientation would be set as Status Bar current orientation. + */ +@property UIInterfaceOrientation preferredOrientation; + +/*! + * The variable that shows if the device is being rotated. + */ +@property BOOL isInRotation; + +/*! + * The variable that shows if the in-app message has been clicked. + */ +@property BOOL inAppMessageIsTapped; + +/*! + * The ID of a button that has been clicked. + */ +@property NSInteger clickedButtonId; + +/*! + * The ID of an HTML button that has been clicked. + */ +@property (nullable) NSString *clickedHTMLButtonId; + +- (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage + inAppMessageViewController:(ABKInAppMessageViewController *)inAppMessageViewController + inAppMessageDelegate:(id)delegate; +/*! + * @discussion This method is called when the keyboard is shown when an in-app message is being displayed. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)keyboardWasShown; + +/*! + * @discussion This method is called to display the in-app message. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)displayInAppMessageViewWithAnimation:(BOOL)withAnimation; + +/*! + * @discussion These methods are called to hide the in-app message. + * + * For customization, please use a subclass or category to override one of these methods. + */ +- (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation; +- (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation + completionHandler:(void (^ __nullable)(void))completionHandler; + +/*! + * @discussion This method is called when an in-app message button is clicked. + * + * For customization, please use a subclass or category to override this method. + */ +- (void)inAppMessageClickedWithActionType:(ABKInAppMessageClickActionType)actionType + URL:(nullable NSURL *)url + openURLInWebView:(BOOL)openUrlInWebView; + +NS_ASSUME_NONNULL_END + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.m b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.m new file mode 100644 index 0000000000..ec3c9070b1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.m @@ -0,0 +1,503 @@ +#import "ABKInAppMessageWindowController.h" +#import "ABKInAppMessageWindow.h" +#import "ABKInAppMessageView.h" +#import "ABKInAppMessageHTMLBaseViewController.h" +#import "ABKInAppMessageImmersiveViewController.h" +#import "ABKInAppMessageSlideupViewController.h" +#import "ABKInAppMessageModalViewController.h" +#import "ABKInAppMessageViewController.h" +#import "ABKUIURLUtils.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +static CGFloat const MinimumInAppMessageDismissVelocity = 20.0; +static CGFloat const SlideUpDragResistanceFactor = 0.055; +static NSInteger const KeyWindowRetryMaxCount = 10; + +@interface ABKInAppMessageWindowController () + +@property (nonatomic, assign) NSInteger keyWindowRetryCount; + +@property (nonatomic, assign) BOOL isRemovingWindow; + +@end + +@implementation ABKInAppMessageWindowController + +- (instancetype)initWithInAppMessage:(ABKInAppMessage *)inAppMessage + inAppMessageViewController:(ABKInAppMessageViewController *)inAppMessageViewController + inAppMessageDelegate:(id)delegate { + if (self = [super init]) { + _inAppMessage = inAppMessage; + _inAppMessageViewController = inAppMessageViewController; + _inAppMessageUIDelegate = (id)delegate; + + _inAppMessageWindow = [self createInAppMessageWindow]; + _inAppMessageWindow.backgroundColor = [UIColor clearColor]; + _inAppMessageWindow.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleRightMargin; + _inAppMessageIsTapped = NO; + _clickedButtonId = -1; + _keyWindowRetryCount = 0; + _isRemovingWindow = NO; + } + return self; +} + +#pragma mark - Lifecycle Methods + +- (void)viewDidLoad { + [super viewDidLoad]; + [self addChildViewController:self.inAppMessageViewController]; + [self.inAppMessageViewController didMoveToParentViewController:self]; + self.view.backgroundColor = [UIColor clearColor]; + + if ([self.inAppMessage isKindOfClass:[ABKInAppMessageSlideup class]]) { + + // Note: this gestureRecognizer won't catch taps which occur during the animation. + UITapGestureRecognizer *inAppSlideupTapGesture = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(inAppMessageTapped:)]; + [self.inAppMessageViewController.view addGestureRecognizer:inAppSlideupTapGesture]; + UIPanGestureRecognizer *inAppSlideupPanGesture = [[UIPanGestureRecognizer alloc] + initWithTarget:self + action:@selector(inAppSlideupWasPanned:)]; + [self.inAppMessageViewController.view addGestureRecognizer:inAppSlideupPanGesture]; + // We want to detect the pan gesture first, so we only recognize a tap when the pan recognizer fails. + [inAppSlideupTapGesture requireGestureRecognizerToFail:inAppSlideupPanGesture]; + } else if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]]) { + UITapGestureRecognizer *inAppImmersiveInsideTapGesture = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(inAppMessageTapped:)]; + [self.inAppMessageViewController.view addGestureRecognizer:inAppImmersiveInsideTapGesture]; + + if ([self.inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { + self.inAppMessageWindow.handleAllTouchEvents = YES; + UITapGestureRecognizer *inAppModalOutsideTapGesture = [[UITapGestureRecognizer alloc] + initWithTarget:self + action:@selector(inAppMessageTappedOutside:)]; + [self.view addGestureRecognizer:inAppModalOutsideTapGesture]; + } + } + + if ([self.inAppMessageViewController isKindOfClass:[ABKInAppMessageImmersiveViewController class]] || + [self.inAppMessageViewController isKindOfClass:[ABKInAppMessageHTMLBaseViewController class]]) { + self.inAppMessageWindow.accessibilityViewIsModal = YES; + } + + [self.view addSubview:self.inAppMessageViewController.view]; +} + +- (UIViewController *)childViewControllerForStatusBarHidden { + return self.inAppMessageViewController; +} + +- (UIViewController *)childViewControllerForStatusBarStyle { + return self.inAppMessageViewController; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + // When the in-app message first become visible, monitor windows changes in the view hierarchy to + // ensure that the in-app message stays visible. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(handleWindowDidBecomeKeyNotification:) + name:UIWindowDidBecomeKeyNotification + object:nil]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + + [[NSNotificationCenter defaultCenter] removeObserver:self + name:UIWindowDidBecomeKeyNotification + object:nil]; +} + +#pragma mark - Rotation + +- (UIInterfaceOrientationMask)supportedInterfaceOrientations { + return self.supportedOrientationMask; +} + +- (BOOL)shouldAutorotate { + if ([UIDevice currentDevice].userInterfaceIdiom != UIUserInterfaceIdiomPad && + self.inAppMessage.orientation != ABKInAppMessageOrientationAny && + !self.inAppMessageWindow.hidden) { + return NO; + } else { + return YES; + } +} + +- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { + if (self.preferredOrientation != UIInterfaceOrientationUnknown) { + return self.preferredOrientation; + } + return [ABKUIUtils getInterfaceOrientation]; +} + +#pragma mark - Gesture Recognizers + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + return ![touch.view isKindOfClass:[ABKInAppMessageView class]]; +} + +- (void)inAppSlideupWasPanned:(UIPanGestureRecognizer *)panGestureRecognizer { + ABKInAppMessageSlideupViewController *slideupVC = (ABKInAppMessageSlideupViewController *)self.inAppMessageViewController; + BOOL animatesFromTop = ((ABKInAppMessageSlideup *)self.inAppMessage).inAppMessageSlideupAnchor == ABKInAppMessageSlideupFromTop; + CGFloat offset = [panGestureRecognizer translationInView:self.view].y; + CGFloat velocity = [panGestureRecognizer velocityInView:self.view].y; + + switch (panGestureRecognizer.state) { + case UIGestureRecognizerStateChanged: { + if (animatesFromTop) { + slideupVC.offset = offset <= 0 ? offset : (SlideUpDragResistanceFactor * offset); + } else { + slideupVC.offset = offset >= 0 ? offset : (SlideUpDragResistanceFactor * offset); + } + break; + } + + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: { + // Reset position + if ((animatesFromTop && slideupVC.offset > 0) || + (!animatesFromTop && slideupVC.offset < 0) || + (fabs(velocity) < MinimumInAppMessageDismissVelocity && fabs(offset) < 16)) { + slideupVC.offset = 0; + [UIView animateWithDuration:0.2 animations:^{ + [self.view layoutIfNeeded]; + }]; + return; + } + + // Dismiss + [self invalidateSlideAwayTimer]; + + if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { + [self.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; + } + + [slideupVC beforeMoveInAppMessageViewOffScreen]; + [UIView animateWithDuration:0.2 + animations:^{ + [slideupVC moveInAppMessageViewOffScreen]; + } + completion:^(BOOL finished) { + if (finished) { + [self hideInAppMessageWindow]; + } + }]; + break; + } + + default: + break; + } +} + +- (void)inAppMessageTapped:(id)sender { + if ([self.inAppMessage isKindOfClass:[ABKInAppMessageImmersive class]] && + [ABKUIUtils objectIsValidAndNotEmpty:((ABKInAppMessageImmersive *)self.inAppMessage).buttons]) { + return; + } + [self invalidateSlideAwayTimer]; + self.inAppMessageIsTapped = YES; + + if (![self delegateHandlesInAppMessageClick]) { + [self inAppMessageClickedWithActionType:self.inAppMessage.inAppMessageClickActionType + URL:self.inAppMessage.uri + openURLInWebView:self.inAppMessage.openUrlInWebView]; + } +} + +- (void)inAppMessageTappedOutside:(id)sender { + if (![self.inAppMessage isKindOfClass:[ABKInAppMessageModal class]]) { + return; + } + if ([self.inAppMessageViewController isKindOfClass:ABKInAppMessageModalViewController.class]) { + ABKInAppMessageModalViewController *viewController = (ABKInAppMessageModalViewController *)self.inAppMessageViewController; + if (viewController.enableDismissOnOutsideTap) { + [viewController dismissInAppMessage:self.inAppMessage]; + } + } +} + +#pragma mark - Timer + +- (void)invalidateSlideAwayTimer { + if (self.slideAwayTimer != nil) { + [self.slideAwayTimer invalidate]; + self.slideAwayTimer = nil; + } +} + +- (void)inAppMessageTimerFired:(NSTimer *)timer { + if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageDismissed:)]) { + [self.inAppMessageUIDelegate onInAppMessageDismissed:self.inAppMessage]; + } + [self hideInAppMessageViewWithAnimation:self.inAppMessage.animateOut]; +} + +#pragma mark - Keyboard + +- (void)keyboardWasShown { + if (![self.inAppMessageViewController isKindOfClass:[ABKInAppMessageHTMLBaseViewController class]] + && !self.inAppMessageWindow.hidden) { + // If the keyboard is shown while an in-app message is on the screen, we hide the in-app message + [self hideInAppMessageWindow]; + } +} + +#pragma mark - Windows + +- (void)resetKeyWindowRetryCount { + self.keyWindowRetryCount = 0; +} + +/*! + * React to windows changes in the view hierarchy. This is needed to ensure that the in-app message + * stays visible in cases where the host app decides to display a window (possibly the app's main + * window) over our in-app message. + * + * This method tries to make the in-app message window visible up to 10 times — debounced with a + * 0.1s timeout. The in-app message is dismissed when reaching that value to prevent infinite loops + * when another window in the view hierarchy has a similar behavior. + * + * e.g. Some clients have extra logic when bootstrapping their app that can lead to the app's main + * window being made key and visible after a delay at startup. In the case of test in-app messages + * delivered via push notifications, our in-app messages would be displayed before the host app + * window being made key and visible. Soon after, the host app window takes over and hides our + * in-app message. + */ +- (void)handleWindowDidBecomeKeyNotification:(NSNotification *)notification { + UIWindow *window = notification.object; + + // Cancel debounced reset + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(resetKeyWindowRetryCount) + object:nil]; + + // Skip if this in-app message is already removing the window + if (self.isRemovingWindow) { + return; + } + // Skip for any in-app message window + if ([window isKindOfClass:[ABKInAppMessageWindow class]]) { + return; + } + // Skip if the new key window is meant to be displayed above the in-app message (alert, sheet, + // host app toast) + if (window.windowLevel > UIWindowLevelNormal) { + return; + } + + // Dismiss in-app message if we can't guarantee its visibility. + self.keyWindowRetryCount += 1; + if (self.keyWindowRetryCount >= KeyWindowRetryMaxCount) { + NSLog(@"Error: Failed to make in-app message window key and visible %ld times, dismissing the in-app message.", (long)self.keyWindowRetryCount); + [self hideInAppMessageViewWithAnimation:YES]; + return; + } + + // Force in-app message window to be displayed + [self.inAppMessageWindow makeKeyAndVisible]; + + // Debounced reset, use NSRunLoopCommonModes as NSDefaultRunLoopMode does not update during + // scroll events. + [self performSelector:@selector(resetKeyWindowRetryCount) + withObject:nil + afterDelay:0.1 + inModes:@[NSRunLoopCommonModes]]; +} + +#pragma mark - Display and Hide In-app Message + +- (void)displayInAppMessageViewWithAnimation:(BOOL)withAnimation { + dispatch_async(dispatch_get_main_queue(), ^{ + // Set the root view controller after the inAppMessagewindow becomes the key window so it gets the + // correct window size during and after rotation. + self.keyWindowRetryCount = 0; + [self.inAppMessageWindow makeKeyWindow]; + self.inAppMessageWindow.rootViewController = self; + self.inAppMessageWindow.hidden = NO; + + if (self.inAppMessage.inAppMessageDismissType == ABKInAppMessageDismissAutomatically) { + self.slideAwayTimer = [NSTimer scheduledTimerWithTimeInterval:self.inAppMessage.duration + InAppMessageAnimationDuration + target:self + selector:@selector(inAppMessageTimerFired:) + userInfo:nil repeats:NO]; + } + [self.view layoutIfNeeded]; + [self.inAppMessageViewController beforeMoveInAppMessageViewOnScreen]; + if (withAnimation) { + [UIView animateWithDuration:InAppMessageAnimationDuration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + [self.inAppMessageViewController moveInAppMessageViewOnScreen]; + } + completion:^(BOOL finished){ + [self.inAppMessage logInAppMessageImpression]; + }]; + } else { + [self.inAppMessageViewController moveInAppMessageViewOnScreen]; + [self.inAppMessage logInAppMessageImpression]; + } + }); +} + +- (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation { + [self hideInAppMessageViewWithAnimation:withAnimation completionHandler:nil]; +} + +- (void)hideInAppMessageViewWithAnimation:(BOOL)withAnimation + completionHandler:(void (^ __nullable)(void))completionHandler { + [self.slideAwayTimer invalidate]; + self.slideAwayTimer = nil; + [self.view layoutIfNeeded]; + [self.inAppMessageViewController beforeMoveInAppMessageViewOffScreen]; + if (withAnimation) { + [UIView animateWithDuration:InAppMessageAnimationDuration + delay:0 + options:UIViewAnimationOptionBeginFromCurrentState + animations:^{ + [self.inAppMessageViewController moveInAppMessageViewOffScreen]; + } + completion:^(BOOL finished){ + if (completionHandler) { + completionHandler(); + } + [self hideInAppMessageWindow]; + }]; + } else { + [self.inAppMessageViewController moveInAppMessageViewOffScreen]; + [self hideInAppMessageWindow]; + } +} + +- (void)hideInAppMessageWindow { + if (self.isRemovingWindow) { + return; + } + self.isRemovingWindow = YES; + + [self.slideAwayTimer invalidate]; + self.slideAwayTimer = nil; + + self.inAppMessageWindow.rootViewController = nil; + if (@available(iOS 13.0, *)) { + self.inAppMessageWindow.windowScene = nil; + } + self.inAppMessageWindow = nil; + [[NSNotificationCenter defaultCenter] postNotificationName:ABKNotificationInAppMessageWindowDismissed + object:self + userInfo:nil]; + if (self.clickedButtonId >= 0) { + [(ABKInAppMessageImmersive *)self.inAppMessage logInAppMessageClickedWithButtonID:self.clickedButtonId]; + } else if (self.inAppMessageIsTapped) { + [self.inAppMessage logInAppMessageClicked]; + } else if ([ABKUIUtils objectIsValidAndNotEmpty:self.clickedHTMLButtonId]) { + [(ABKInAppMessageHTMLBase *)self.inAppMessage logInAppMessageHTMLClickWithButtonID:self.clickedHTMLButtonId]; + } +} + +#pragma mark - In-app Message and Button Clicks + +- (BOOL)delegateHandlesInAppMessageClick { + if ([self.inAppMessageUIDelegate respondsToSelector:@selector(onInAppMessageClicked:)]) { + if ([self.inAppMessageUIDelegate onInAppMessageClicked:self.inAppMessage]) { + NSLog(@"No in-app message click action will be performed by Braze as inAppMessageDelegate %@ returned YES in onInAppMessageClicked:", self.inAppMessageUIDelegate); + return YES; + } + } + return NO; +} + +- (void)inAppMessageClickedWithActionType:(ABKInAppMessageClickActionType)actionType + URL:(NSURL *)url + openURLInWebView:(BOOL)openUrlInWebView { + [self invalidateSlideAwayTimer]; + switch (actionType) { + case ABKInAppMessageNoneClickAction: + break; + case ABKInAppMessageDisplayNewsFeed: + [self displayModalFeedView]; + break; + case ABKInAppMessageRedirectToURI: + if ([ABKUIUtils objectIsValidAndNotEmpty:url]) { + [self handleInAppMessageURL:url inWebView:openUrlInWebView]; + } + break; + } + [self hideInAppMessageViewWithAnimation:self.inAppMessage.animateOut]; +} + +#pragma mark - Display News Feed + +- (void)displayModalFeedView { + Class ModalFeedViewControllerClass = [ABKUIUtils getModalFeedViewControllerClass]; + if (ModalFeedViewControllerClass != nil) { + UIViewController *topmostViewController = + [ABKUIURLUtils topmostViewControllerWithRootViewController:ABKUIUtils.activeApplicationViewController]; + [topmostViewController presentViewController:[[ModalFeedViewControllerClass alloc] init] + animated:YES + completion:nil]; + } +} + +#pragma mark - URL Handling + +- (void)handleInAppMessageURL:(NSURL *)url inWebView:(BOOL)openUrlInWebView { + // URL Delegate + if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate + handlesURL:url + fromChannel:ABKInAppMessageChannel + withExtras:self.inAppMessage.extras]) { + return; + } + + // WebView + if ([ABKUIURLUtils URL:url shouldOpenInWebView:openUrlInWebView]) { + UIViewController *topmostViewController = + [ABKUIURLUtils topmostViewControllerWithRootViewController:ABKUIUtils.activeApplicationViewController]; + [ABKUIURLUtils displayModalWebViewWithURL:url topmostViewController:topmostViewController]; + return; + } + + // System + [ABKUIURLUtils openURLWithSystem:url]; +} + +#pragma mark - Helpers + +/*! + * Creates and setups the ABKInAppMessageWindow used to display the in-app message + * + * @discussion First tries to create the window with the current UIWindowScene if available, then fallbacks + * to create the window with a frame. + */ +- (ABKInAppMessageWindow *)createInAppMessageWindow { + ABKInAppMessageWindow *window; + + if (@available(iOS 13.0, *)) { + UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; + if (windowScene) { + window = [[ABKInAppMessageWindow alloc] initWithWindowScene:windowScene]; + } + } + + if (!window) { + window = [[ABKInAppMessageWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + } + + window.backgroundColor = UIColor.clearColor; + window.autoresizingMask = UIViewAutoresizingFlexibleWidth | + UIViewAutoresizingFlexibleHeight; + + return window; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/AppboyNewsFeed.h b/Sources/BrazeUICompat/ABKNewsFeed/AppboyNewsFeed.h new file mode 100644 index 0000000000..f0a14a5f89 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/AppboyNewsFeed.h @@ -0,0 +1,10 @@ +// Braze News Feed View Controllers +#import "ABKFeedWebViewController.h" +#import "ABKNewsFeedTableViewController.h" +#import "ABKNewsFeedViewController.h" + +// Braze News Feed Cells +#import "ABKNFBannerCardCell.h" +#import "ABKNFBaseCardCell.h" +#import "ABKNFCaptionedMessageCardCell.h" +#import "ABKNFClassicCardCell.h" diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/Base.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/Base.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..feb18cf786 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/Base.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Done"; +"Appboy.feed.no-card.text" = "We have no updates.\nPlease check again later."; +"Appboy.feed.no-connection.title" = "Connection Error"; +"Appboy.feed.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/ar.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ar.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..7f018fa533 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ar.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "تم"; +"Appboy.feed.no-card.text" = "ليس لدينا أي تحديث. يرجى التحقق مرة أخرى لاحقاً."; +"Appboy.feed.no-connection.title" = "خلل في الاتصال"; +"Appboy.feed.no-connection.message" = "لا يمكن إجراء الاتصال بالشبكة. يرجى تكرار المحاولة لاحقا. "; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/cs.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/cs.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..ec2d783353 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/cs.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Hotovo"; +"Appboy.feed.no-card.text" = "Nemáme žádné aktualizace.\nZkontrolujte prosím znovu později."; +"Appboy.feed.no-connection.title" = "Chyba připojení"; +"Appboy.feed.no-connection.message" = "Nelze navázat síťové připojení.\nProsím zkuste to znovu později."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/da.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/da.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..797d7f0f39 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/da.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Afsluttet"; +"Appboy.feed.no-card.text" = "Vi har ingen updates. Prøv venligst senere"; +"Appboy.feed.no-connection.title" = "Netværksfejl"; +"Appboy.feed.no-connection.message" = "Kan ikke etablere netværksforbindelse. Prøv venligst senere."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/de.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/de.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..0e93df5eee --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/de.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Fertig"; +"Appboy.feed.no-card.text" = "Derzeit sind keine Updates verfügbar.\nBitte später noch einmal versuchen."; +"Appboy.feed.no-connection.title" = "Verbindungsfehler"; +"Appboy.feed.no-connection.message" = "Netzwerkverbindung kann nicht aufgebaut werden.\nBitte später noch einmal versuchen."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/en.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/en.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..feb18cf786 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/en.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Done"; +"Appboy.feed.no-card.text" = "We have no updates.\nPlease check again later."; +"Appboy.feed.no-connection.title" = "Connection Error"; +"Appboy.feed.no-connection.message" = "Cannot establish network connection.\nPlease try again later."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-419.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-419.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..6a8a62b24d --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-419.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Listo"; +"Appboy.feed.no-card.text" = "No tenemos ninguna actualización. Vuelva a verificar más tarde."; +"Appboy.feed.no-connection.title" = "Error de conexión"; +"Appboy.feed.no-connection.message" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-MX.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-MX.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..6a8a62b24d --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es-MX.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Listo"; +"Appboy.feed.no-card.text" = "No tenemos ninguna actualización. Vuelva a verificar más tarde."; +"Appboy.feed.no-connection.title" = "Error de conexión"; +"Appboy.feed.no-connection.message" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/es.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..da700291f9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/es.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Finalizado"; +"Appboy.feed.no-card.text" = "No tenemos actualizaciones. Por favor compruébelo más tarde."; +"Appboy.feed.no-connection.title" = "Error de conexión"; +"Appboy.feed.no-connection.message" = "No se puede establecer conexión de red. Por favor inténtelo más tarde."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/et.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/et.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..e20203502b --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/et.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Valmis"; +"Appboy.feed.no-card.text" = "Uuendusi pole praegu saadaval. Proovige hiljem uuesti."; +"Appboy.feed.no-connection.title" = "Üheduse viga"; +"Appboy.feed.no-connection.message" = "Võrguühenduse loomine ebaõnnestus. Proovige hiljem uuesti."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/fi.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fi.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..c1cd6c10f7 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fi.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Valmis"; +"Appboy.feed.no-card.text" = "Päivityksiä ei ole saatavilla. Tarkista myöhemmin uudelleen."; +"Appboy.feed.no-connection.title" = "Yhteysvirhe"; +"Appboy.feed.no-connection.message" = "Verkkoyhteyttä ei voida luoda. Yritä myöhemmin uudelleen."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/fil.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fil.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..fccfb7bbbd --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fil.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Gawa na"; +"Appboy.feed.no-card.text" = "Wala kaming mga update. Mangyaring suriin muli sa ibang pagkakataon."; +"Appboy.feed.no-connection.title" = "May Error sa Koneksyon"; +"Appboy.feed.no-connection.message" = "Hindi makapagtatag ng koneksyon sa network. Mangyaring subukan muli mamaya."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/fr.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fr.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..515618c802 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/fr.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Fini"; +"Appboy.feed.no-card.text" = "Aucune mise à jour disponible. Veuillez vérifier ultérieurement."; +"Appboy.feed.no-connection.title" = "Erreur de connexion."; +"Appboy.feed.no-connection.message" = "Impossible d'établir la connexion réseau. Veuillez réessayer ultérieurement."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/he.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/he.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..6893ae81f1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/he.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "סיום"; +"Appboy.feed.no-card.text" = ".אין לנו עדכונים. בבקשה בדוק שוב בקרוב"; +"Appboy.feed.no-connection.title" = "שגיאת חיבור רשת"; +"Appboy.feed.no-connection.message" = ".לא ניתן לקבוע חיבור רשת. בבקשה נסה שוב בקרוב"; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/hi.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/hi.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..432540c81b --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/hi.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "कर दिया गया"; +"Appboy.feed.no-card.text" = "हमारे पास कोई अपडेट नहीं हैं। कृपया बाद में फिर से जाँच करें.।"; +"Appboy.feed.no-connection.title" = "कनेक्शन की त्रुटि"; +"Appboy.feed.no-connection.message" = "नेटवर्क कनेक्शन स्थापित नहीं हो रहा है। कृपया बाद में दोबारा प्रयास करें।."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/id.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/id.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..7fefb85354 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/id.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Selesai"; +"Appboy.feed.no-card.text" = "Kami tidak memiliki pembaruan. Coba lagi nanti."; +"Appboy.feed.no-connection.title" = "Kesalahan Koneksi"; +"Appboy.feed.no-connection.message" = "Tidak bisa melakukan koneksi jaringan. Coba lagi nanti."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read.png new file mode 100644 index 0000000000..619b5a0f30 Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read@2x.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read@2x.png new file mode 100644 index 0000000000..27ab7a68c1 Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Read@2x.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread.png new file mode 100644 index 0000000000..5581c1516a Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread@2x.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread@2x.png new file mode 100644 index 0000000000..d03884291f Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/Icons_Unread@2x.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg.png new file mode 100644 index 0000000000..0968979999 Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg@2x.png b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg@2x.png new file mode 100644 index 0000000000..7d34f620ef Binary files /dev/null and b/Sources/BrazeUICompat/ABKNewsFeed/Resources/images/img-noimage-lrg@2x.png differ diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/it.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/it.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..ff604348a2 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/it.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Fatto"; +"Appboy.feed.no-card.text" = "Non ci sono aggiornamenti. Ricontrollare più tardi."; +"Appboy.feed.no-connection.title" = "Errore di connessione"; +"Appboy.feed.no-connection.message" = "Impossibile stabilire una connessione di rete. Riprovare più tardi."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/ja.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ja.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..34586b7223 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ja.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完了"; +"Appboy.feed.no-card.text" = "アップデートはありません。後でもう一度確認してください。"; +"Appboy.feed.no-connection.title" = "接続エラー"; +"Appboy.feed.no-connection.message" = "ネットワークに接続できません。後でもう一度試してください。"; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/km.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/km.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..6e24106da5 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/km.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "បានសម្រេច"; +"Appboy.feed.no-card.text" = "យើងមិនមានការធ្វើបច្ចុប្បន្នភាពទេ។ សូមពិនិត្យមើលម្តងទៀតនៅពេលក្រោយ."; +"Appboy.feed.no-connection.title" = "កំហុសឆ្គងក្នុងការតភ្ជាប់"; +"Appboy.feed.no-connection.message" = "មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ។ សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/ko.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ko.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..29e0536a96 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ko.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "완료 "; +"Appboy.feed.no-card.text" = "업데이트가 없습니다. 다음에 다시 확인해 주십시오."; +"Appboy.feed.no-connection.title" = "연결 오류"; +"Appboy.feed.no-connection.message" = "네트워크 연결을 할 수 없습니다. 나중에 다시 시도해 주십시오."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/lo.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/lo.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..3da8d5a9d0 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/lo.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "ສຳ​ເລັດ"; +"Appboy.feed.no-card.text" = "ພວກ​ເຮົາ​ບໍ່​ມີ​ການ​ອັບ​ເດດ. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; +"Appboy.feed.no-connection.title" = "ການ​ເຊື່ອມ​ຕໍ່​ຜິດ​ພາດ"; +"Appboy.feed.no-connection.message" = "ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/ms.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ms.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..4579f4ce86 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ms.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Selesai"; +"Appboy.feed.no-card.text" = "Tiada kemas kini. Sila periksa kemudian."; +"Appboy.feed.no-connection.title" = "Ralat Sambungan"; +"Appboy.feed.no-connection.message" = "Tidak boleh membuat sambungan rangkaian. Sila cuba kemudian."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/my.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/my.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..c913a09319 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/my.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "ျပီးျပီ"; +"Appboy.feed.no-card.text" = "ကၽႊႏု္ပ္ တို႕တြင္ အသစ္တင္ျပရန္မရွိပါ။ ေက်းဇူးျပဳ၍ ေနာင္တြင္ ထပ္စစ္ပါ။ ."; +"Appboy.feed.no-connection.title" = "ဆက္သြယ္ေရး အမွား"; +"Appboy.feed.no-connection.message" = "ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။ ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/nb.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/nb.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..ec293b9fe9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/nb.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Ferdig"; +"Appboy.feed.no-card.text" = "Vi har ingen oppdateringer. Vennligst sjekk igjen senere."; +"Appboy.feed.no-connection.title" = "Tilkoblingsfeil"; +"Appboy.feed.no-connection.message" = "Kan ikke etablere nettverkstilkobling. Vennligst prøv igjen senere."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/nl.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/nl.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..37a3b1f5c1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/nl.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Gereed"; +"Appboy.feed.no-card.text" = "Er zijn geen updates. Probeer het later opnieuw."; +"Appboy.feed.no-connection.title" = "Verbindingsfout"; +"Appboy.feed.no-connection.message" = "Kan geen netwerkverbinding maken. Probeer het later opnieuw."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/pl.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pl.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..96b9209df1 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pl.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Gotowe"; +"Appboy.feed.no-card.text" = "Brak aktualizacji. Proszę sprawdzić ponownie później."; +"Appboy.feed.no-connection.title" = "Błąd połączenia"; +"Appboy.feed.no-connection.message" = "Nie można ustanowić połączenia z siecią. Proszę spróbować ponownie później."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt-PT.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt-PT.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..39518e4b0f --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt-PT.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Concluído"; +"Appboy.feed.no-card.text" = "Não temos atualizações. Por favor, verifique mais tarde."; +"Appboy.feed.no-connection.title" = "Erro de Ligação"; +"Appboy.feed.no-connection.message" = "Não é possível estabelecer a ligação à rede. Por favor, tente mais tarde."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..cf7a4cd571 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/pt.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Concluído"; +"Appboy.feed.no-card.text" = "Não temos nenhuma atualização.\nVerifique novamente mais tarde."; +"Appboy.feed.no-connection.title" = "Erro de conexão"; +"Appboy.feed.no-connection.message" = "Não foi possível estabelecer uma\nconexão. Tente novamente mais tarde."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/ru.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ru.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..3c8968ff42 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/ru.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Готово"; +"Appboy.feed.no-card.text" = "Обновления недоступны. Пожалуйста, проверьте снова позже."; +"Appboy.feed.no-connection.title" = "Ошибка подключения"; +"Appboy.feed.no-connection.message" = "Невозможно установить сетевое подключение. Пожалуйста, повторите попытку позже."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/sv.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/sv.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..47bdbd18f3 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/sv.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Klar"; +"Appboy.feed.no-card.text" = "Det finns inga uppdateringar. Försök igen senare."; +"Appboy.feed.no-connection.title" = "Anslutningsfel"; +"Appboy.feed.no-connection.message" = "Det gick inte att skapa en nätverksanslutning. Försök igen senare."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/th.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/th.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..2dff8f276e --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/th.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "เสร็จสิ้น"; +"Appboy.feed.no-card.text" = "เราไม่มีการอัพเดต กรุณาตรวจสอบภายหลัง."; +"Appboy.feed.no-connection.title" = "ผิดพลาดการเชื่อมต่อ"; +"Appboy.feed.no-connection.message" = "ไม่สามารถสร้างการเชื่อมต่อเครือข่าย กรุณาลองใหม่ภายหลัง."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/uk.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/uk.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..8f8a42b1d6 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/uk.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Готово"; +"Appboy.feed.no-card.text" = "Оновлення недоступні.\nБудь ласка, перевірте знову пізніше."; +"Appboy.feed.no-connection.title" = "Помилка підключення"; +"Appboy.feed.no-connection.message" = "неможливо встановити з'єднання з мережею.\nБудь ласка, спробуйте ще раз пізніше."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/vi.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/vi.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..a5dda49b48 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/vi.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "Hoàn tất"; +"Appboy.feed.no-card.text" = "Chúng tôi không có cập nhật nào. Vui lòng kiểm tra lại sau."; +"Appboy.feed.no-connection.title" = "Lỗi Kết Nối"; +"Appboy.feed.no-connection.message" = "Không thể thiết lập kết nối mạng. Vui lòng thử lại sau."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-HK.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-HK.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..9543d72cd2 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-HK.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完成"; +"Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.feed.no-connection.title" = "連線錯誤"; +"Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hans.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hans.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..614fa17a49 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hans.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完成"; +"Appboy.feed.no-card.text" = "暂时没有更新.\n请稍后再试."; +"Appboy.feed.no-connection.title" = "连接错误"; +"Appboy.feed.no-connection.message" = "无法建立网络连接.\n请稍候再试."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hant.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hant.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..9543d72cd2 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-Hant.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完成"; +"Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.feed.no-connection.title" = "連線錯誤"; +"Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-TW.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-TW.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..9543d72cd2 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh-TW.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完成"; +"Appboy.feed.no-card.text" = "暫時沒有更新.\n請稍候再試."; +"Appboy.feed.no-connection.title" = "連線錯誤"; +"Appboy.feed.no-connection.message" = "無法建立網路連線.\n請稍候再試."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh.lproj/AppboyFeedLocalizable.strings b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh.lproj/AppboyFeedLocalizable.strings new file mode 100644 index 0000000000..614fa17a49 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/Resources/zh.lproj/AppboyFeedLocalizable.strings @@ -0,0 +1,5 @@ +/* News Feed Context Labels */ +"Appboy.feed.done-button.title" = "完成"; +"Appboy.feed.no-card.text" = "暂时没有更新.\n请稍后再试."; +"Appboy.feed.no-connection.title" = "连接错误"; +"Appboy.feed.no-connection.message" = "无法建立网络连接.\n请稍候再试."; diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.h new file mode 100644 index 0000000000..312de257d9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.h @@ -0,0 +1,29 @@ +#import +#import + +@interface ABKFeedWebViewController : UIViewController + +/*! + * The URL the modal web view controller should open. Please note that this is the initial URL and + * won't be updated if the initial URL re-directs to another URL. + */ +@property NSURL *url; + +/*! + * The WKWebView which displays the web page. + */ +@property (nonatomic) IBOutlet WKWebView *webView; + +/*! + * The UIProgressView which shows the web view loading process. It will be on top of the web view and + * will disappear as soon as the page is loaded. + */ +@property (nonatomic) IBOutlet UIProgressView *progressBar; + +/*! + * The property tells the web view controller to add a Done button or not. The default value is NO. + * Please set this property before displaying the web view controller. + */ +@property (nonatomic) BOOL showDoneButton; + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.m new file mode 100644 index 0000000000..d41e7bfbe9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKFeedWebViewController.m @@ -0,0 +1,184 @@ +#import "ABKFeedWebViewController.h" +#import "ABKNoConnectionLocalization.h" +#import "ABKUIUtils.h" + +static NSString *const EstimatedProgressKeyPath = @"estimatedProgress"; +static NSString *const LocalizedNoConnectionKey = @"Appboy.no-connection.message"; + +@implementation ABKFeedWebViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.webView.navigationDelegate = self; + self.webView = [self getWebView]; + self.view = self.webView; + +#if !TARGET_OS_TV + if (@available(iOS 15.0, *)) { + self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; + } +#endif + + [self setupProgressBar]; + + if (self.showDoneButton) { + UIBarButtonItem *closeBarButton = [self getDoneBarButtonItem]; + [self.navigationItem setRightBarButtonItem:closeBarButton]; + } + + [self.webView addObserver:self + forKeyPath:EstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + + [self.webView loadRequest:[NSURLRequest requestWithURL:self.url]]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([ABKUIUtils string:EstimatedProgressKeyPath isEqualToString:keyPath]) { + if (self.webView.estimatedProgress == 1.0) { + [UIView animateWithDuration:1 animations:^{ + self.progressBar.alpha = 0.0; + }]; + } else if (self.webView.estimatedProgress < 1.0) { + self.progressBar.alpha = 1.0; + [self.progressBar setProgress:self.webView.estimatedProgress animated:YES]; + } + } +} + +- (void)dealloc { + [self.webView removeObserver:self forKeyPath:EstimatedProgressKeyPath]; +} + +#pragma mark - Customization Methods + +/*! + * @discussion Returns a WKWebView object, whose navigationDelegate is this ABKFeedWebViewController instance. + * + * If you want to do any customization to the WKWebView, please override this method in an ABKFeedWebViewController + * category and return the customized WKWebView. All instances of ABKFeedWebViewController will then + * call the category's `getWebView` implementation instead of this method. + * + */ +- (WKWebView *)getWebView { + WKWebView *webView = [[WKWebView alloc] initWithFrame:CGRectZero]; + webView.navigationDelegate = self; + return webView; +} + +/*! + * + * @discussion Creates a UIProgressView and puts it on top of the web view. + * + * If you want to do any customization to the progress bar, please override this method in an ABKFeedWebViewController + * category and set up the progress bar. All instances of ABKFeedWebViewController will then + * call the category's `setupProgressBar:` implementation instead of this method. + * + */ +- (void)setupProgressBar{ + UIProgressView *progressBar = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleBar]; + progressBar.alpha = 0; + self.progressBar = progressBar; + + [self.view addSubview:self.progressBar]; + self.progressBar.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.progressBar + attribute:NSLayoutAttributeTop + relatedBy:NSLayoutRelationEqual + toItem:self.topLayoutGuide + attribute:NSLayoutAttributeBottom + multiplier:1.0 + constant:0.0]]; + [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[progressBar]|" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:@{@"progressBar" : self.progressBar}]]; +} + +/*! + * @discussion Returns the Done UIBarButtonItem, which allows the user to dismiss the modal web view. + * + * If you want to do any customization to the Done button, please override this method in an ABKFeedWebViewController + * category and return the customized UIBarButtonItem. All instances of ABKFeedWebViewController will then + * call the category's `getDoneBarButtonItem` implementation instead of this method. + * + */ +- (UIBarButtonItem *)getDoneBarButtonItem { + return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(closeButtonPressed:)]; +} + +- (void)closeButtonPressed:(id)sender { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +#pragma mark - WKNavigationDelegate methods + +- (void)webView:(WKWebView *)webView + decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction + decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler { + NSString *urlString = [[navigationAction.request.mainDocumentURL absoluteString] lowercaseString]; + NSArray *stringComponents = [urlString componentsSeparatedByString:@":"]; + if ([stringComponents[1] hasPrefix:@"//itunes.apple.com"] || + (![stringComponents[0] isEqual:@"http"] && + ![stringComponents[0] isEqual:@"https"])) { + // Dismiss the modal web view and let the system handle the deep links + + /* + if ([[UIApplication sharedApplication] openURL:navigationAction.request.URL]) { + decisionHandler(WKNavigationActionPolicyCancel); + [self.navigationController popViewControllerAnimated:NO]; + return; + } + */ + + [[UIApplication sharedApplication] openURL:navigationAction.request.URL options:@{} completionHandler:^(BOOL success) { + if (success) { + decisionHandler(WKNavigationActionPolicyCancel); + [self.navigationController popViewControllerAnimated:NO]; + } else { + decisionHandler(WKNavigationActionPolicyAllow); + } + }]; + } + decisionHandler(WKNavigationActionPolicyAllow); +} + +- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { + self.progressBar.alpha = 0.0; +} + +- (void)webView:(WKWebView *)webView + didFailProvisionalNavigation:(WKNavigation *)navigation + withError:(NSError *)error { + self.progressBar.alpha = 0.0; + + // Display localized "No Connection" message + UILabel *label = [[UILabel alloc] init]; + label.textAlignment = NSTextAlignmentCenter; + label.numberOfLines = 0; + NSString *localizedNoConectionMessage = NSLocalizedString(@"Appboy.no-connection.message", + @"No connection error message for URL loading failure"); + if (localizedNoConectionMessage.length == 0 || [ABKUIUtils string:LocalizedNoConnectionKey isEqualToString:localizedNoConectionMessage]) { + localizedNoConectionMessage = [ABKNoConnectionLocalization getNoConnectionLocalizedString]; + } + label.text = localizedNoConectionMessage; + [self.webView addSubview:label]; + label.translatesAutoresizingMaskIntoConstraints = NO; + [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-10-[noConnectionLabel]-10-|" + options:NSLayoutFormatDirectionLeadingToTrailing + metrics:nil + views:@{@"noConnectionLabel" : label}]]; + [self.webView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[noConnectionLabel]|" + options:NSLayoutFormatAlignAllCenterY + metrics:nil + views:@{@"noConnectionLabel" : label}]]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.h new file mode 100644 index 0000000000..fdea28f933 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.h @@ -0,0 +1,116 @@ +#import +#import "ABKNFBaseCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +typedef NS_OPTIONS(NSUInteger, ABKCardCategory); +@class ABKCard; + +@interface ABKNewsFeedTableViewController : UITableViewController + +/*! + * @discussion Initialization that is done for all ABKNewsFeedTableViewControllers with or without storyboard/XIB. + */ +- (void)setUp; + +/*! + * @discussion Initialization that is done for ABKNewsFeedTableViewControllers with programmatic layout only. + */ +- (void)setUpUI; + +/*! + * @discussion Registers Cell classes with the tableview, override this method when implementing custom + * cell classes to register the new subclasses. + */ +- (void)registerTableViewCellClasses; + +/*! + * @param tableView The table view which need the cell to diplay the card UI. + * @param indexPath The index path of the card UI in the table view. + * @param card The card model for the cell. + * + * @discussion This method dequeues and returns the corresponding card cell based on card type from + * the given table view. + */ +- (ABKNFBaseCardCell *)dequeueCellFromTableView:(UITableView *)tableView + forIndexPath:(NSIndexPath *)indexPath + forCard:(ABKCard *)card; + +/*! + * UI elements which are used in the News Feed table view. You can find them in the News Feed Card Storyboard. + */ +@property (nonatomic) IBOutlet UIView *emptyFeedView; +@property (nonatomic) IBOutlet UILabel *emptyFeedLabel; + +/*! + * This property allows you to enable or disable the unread indicator on the news feed. The default + * value is NO, which will enable the displaying of the unread indicator on cards. + */ +@property (nonatomic) BOOL disableUnreadIndicator; + +/*! + * This property indicates which categories of cards the news feed is displaying. + * Setting this property will automatically update the news feed page and only display cards in the given categories. + * This method won't request refresh of cards from the Braze server, but only look into cards that are cached in the SDK. + */ +@property (nonatomic) ABKCardCategory categories; + +/*! + * This property shows the cards displayed in the News Feed. Please note that the News Feed view + * controller listens to the ABKFeedUpdatedNotification notification from the Braze SDK, which will + * update the value of this property. + */ +@property (nonatomic) NSArray *cards; + +/*! + * This set stores the card IDs for which the impressions have been logged. + */ +@property (nonatomic) NSMutableSet *cardImpressions; + +/*! + * This property defines the timeout for stored News Feed cards in the Braze SDK. If the cards in the + * Braze SDK are older than this value, the News Feed view controller will request a News Feed update. + * + * The default value is 60 seconds. + */ +@property NSTimeInterval cacheTimeout; + +@property id constraintWarningValue; + +/*! + * @discussion This method returns an instance of ABKNewsFeedTableViewController. You can call it + * to get a News Feed view controller for your navigation controller. + * @warning To use a custom News Feed view controller, instantiate your own subclass instead + * (e.g. via alloc / init). + */ ++ (instancetype)getNavigationFeedViewController; + + /*! + * @discussion Given a content card return the type identifier for the above + * registration. + */ + - (NSString *)findCellIdentifierWithCard:(ABKCard *)card; + +/*! + * @discussion This method returns the localized string from AppboyFeedLocalizable.strings file. + * You can easily override the localized string by adding the keys and the translations to your own + * Localizable.strings file. + * + * To do custom handling with the Appboy localized string, you can override this method in a + * subclass. + */ +- (NSString *)localizedAppboyFeedString:(NSString *)key; + +/*! + * @discussion This method handles the user's click on the card. + * + * To do custom handling with the card clicks, you can override this method in a + * subclass. You also need to call [card logCardClicked] manually inside of your new method + * to send the click event to the Braze server. + */ +- (void)handleCardClick:(ABKCard *)card; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.m new file mode 100644 index 0000000000..9299e319fb --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.m @@ -0,0 +1,316 @@ +#import "ABKNewsFeedTableViewController.h" +#import "ABKNFBannerCardCell.h" +#import "ABKNFCaptionedMessageCardCell.h" +#import "ABKNFClassicCardCell.h" +#import "ABKUIUtils.h" +#import "ABKFeedWebViewController.h" +#import "ABKUIURLUtils.h" + +@import BrazeKit; +@import BrazeKitCompat; + +@implementation ABKNewsFeedTableViewController + +#pragma mark - Initialization + +- (instancetype)init { + self = [super init]; + if (self) { + [self setUp]; + [self setUpUI]; + [self registerTableViewCellClasses]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + self = [super initWithCoder:aDecoder]; + if (self) { + [self setUp]; + } + return self; +} + +#pragma mark - SetUp + +- (void)setUp { + _categories = ABKCardCategoryAll; + _cacheTimeout = 60.0; + _cardImpressions = [NSMutableSet set]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(feedUpdated:) + name:ABKFeedUpdatedNotification + object:nil]; +} + +- (void)setUpUI { +#if !TARGET_OS_TV + if (@available(iOS 15.0, *)) { + self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; + } +#endif + self.emptyFeedView = [[UIView alloc] init]; + self.emptyFeedView.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.emptyFeedView]; + [self.emptyFeedView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES; + [self.emptyFeedView.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor].active = YES; + + self.emptyFeedLabel = [[UILabel alloc] init]; + self.emptyFeedLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.emptyFeedLabel.text = [self localizedAppboyFeedString:@"Appboy.feed.no-card.text"]; + [self.emptyFeedView addSubview:self.emptyFeedLabel]; + + [self.emptyFeedLabel.topAnchor constraintEqualToAnchor:self.emptyFeedView.topAnchor].active = YES; + [self.emptyFeedLabel.bottomAnchor constraintEqualToAnchor:self.emptyFeedView.bottomAnchor].active = YES; + [self.emptyFeedLabel.trailingAnchor constraintEqualToAnchor:self.emptyFeedView.trailingAnchor].active = YES; + [self.emptyFeedLabel.leadingAnchor constraintEqualToAnchor:self.emptyFeedView.leadingAnchor].active = YES; + + self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; + self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + self.tableView.backgroundView = nil; + if (@available(iOS 13.0, *)) { + self.tableView.backgroundColor = [UIColor systemGroupedBackgroundColor]; + } else { + self.tableView.backgroundColor = [UIColor groupTableViewBackgroundColor]; + } + + UIRefreshControl *refreshControl = [[UIRefreshControl alloc] init]; + [refreshControl addTarget:self action:@selector(refreshNewsFeed:) + forControlEvents:UIControlEventValueChanged]; + self.refreshControl = refreshControl; + self.navigationItem.title = @"News Feed"; +} + +# pragma mark - View Controller Life Cycle Methods + +- (void)viewDidLoad { + [super viewDidLoad]; + self.cards = [[Appboy sharedInstance].feedController getCardsInCategories:self.categories]; + + self.tableView.rowHeight = UITableViewAutomaticDimension; + self.tableView.estimatedRowHeight = 160; + + [self requestNewCardsIfTimeout]; + + self.emptyFeedLabel.text = [self localizedAppboyFeedString:@"Appboy.feed.no-card.text"]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self updateAndDisplayCardsFromCache]; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [[Appboy sharedInstance] logFeedDisplayed]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [[Braze sharedInstance] _newsFeedApplyLocalCards]; +} + +- (void)viewWillTransitionToSize:(CGSize)size + withTransitionCoordinator:(id)coordinator { + [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; + [coordinator animateAlongsideTransition:nil completion:^(id _Nonnull context) { + [self.tableView reloadData]; + }]; +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +#pragma mark - Update And Display Cached Cards + +- (IBAction)refreshNewsFeed:(UIRefreshControl *)sender { + [[Appboy sharedInstance] requestFeedRefresh]; +} + +- (void)requestNewCardsIfTimeout { + NSTimeInterval passedTime = fabs([[Appboy sharedInstance].feedController.lastUpdate timeIntervalSinceNow]); + if (passedTime > self.cacheTimeout) { + [[Appboy sharedInstance] requestFeedRefresh]; + } +} + +- (void)feedUpdated:(NSNotification *)notification { + BOOL isSuccessful = [notification.userInfo[ABKFeedUpdatedIsSuccessfulKey] boolValue]; + if (isSuccessful) { + [self updateAndDisplayCardsFromCache]; + } + [self.refreshControl endRefreshing]; +} + +- (void)updateAndDisplayCardsFromCache { + self.cards = [[Appboy sharedInstance].feedController getCardsInCategories:self.categories]; + if (self.cards == nil || self.cards.count == 0) { + [self hideTableViewAndShowViewInHeader:self.emptyFeedView]; + } else { + [self showTableViewAndHideHeaderViews]; + } + [self.tableView reloadData]; +} + +- (void)hideTableViewAndShowViewInHeader:(UIView *)view { + view.hidden = NO; + view.frame = self.view.bounds; + [view layoutIfNeeded]; + self.tableView.sectionHeaderHeight = self.tableView.frame.size.height; + self.tableView.tableHeaderView = view; + self.tableView.scrollEnabled = NO; +} + +- (void)showTableViewAndHideHeaderViews { + self.emptyFeedView.hidden = YES; + self.tableView.tableHeaderView = nil; + self.tableView.sectionHeaderHeight = 0; + self.tableView.scrollEnabled = YES; +} + +#pragma mark - Configuration Update + +- (void)setDisableUnreadIndicator:(BOOL)disableUnreadIndicator { + if (disableUnreadIndicator != _disableUnreadIndicator) { + _disableUnreadIndicator = disableUnreadIndicator; + [self updateAndDisplayCardsFromCache]; + } +} + +- (void)setCategories:(ABKCardCategory)categories { + if (categories != _categories) { + _categories = categories; + [self updateAndDisplayCardsFromCache]; + } +} + +#pragma mark - Table view data source + +- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { + return 1; +} + +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.cards.count; +} + +- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { + BOOL cellVisible = [[tableView indexPathsForVisibleRows] containsObject:indexPath]; + if (cellVisible) { + ABKCard *card = self.cards[indexPath.row]; + [self logCardImpressionIfNeeded:card]; + } +} + +- (void)logCardImpressionIfNeeded:(ABKCard *)card { + if ([self.cardImpressions containsObject:card.idString]) { + // do nothing if we have already logged an impression + return; + } + + [card logCardImpression]; + [self.cardImpressions addObject:card.idString]; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + ABKCard *card = self.cards[indexPath.row]; + ABKNFBaseCardCell *cell = [self dequeueCellFromTableView:tableView + forIndexPath:indexPath + forCard:card]; + [cell applyCard:card]; + cell.delegate = self; + cell.hideUnreadIndicator = self.disableUnreadIndicator; + return cell; +} + +- (void)registerTableViewCellClasses { + [self.tableView registerClass:[ABKNFBannerCardCell class] + forCellReuseIdentifier:@"ABKBannerCardCell"]; + [self.tableView registerClass:[ABKNFCaptionedMessageCardCell class] + forCellReuseIdentifier:@"ABKNFCaptionedMessageCardCell"]; + [self.tableView registerClass:[ABKNFClassicCardCell class] + forCellReuseIdentifier:@"ABKNFNewsCardCell"]; +} + +- (ABKNFBaseCardCell *)dequeueCellFromTableView:(UITableView *)tableView + forIndexPath:(NSIndexPath *)indexPath + forCard:(ABKCard *)card { + NSString *cellIdentifier = [self findCellIdentifierWithCard:card]; + return [tableView dequeueReusableCellWithIdentifier:cellIdentifier + forIndexPath:indexPath]; +} + +- (NSString *)findCellIdentifierWithCard:(ABKCard *)card { + if ([card isKindOfClass:[ABKBannerCard class]]) { + return @"ABKBannerCardCell"; + } else if ([card isKindOfClass:[ABKCaptionedImageCard class]]) { + return @"ABKNFCaptionedMessageCardCell"; + } else if ([card isKindOfClass:[ABKClassicCard class]]) { + return @"ABKNFNewsCardCell"; + } else if ([card isKindOfClass:[ABKTextAnnouncementCard class]]) { + return @"ABKNFCaptionedMessageCardCell"; + } + return nil; +} + +- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { + ABKCard *card = self.cards[indexPath.row]; + [self handleCardClick:card]; +} + +#pragma mark - Card Click Actions + +- (void)handleCardClick:(ABKCard *)card { + [card logCardClicked]; + + NSURL *cardURL = [ABKUIURLUtils getEncodedURIFromString:card.urlString]; + + // URL Delegate + if ([ABKUIURLUtils URLDelegate:Appboy.sharedInstance.appboyUrlDelegate + handlesURL:cardURL + fromChannel:ABKNewsFeedChannel + withExtras:nil]) { + return; + } + + // WebView + if ([ABKUIURLUtils URL:cardURL shouldOpenInWebView:card.openUrlInWebView]) { + [self openURLInWebView:cardURL]; + return; + } + + // System + [ABKUIURLUtils openURLWithSystem:cardURL]; +} + +- (void)openURLInWebView:(NSURL *)url { + ABKFeedWebViewController *webViewController = [[ABKFeedWebViewController alloc] init]; + webViewController.url = url; + webViewController.showDoneButton = self.navigationItem.rightBarButtonItem != nil; + [self.navigationController pushViewController:webViewController animated:YES]; +} + +# pragma mark - Utility Methods + ++ (instancetype)getNavigationFeedViewController { + return [[ABKNewsFeedTableViewController alloc] init]; +} + +- (NSString *)localizedAppboyFeedString:(NSString *)key { + return [ABKUIUtils getLocalizedString:key + inAppboyBundle:[ABKUIUtils bundle:[ABKNewsFeedTableViewController class] channel:ABKNewsFeedChannel] + table:@"AppboyFeedLocalizable"]; +} + +# pragma mark - ABKBaseNewsFeedCellDelegate + +- (void)refreshTableViewCellHeights { + [UIView performWithoutAnimation:^{ + [self.tableView beginUpdates]; + [self.tableView endUpdates]; + }]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.h new file mode 100644 index 0000000000..6eac761b19 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.h @@ -0,0 +1,12 @@ +#import +#import "ABKNewsFeedTableViewController.h" + +@interface ABKNewsFeedViewController : UINavigationController + +/*! + * This property is the table view controller which displays all the cards. It's also the root view + * controller. + */ +@property (nonatomic) ABKNewsFeedTableViewController *newsFeed; + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.m new file mode 100644 index 0000000000..fac35001e0 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.m @@ -0,0 +1,39 @@ +#import "ABKNewsFeedViewController.h" +#import "ABKNewsFeedTableViewController.h" +#import "ABKUIUtils.h" + +@implementation ABKNewsFeedViewController + +- (instancetype)init { + self = [super init]; + if (self) { + self.newsFeed = [[ABKNewsFeedTableViewController alloc] init]; + [self pushViewController:self.newsFeed animated:NO]; + [self addDoneButton]; +#if !TARGET_OS_TV + if (@available(iOS 15.0, *)) { + self.view.backgroundColor = UIColor.systemGroupedBackgroundColor; + } +#endif + } + return self; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + self.newsFeed = self.viewControllers.firstObject; + [self addDoneButton]; +} + +- (void)addDoneButton { + UIBarButtonItem *closeBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone + target:self + action:@selector(dismissNewsFeed:)]; + [self.newsFeed.navigationItem setRightBarButtonItem:closeBarButton]; +} + +- (IBAction)dismissNewsFeed:(id)sender { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.h new file mode 100644 index 0000000000..62c5d042b7 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.h @@ -0,0 +1,22 @@ +#import "ABKNFBaseCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKCard; + +@interface ABKNFBannerCardCell : ABKNFBaseCardCell + +@property (nonatomic) IBOutlet UIImageView *bannerImageView; +@property (nonatomic) IBOutlet NSLayoutConstraint *imageRatioConstraint; + +/*! + * @discussion Programmatic initialization and layout of the banner imageView, exposed for customization. + */ +- (void)setUpBannerImageView; + +- (void)applyCard:(ABKCard *)bannerCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.m new file mode 100644 index 0000000000..8f8b4a35fb --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.m @@ -0,0 +1,99 @@ +#import "ABKNFBannerCardCell.h" + +@import BrazeKitCompat; + +@implementation ABKNFBannerCardCell + +#pragma mark - SetUp + +- (void)setUpUI { + [super setUpUI]; + [self setUpBannerImageView]; +} + +- (void)setUpBannerImageView { + self.bannerImageView = [[[self imageViewClass] alloc] init]; + self.bannerImageView.contentMode = UIViewContentModeScaleAspectFit; + self.bannerImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.bannerImageView setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; + [self.bannerImageView setContentHuggingPriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisHorizontal]; + [self.bannerImageView setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh forAxis:UILayoutConstraintAxisVertical]; + [self.rootView addSubview:self.bannerImageView]; + [self.bannerImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; + [self.bannerImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; + [self.bannerImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; + [self.bannerImageView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor].active = YES; + + NSLayoutConstraint *estimatedWidth = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.rootView.widthAnchor]; + estimatedWidth.priority = UILayoutPriorityDefaultHigh; + estimatedWidth.active = YES; + self.imageRatioConstraint = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.bannerImageView.heightAnchor multiplier:355.0/79.0]; + self.imageRatioConstraint.priority = UILayoutPriorityRequired-1; + self.imageRatioConstraint.active = YES; + NSLayoutConstraint *estimatedHeight = [self.rootView.heightAnchor constraintGreaterThanOrEqualToConstant:100]; + estimatedHeight.priority = UILayoutPriorityDefaultLow; + estimatedHeight.active = YES; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKCard *)card { + if (![card isKindOfClass:[ABKBannerCard class]]) { + return; + } + + [super applyCard:card]; + ABKBannerCard *bannerCard = (ABKBannerCard *)card; + + [self updateImageRatioConstraintToRatio:bannerCard.imageAspectRatio]; + [self setNeedsUpdateConstraints]; + [self setNeedsLayout]; + + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + typeof(self) __weak weakSelf = self; + [[Appboy sharedInstance].imageDelegate setImageForView:self.bannerImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:bannerCard.image] + imagePlaceHolder:nil + completed:^(UIImage * _Nullable image, + NSError * _Nullable error, + NSInteger cacheType, + NSURL * _Nullable imageURL) { + if (weakSelf == nil) { + return; + } + if (image) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGFloat newRatio = image.size.width / image.size.height; + if (fabs(newRatio - weakSelf.imageRatioConstraint.multiplier) > 0.1f) { + [weakSelf updateImageRatioConstraintToRatio:newRatio]; + [weakSelf setNeedsUpdateConstraints]; + [weakSelf setNeedsLayout]; + } + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.bannerImageView.image = [weakSelf getPlaceHolderImage]; + }); + } + }]; +} + +- (void)updateImageRatioConstraintToRatio:(CGFloat)newRatio { + if (self.imageRatioConstraint) { + self.imageRatioConstraint.active = NO; + } + self.imageRatioConstraint = [self.bannerImageView.widthAnchor constraintEqualToAnchor:self.bannerImageView.heightAnchor multiplier:newRatio]; + self.imageRatioConstraint.priority = UILayoutPriorityRequired-1; + NSLayoutConstraint *estimatedHeight = [self.rootView.heightAnchor constraintGreaterThanOrEqualToConstant:ceil(self.rootView.frame.size.width/self.imageRatioConstraint.multiplier)]; + estimatedHeight.priority = UILayoutPriorityDefaultLow; + estimatedHeight.active = YES; + self.imageRatioConstraint.active = YES; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.h new file mode 100644 index 0000000000..14980735fa --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.h @@ -0,0 +1,100 @@ +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKCard; + +@protocol ABKBaseNewsFeedCellDelegate + +- (void)refreshTableViewCellHeights; + +@end + +extern CGFloat ABKNFLabelHorizontalSpace; +extern CGFloat ABKNFLabelVerticalSpace; +extern CGFloat ABKNFTopSpace; + +@interface ABKNFBaseCardCell : UITableViewCell + ++ (UIColor *)ABKNFDescriptionLabelColor; ++ (UIColor *)ABKNFTitleLabelColor; ++ (UIColor *)ABKNFTitleLabelColorOnGray; + +/*! + * This view displays the card contents and is the base view container for each card. To change or + * configure the outline of the card like card width, background color board width, etc, you can + * update this property accordingly. + */ +@property (nonatomic) IBOutlet UIView *rootView; + +/*! + * This is the triangle image which shows if a card has been viewed by the user. + */ +@property (nonatomic) IBOutlet UIImageView *unreadIndicatorView; + +@property (nonatomic) id delegate; + +/*! + * Card root view related constraints + */ +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewLeadingConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTrailingConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewTopConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *rootViewBottomConstraint; + +/*! + * These are basic UI configuration for the News Feed. They are set to the default value in `setUp` + * method. + * + * It's recommended to set the values before the view is displayed. + */ +@property CGFloat cardSidePadding; +@property CGFloat cardSpacing; +@property (nonatomic) BOOL hideUnreadIndicator; + +/*! + * @discussion Initialization of cell called even with storyboard/XIB, exposed for customization. + */ +- (void)setUp; + +/*! + * @discussion Programmatic initialization and layout cell, exposed for customization. + */ +- (void)setUpUI; + +/*! + * @discussion Programmatic initialization and layout of cell rootView, exposed for customization. + */ +- (void)setUpRootView; + +/*! + * @discussion Programmatic initialization and layout of cell border, exposed for customization. + */ +- (void)setUpRootViewBorder; + +/*! + * @discussion Programmatic initialization and layout of unread indicator image, exposed for customization. + */ +- (void)setUpUnreadIndicatorView; + +/*! + * @param card The card model for the cell. + * + * @discussion Apply the data from the given card to the card cell. + */ +- (void)applyCard:(ABKCard *)card; + +/*! + * @discussion This is a utility method to return the place holder image. + */ +- (UIImage *)getPlaceHolderImage; + +/*! + * @discussion This is a utility method to return the image view class from the ABKImageDelegate. + */ +- (Class)imageViewClass; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.m new file mode 100644 index 0000000000..4c58a34af6 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.m @@ -0,0 +1,153 @@ +#import "ABKNFBaseCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +CGFloat ABKNFLabelHorizontalSpace = 22.0; +CGFloat ABKNFLabelVerticalSpace = 13.0; +CGFloat ABKNFTopSpace = 7.0; + +static CGFloat AppboyCardSidePadding = 10.0; +static CGFloat AppboyCardSpacing = 20.0; +static CGFloat AppboyCardBorderWidth = 0.5; +static CGFloat AppboyCardCornerRadius = 3.0; + +@implementation ABKNFBaseCardCell + ++ (UIColor *)ABKNFDescriptionLabelColor { + return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.1747547901 green:0.1760663777 blue:0.1758382755 alpha:1] darkColor:[UIColor lightTextColor]]; +} + ++ (UIColor *)ABKNFTitleLabelColor { + return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.25098039220000001 green:0.27657390510000002 blue:0.32259352190000001 alpha:1] darkColor:[UIColor lightTextColor]]; +} + ++ (UIColor *)ABKNFTitleLabelColorOnGray { + return [ABKUIUtils dynamicColorForLightColor:[UIColor colorWithRed:0.25327896900000002 green:0.28065123180000001 blue:0.32005588499999998 alpha:1] darkColor:[UIColor lightTextColor]]; +} + +#pragma mark - Initialization + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + [self setUp]; + [self setUpUI]; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + if (self = [super initWithCoder:aDecoder]) { + [self setUp]; + } + return self; +} + +#pragma mark - SetUp + +- (void)setUp { + _cardSidePadding = AppboyCardSidePadding; + _cardSpacing = AppboyCardSpacing; +} + +- (void)setUpUI { + [self setUpRootView]; + [self setUpRootViewBorder]; + [self setUpUnreadIndicatorView]; +} + +- (void)setUpRootView { + self.backgroundColor = [UIColor clearColor]; + self.contentView.backgroundColor = [UIColor clearColor]; + self.contentView.autoresizingMask = UIViewAutoresizingFlexibleHeight; + self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; + self.selectionStyle = UITableViewCellSelectionStyleNone; + + self.rootView = [[UIView alloc] init]; + self.rootView.translatesAutoresizingMaskIntoConstraints = NO; + [[self contentView] addSubview:self.rootView]; + if (@available(iOS 13.0, *)) { + self.rootView.backgroundColor = [UIColor systemBackgroundColor]; + } else { + self.rootView.backgroundColor = [UIColor whiteColor]; + } + + self.rootViewTopConstraint = [self.rootView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:AppboyCardSpacing / 2.0]; + self.rootViewBottomConstraint = [self.contentView.bottomAnchor constraintEqualToAnchor:self.rootView.bottomAnchor constant:AppboyCardSpacing / 2.0]; + self.rootViewLeadingConstraint = [self.rootView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:AppboyCardSidePadding]; + self.rootViewTrailingConstraint = [self.contentView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor constant:AppboyCardSidePadding]; + [NSLayoutConstraint activateConstraints:@[self.rootViewTopConstraint, + self.rootViewBottomConstraint, + self.rootViewLeadingConstraint, + self.rootViewTrailingConstraint]]; +} + +- (void)setUpRootViewBorder { + self.rootView.layer.cornerRadius = AppboyCardCornerRadius; + self.rootView.layer.masksToBounds = YES; + self.rootView.layer.borderColor = [UIColor colorWithWhite:0.75f alpha:1.0].CGColor; + self.rootView.layer.borderWidth = AppboyCardBorderWidth; + + self.rootViewTopConstraint.constant = AppboyCardSpacing / 2.0; + self.rootViewBottomConstraint.constant = AppboyCardSpacing / 2.0; + self.rootViewLeadingConstraint.constant = AppboyCardSidePadding; + self.rootViewTrailingConstraint.constant = AppboyCardSidePadding; +} + +- (void)setUpUnreadIndicatorView { + self.unreadIndicatorView = [[UIImageView alloc] initWithImage:[ABKUIUtils imageNamed:@"Icons_Read" + bundle:[ABKNFBaseCardCell class] + channel:ABKNewsFeedChannel]]; + self.unreadIndicatorView.translatesAutoresizingMaskIntoConstraints = NO; + self.unreadIndicatorView.highlightedImage = [ABKUIUtils imageNamed:@"Icons_Unread" + bundle:[ABKNFBaseCardCell class] + channel:ABKNewsFeedChannel]; + [self.rootView addSubview:self.unreadIndicatorView]; + + [self.unreadIndicatorView.heightAnchor constraintEqualToConstant:20].active = YES; + [self.unreadIndicatorView.widthAnchor constraintEqualToConstant:20].active = YES; + [self.unreadIndicatorView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; + [self.rootView.trailingAnchor constraintEqualToAnchor:self.unreadIndicatorView.trailingAnchor].active = YES; + self.unreadIndicatorView.image = [self.unreadIndicatorView.image imageFlippedForRightToLeftLayoutDirection]; +} + +# pragma mark - Cell UI Configuration + +- (void)setHideUnreadIndicator:(BOOL)hideUnreadIndicator { + if(self.hideUnreadIndicator != hideUnreadIndicator) { + _hideUnreadIndicator = hideUnreadIndicator; + self.unreadIndicatorView.hidden = hideUnreadIndicator; + } +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKCard *)card { + if(!self.hideUnreadIndicator) { + self.unreadIndicatorView.highlighted = !card.viewed; + } +} + +#pragma mark - Utiliy Methods + +- (UIImage *)getPlaceHolderImage { + return [ABKUIUtils imageNamed:@"img-noimage-lrg" + bundle:[ABKNFBaseCardCell class] + channel:ABKNewsFeedChannel]; +} + +- (Class)imageViewClass { + if ([Appboy sharedInstance].imageDelegate) { + return [[Appboy sharedInstance].imageDelegate imageViewClass]; + } + return [UIImageView class]; + } + +- (void)awakeFromNib { + [super awakeFromNib]; + [self setUpRootViewBorder]; + self.unreadIndicatorView.image = [self.unreadIndicatorView.image imageFlippedForRightToLeftLayoutDirection]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.h new file mode 100644 index 0000000000..ebfa84b6fd --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.h @@ -0,0 +1,62 @@ +#import "ABKNFBaseCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKCaptionedImageCard; + +@interface ABKNFCaptionedMessageCardCell : ABKNFBaseCardCell + +@property (class, nonatomic) UIColor *titleLabelColor; +@property (class, nonatomic) UIColor *descriptionLabelColor; +@property (class, nonatomic) UIColor *linkLabelColor; + +@property (nonatomic) IBOutlet UIImageView *captionedImageView; +@property (nonatomic) IBOutlet UILabel *titleLabel; +@property (nonatomic) IBOutlet UILabel *descriptionLabel; +@property (nonatomic) IBOutlet UIView *titleBackgroundView; +@property (nonatomic) IBOutlet UILabel *linkLabel; +@property (nonatomic) IBOutlet NSLayoutConstraint *imageHeightConstraint; +@property (nonatomic) IBOutlet NSLayoutConstraint *bodyAndLinkConstraint; + +/*! + * @discussion Programmatic initialization and layout of the title background view, grey bar that the title label is in. + * Exposed for customization. + */ +- (void)setUpTitleBackgroundView; + +/*! + * @discussion Programmatic initialization and layout of the title label. Exposed for customization. + */ +- (void)setUpTitleLabel; + +/*! + * @discussion Programmatic initialization and layout of the description label. Exposed for customization. + */ +- (void)setUpDescriptionLabel; + +/*! + * @discussion Programmatic initialization and layout of the link label. Exposed for customization. + */ +- (void)setUpLinkLabel; + +/*! + * @discussion Programmatic initialization and layout of image view. Exposed for customization. + */ +- (void)setUpCaptionedImageView; + +/*! + * @discussion Configures fonts of labels with dynamic type on supported versions of iOS uses older font style + * on earlier versions. Exposed for customization. + */ +- (void)setUpFonts; + +/*! + * This method adjusts the bodyAndLinkConstraint and hides or shows the link label. + */ +- (void)hideLinkLabel:(BOOL)hide; +- (void)applyCard:(ABKCaptionedImageCard *)captionedImageCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.m new file mode 100644 index 0000000000..619a1127cd --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.m @@ -0,0 +1,234 @@ +#import "ABKNFCaptionedMessageCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@implementation ABKNFCaptionedMessageCardCell + +static UIColor *_titleLabelColor = nil; +static UIColor *_descriptionLabelColor = nil; +static UIColor *_linkLabelColor = nil; + ++ (UIColor *)titleLabelColor { + if (_titleLabelColor == nil) { + _titleLabelColor = [ABKNFBaseCardCell ABKNFTitleLabelColor]; + } + return _titleLabelColor; +} + ++ (void)setTitleLabelColor:(UIColor *)titleLabelColor { + _titleLabelColor = titleLabelColor; +} + ++ (UIColor *)descriptionLabelColor { + if (_descriptionLabelColor == nil) { + _descriptionLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; + } + return _descriptionLabelColor; +} + ++ (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { + _descriptionLabelColor = descriptionLabelColor; +} + ++ (UIColor *)linkLabelColor { + if (_linkLabelColor == nil) { + _linkLabelColor = [ABKUIUtils dynamicColorForLightColor:[UIColor blackColor] darkColor:[UIColor whiteColor]]; + } + return _linkLabelColor; +} + ++ (void)setLinkLabelColor:(UIColor *)linkLabelColor{ + _linkLabelColor = linkLabelColor; +} + +#pragma mark - SetUp + +- (void)setUpUI { + [super setUpUI]; + [self setUpTitleBackgroundView]; + [self setUpTitleLabel]; + [self setUpDescriptionLabel]; + [self setUpLinkLabel]; + [self setUpCaptionedImageView]; + [self setUpFonts]; +} + +- (void)setUpTitleBackgroundView { + self.titleBackgroundView = [[UIView alloc] init]; + self.titleBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; + if (@available(iOS 13.0, *)) { + self.titleBackgroundView.backgroundColor = [UIColor systemGroupedBackgroundColor]; + } else { + self.titleBackgroundView.backgroundColor = [UIColor groupTableViewBackgroundColor]; + } + [self.rootView addSubview:self.titleBackgroundView]; + [self.titleBackgroundView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; + [self.titleBackgroundView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; + [self.unreadIndicatorView removeFromSuperview]; + [self.titleBackgroundView addSubview:self.unreadIndicatorView]; + [self.unreadIndicatorView.topAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor].active = YES; + [self.unreadIndicatorView.trailingAnchor constraintEqualToAnchor:self.titleBackgroundView.trailingAnchor].active = YES; +} + +- (void)setUpTitleLabel { + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.titleLabel.textColor = [self class].titleLabelColor; + self.titleLabel.text = @"Title"; + self.titleLabel.numberOfLines = 2; + self.titleLabel.lineBreakMode = NSLineBreakByTruncatingTail; + [self.titleBackgroundView addSubview:self.titleLabel]; + [self.titleLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [self.titleLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.titleBackgroundView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.titleBackgroundView.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.titleLabel.topAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor constant:10].active = YES; + [self.titleBackgroundView.bottomAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:10].active = YES; +} + +- (void)setUpDescriptionLabel { + self.descriptionLabel = [[UILabel alloc] init]; + self.descriptionLabel.textColor = [self class].descriptionLabelColor; + self.descriptionLabel.text = @"Description"; + self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.descriptionLabel.numberOfLines = 0; + self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; + [self.descriptionLabel setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [self.rootView addSubview:self.descriptionLabel]; + [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.rootView.trailingAnchor constraintEqualToAnchor:self.descriptionLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.descriptionLabel.topAnchor constraintEqualToAnchor:self.titleBackgroundView.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; + [self.rootView.bottomAnchor constraintGreaterThanOrEqualToAnchor:self.descriptionLabel.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; +} + +- (void)setUpLinkLabel { + self.linkLabel = [[UILabel alloc] init]; + self.linkLabel.textColor = [self class].linkLabelColor; + self.linkLabel.text = @"Link"; + self.linkLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.linkLabel.numberOfLines = 0; + self.linkLabel.lineBreakMode = NSLineBreakByCharWrapping; + [self.rootView addSubview:self.linkLabel]; + [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.rootView.trailingAnchor constraintEqualToAnchor:self.linkLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.linkLabel.topAnchor constraintEqualToAnchor:self.descriptionLabel.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; + self.bodyAndLinkConstraint = [self.rootView.bottomAnchor constraintEqualToAnchor:self.linkLabel.bottomAnchor constant:ABKNFLabelVerticalSpace]; + self.bodyAndLinkConstraint.active = YES; +} + +- (void)setUpCaptionedImageView { + self.captionedImageView = [[[self imageViewClass] alloc] init]; + self.captionedImageView.contentMode = UIViewContentModeScaleAspectFit; + self.captionedImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.rootView addSubview:self.captionedImageView]; + [self.captionedImageView setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [self.captionedImageView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical]; + [self.captionedImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor].active = YES; + [self.captionedImageView.trailingAnchor constraintEqualToAnchor:self.rootView.trailingAnchor].active = YES; + [self.captionedImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor].active = YES; + NSLayoutConstraint *bottom = [self.captionedImageView.bottomAnchor constraintEqualToAnchor:self.titleBackgroundView.topAnchor]; + bottom.priority = UILayoutPriorityDefaultHigh; + bottom.active = YES; + self.imageHeightConstraint = [self.captionedImageView.heightAnchor constraintEqualToConstant:223]; + self.imageHeightConstraint.active = YES; +} + +- (void)setUpFonts { + // DynamicType + self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; + self.descriptionLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.descriptionLabel]; + self.linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleSubheadline weight:UIFontWeightBold]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.linkLabel]; + + // Bug: On Mac Catalyst 13, allowsDefaultTighteningForTruncation defaults to YES + // - Occurs only if numberOfLine is not 0 + // - Default value should be NO (see documentation – https://apple.co/3bZFc8q) + // - Might be fixed in a later version + self.titleLabel.allowsDefaultTighteningForTruncation = NO; +} + +- (void)hideLinkLabel:(BOOL)hide { + self.linkLabel.hidden = hide; + self.bodyAndLinkConstraint.constant = hide ? 0 : ABKNFLabelVerticalSpace; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKCard *)card { + [super applyCard:card]; + if ([card isKindOfClass:[ABKCaptionedImageCard class]]) { + [self applyCaptionedImageCard:(ABKCaptionedImageCard *)card]; + } else if ([card isKindOfClass:[ABKTextAnnouncementCard class]]) { + [self applyTextAnnouncementCard:(ABKTextAnnouncementCard *)card]; + } +} + +- (void)applyCaptionedImageCard:(ABKCaptionedImageCard *)captionedImageCard { + self.titleLabel.text = captionedImageCard.title; + self.descriptionLabel.text = captionedImageCard.cardDescription; + self.linkLabel.text = captionedImageCard.domain; + BOOL shouldHideLink = captionedImageCard.domain == nil || captionedImageCard.domain.length == 0; + [self hideLinkLabel:shouldHideLink]; + + CGFloat currImageHeightConstraint = self.captionedImageView.frame.size.width / captionedImageCard.imageAspectRatio; + self.imageHeightConstraint.constant = currImageHeightConstraint; + [self setNeedsUpdateConstraints]; + [self setNeedsDisplay]; + + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + typeof(self) __weak weakSelf = self; + [[Appboy sharedInstance].imageDelegate setImageForView:self.captionedImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:captionedImageCard.image] + imagePlaceHolder:nil + completed:^(UIImage * _Nullable image, + NSError * _Nullable error, + NSInteger cacheType, + NSURL * _Nullable imageURL) { + if (weakSelf == nil) { + return; + } + if (image) { + dispatch_async(dispatch_get_main_queue(), ^{ + CGFloat newImageHeightConstraint = weakSelf.captionedImageView.frame.size.width * image.size.height / image.size.width; + if (fabs(newImageHeightConstraint - currImageHeightConstraint) > 5e-1) { + weakSelf.imageHeightConstraint.constant = newImageHeightConstraint; + [weakSelf setNeedsUpdateConstraints]; + [weakSelf setNeedsDisplay]; + // Force a redraw, as SDWebImage 5+ consistently gets the original constraint wrong. + [weakSelf.delegate refreshTableViewCellHeights]; + } + }); + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + weakSelf.captionedImageView.image = [weakSelf getPlaceHolderImage]; + }); + } + }]; +} + +- (void)applyTextAnnouncementCard:(ABKTextAnnouncementCard *)textAnnouncementCard { + self.titleLabel.text = textAnnouncementCard.title; + self.descriptionLabel.text = textAnnouncementCard.cardDescription; + self.linkLabel.text = textAnnouncementCard.domain; + BOOL shouldHideLink = textAnnouncementCard.domain == nil || textAnnouncementCard.domain.length == 0; + [self hideLinkLabel:shouldHideLink]; + + self.imageHeightConstraint.constant = 0; + [self setNeedsLayout]; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + [self setUpFonts]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.h b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.h new file mode 100644 index 0000000000..da42f52bd9 --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.h @@ -0,0 +1,49 @@ +#import "ABKNFBaseCardCell.h" + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +@class ABKClassicCard; + +@interface ABKNFClassicCardCell : ABKNFBaseCardCell + +@property (class, nonatomic) UIColor *titleLabelColor; +@property (class, nonatomic) UIColor *descriptionLabelColor; +@property (class, nonatomic) UIColor *linkLabelColor; + +@property (nonatomic) IBOutlet UIImageView *classicImageView; +@property (nonatomic) IBOutlet UILabel *titleLabel; +@property (nonatomic) IBOutlet UILabel *descriptionLabel; +@property (nonatomic) IBOutlet UILabel *linkLabel; + +/*! + * @discussion Programmatic initialization and layout of image view. Exposed for customization. + */ +- (void)setUpClassicImageView; + +/*! + * @discussion Programmatic initialization and layout of the title label. Exposed for customization. + */ +- (void)setUpTitleLabel; + +/*! + * @discussion Programmatic initialization and layout of the description label. Exposed for customization. + */ +- (void)setUpDescriptionLabel; + +/*! + * @discussion Programmatic initialization and layout of the link label. Exposed for customization. + */ +- (void)setUpLinkLabel; + +/*! + * @discussion Configures fonts of labels with dynamic type on supported versions of iOS uses older font style + * on earlier versions. Exposed for customization. + */ +- (void)setUpFonts; + +- (void)applyCard:(ABKClassicCard *)classicCard; + +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.m b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.m new file mode 100644 index 0000000000..0af101253c --- /dev/null +++ b/Sources/BrazeUICompat/ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.m @@ -0,0 +1,154 @@ +#import "ABKNFClassicCardCell.h" +#import "ABKUIUtils.h" + +@import BrazeKitCompat; + +@implementation ABKNFClassicCardCell + +static UIColor *_titleLabelColor = nil; +static UIColor *_descriptionLabelColor = nil; +static UIColor *_linkLabelColor = nil; + ++ (UIColor *)titleLabelColor { + if (_titleLabelColor == nil) { + _titleLabelColor = [ABKNFBaseCardCell ABKNFTitleLabelColor]; + } + return _titleLabelColor; +} + ++ (void)setTitleLabelColor:(UIColor *)titleLabelColor { + _titleLabelColor = titleLabelColor; +} + ++ (UIColor *)descriptionLabelColor { + if (_descriptionLabelColor == nil) { + _descriptionLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; + } + return _descriptionLabelColor; +} + ++ (void)setDescriptionLabelColor:(UIColor *)descriptionLabelColor { + _descriptionLabelColor = descriptionLabelColor; +} + ++ (UIColor *)linkLabelColor { + if (_linkLabelColor == nil) { + _linkLabelColor = [ABKNFBaseCardCell ABKNFDescriptionLabelColor]; + } + return _linkLabelColor; +} + ++ (void)setLinkLabelColor:(UIColor *)linkLabelColor{ + _linkLabelColor = linkLabelColor; +} + +#pragma mark - SetUp + +- (void)setUpUI { + [super setUpUI]; + [self setUpClassicImageView]; + [self setUpTitleLabel]; + [self setUpDescriptionLabel]; + [self setUpLinkLabel]; + [self setUpFonts]; +} + +- (void)setUpClassicImageView { + self.classicImageView = [[[self imageViewClass] alloc] init]; + self.classicImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.rootView addSubview:self.classicImageView]; + [self.classicImageView.heightAnchor constraintEqualToAnchor:self.classicImageView.widthAnchor multiplier:1.0].active = YES; + [self.classicImageView.leadingAnchor constraintEqualToAnchor:self.rootView.leadingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.classicImageView.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:ABKNFLabelVerticalSpace].active = YES; + [self.rootView.bottomAnchor constraintGreaterThanOrEqualToAnchor:self.classicImageView.bottomAnchor constant:ABKNFLabelVerticalSpace].active = YES; + [self.classicImageView.widthAnchor constraintEqualToAnchor:self.rootView.widthAnchor multiplier:0.177].active = YES; +} + +- (void)setUpTitleLabel { + self.titleLabel = [[UILabel alloc] init]; + self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.titleLabel.numberOfLines = 0; + self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.titleLabel.textColor = [self class].titleLabelColor; + self.titleLabel.text = @"Title"; + [self.rootView addSubview:self.titleLabel]; + [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.classicImageView.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.rootView.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor constant:ABKNFLabelHorizontalSpace].active = YES; + [self.titleLabel.topAnchor constraintEqualToAnchor:self.rootView.topAnchor constant:ABKNFTopSpace].active = YES; +} + +- (void)setUpDescriptionLabel { + self.descriptionLabel = [[UILabel alloc] init]; + self.descriptionLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.descriptionLabel.numberOfLines = 0; + self.descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping; + self.descriptionLabel.textColor = [self class].descriptionLabelColor; + self.descriptionLabel.text = @"Description"; + [self.rootView addSubview:self.descriptionLabel]; + [self.titleLabel.bottomAnchor constraintEqualToAnchor:self.descriptionLabel.topAnchor].active = YES; + + [self.descriptionLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor].active = YES; + [self.descriptionLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor].active = YES; +} + +- (void)setUpLinkLabel { + self.linkLabel = [[UILabel alloc] init]; + self.linkLabel.translatesAutoresizingMaskIntoConstraints = NO; + self.linkLabel.numberOfLines = 0; + self.linkLabel.lineBreakMode = NSLineBreakByCharWrapping; + self.linkLabel.textColor = [self class].linkLabelColor; + self.linkLabel.text = @"Link"; + [self.rootView addSubview:self.linkLabel]; + [self.linkLabel.leadingAnchor constraintEqualToAnchor:self.titleLabel.leadingAnchor].active = YES; + [self.linkLabel.trailingAnchor constraintEqualToAnchor:self.titleLabel.trailingAnchor].active = YES; + [self.linkLabel.topAnchor constraintGreaterThanOrEqualToAnchor:self.descriptionLabel.bottomAnchor constant:5].active = YES; + [self.rootView.bottomAnchor constraintEqualToAnchor:self.linkLabel.bottomAnchor constant:ABKNFTopSpace].active = YES; +} + +- (void)setUpFonts { + // DynamicType + self.titleLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleTitle3 weight:UIFontWeightBold]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.titleLabel]; + self.descriptionLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.descriptionLabel]; + self.linkLabel.font = [ABKUIUtils preferredFontForTextStyle:UIFontTextStyleSubheadline weight:UIFontWeightBold]; + [ABKUIUtils enableAdjustsFontForContentSizeCategory:self.linkLabel]; + + // Bug: On Mac Catalyst 13, allowsDefaultTighteningForTruncation defaults to YES + // - Occurs only if numberOfLine is not 0 + // - Default value should be NO (see documentation – https://apple.co/3bZFc8q) + // - Might be fixed in a later version + self.titleLabel.allowsDefaultTighteningForTruncation = NO; +} + +#pragma mark - ApplyCard + +- (void)applyCard:(ABKCard *)card { + [super applyCard:card]; + if (![card isKindOfClass:[ABKClassicCard class]]) { + return; + } + ABKClassicCard *classicCard = (ABKClassicCard *)card; + self.titleLabel.text = classicCard.title; + self.descriptionLabel.text = classicCard.cardDescription; + self.linkLabel.text = classicCard.domain; + + if (![Appboy sharedInstance].imageDelegate) { + NSLog(@"[APPBOY][WARN] %@ %s", + @"ABKImageDelegate on Appboy is nil. Image loading may be disabled.", + __PRETTY_FUNCTION__); + return; + } + [[Appboy sharedInstance].imageDelegate setImageForView:self.classicImageView + showActivityIndicator:NO + withURL:[NSURL URLWithString:classicCard.image] + imagePlaceHolder:[self getPlaceHolderImage] + completed:nil]; +} + +- (void)awakeFromNib { + [super awakeFromNib]; + [self setUpFonts]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.h b/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.h new file mode 100644 index 0000000000..682209e810 --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.h @@ -0,0 +1,15 @@ +@import BrazeKitCompat; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + + +NS_ASSUME_NONNULL_BEGIN + +@interface ABKSDWebImageImageDelegate : NSObject + +@end + +NS_ASSUME_NONNULL_END + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.m b/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.m new file mode 100644 index 0000000000..8e0b7790f3 --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKSDWebImageImageDelegate.m @@ -0,0 +1,45 @@ +#import "ABKSDWebImageImageDelegate.h" +#import "SDWebImage/SDWebImage.h" + +@implementation ABKSDWebImageImageDelegate + +- (void)setImageForView:(UIImageView *)imageView + showActivityIndicator:(BOOL)showActivityIndicator + withURL:(nullable NSURL *)imageURL + imagePlaceHolder:(nullable UIImage *)placeHolder + completed:(nullable void (^)(UIImage * _Nullable image, NSError * _Nullable error, NSInteger cacheType, NSURL * _Nullable imageURL))completion { + if (showActivityIndicator) { + imageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator; + } + [imageView sd_setImageWithURL:imageURL + placeholderImage:placeHolder + options: (SDWebImageQueryMemoryData | SDWebImageQueryDiskDataSync) + completed:completion]; +} + +- (void)loadImageWithURL:(nullable NSURL *)url + options:(ABKImageOptions)options + completed:(nullable void(^)(UIImage *image, NSData *data, NSError *error, NSInteger cacheType, BOOL finished, NSURL *imageURL))completion { + [SDWebImageManager.sharedManager loadImageWithURL:url + options:(SDWebImageOptions)options + progress:nil + completed:completion]; +} + +- (void)diskImageExistsForURL:(nullable NSURL *)url + completed:(nullable void (^)(BOOL isInCache))completion { + if (url != nil) { + [[SDImageCache sharedImageCache] diskImageExistsWithKey:url.absoluteString completion:completion]; + } +} + +- (nullable UIImage *)imageFromCacheForURL:(nullable NSURL *)url { + NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url]; + return [SDImageCache.sharedImageCache imageFromCacheForKey:key]; +} + +- (Class)imageViewClass { + return [SDAnimatedImageView class]; +} + +@end diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.h b/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.h new file mode 100644 index 0000000000..c4c6716ddf --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.h @@ -0,0 +1,25 @@ +#import +#import + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + +typedef NS_ENUM(NSInteger, ABKChannel); +@protocol ABKURLDelegate; + +@interface ABKUIURLUtils : NSObject + ++ (BOOL)URLDelegate:(id)urlDelegate + handlesURL:(NSURL *)url + fromChannel:(ABKChannel)channel + withExtras:(NSDictionary *)extras; ++ (BOOL)URL:(NSURL *)url shouldOpenInWebView:(BOOL)openUrlInWebView; ++ (BOOL)URLHasSystemScheme:(NSURL *)url; ++ (void)openURLWithSystem:(NSURL *)url; ++ (UIViewController *)topmostViewControllerWithRootViewController:(UIViewController *)viewController; ++ (void)displayModalWebViewWithURL:(NSURL *)url + topmostViewController:(UIViewController *)topmostViewController; ++ (NSURL *)getEncodedURIFromString:(NSString *)uriString; +@end + +#pragma clang diagnostic pop diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.m b/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.m new file mode 100644 index 0000000000..0d20e81553 --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKUIURLUtils.m @@ -0,0 +1,138 @@ +#import "ABKUIURLUtils.h" +#import "ABKUIUtils.h" +#import "ABKModalWebViewController.h" + +@import BrazeKitCompat; + +@interface ABKUIURLUtils () + ++ (NSString *)trim:(NSString *)string; + +@end + +@implementation ABKUIURLUtils + ++ (BOOL)URLDelegate:(id)urlDelegate + handlesURL:(NSURL *)url + fromChannel:(ABKChannel)channel + withExtras:(NSDictionary *)extras { + if (![ABKUIURLUtils URLDelegateIsValid:urlDelegate]) { + NSLog(@"Not handling URL %@ with invalid ABKURLDelegate %@.", + url.absoluteString, urlDelegate); + return NO; + } + if ([urlDelegate handleAppboyURL:url fromChannel:channel withExtras:extras]) { + NSLog(@"Handled URL %@ with external ABKURLDelegate %@.", + url.absoluteString, urlDelegate); + return YES; + } + return NO; +} + ++ (BOOL)URLDelegateIsValid:(id)urlDelegate { + return [urlDelegate respondsToSelector:@selector(handleAppboyURL:fromChannel:withExtras:)]; +} + ++ (BOOL)URL:(NSURL *)url shouldOpenInWebView:(BOOL)openUrlInWebView { + if ([ABKUIUtils objectIsValidAndNotEmpty:url.absoluteString] && openUrlInWebView) { + if ([ABKUIURLUtils URLHasValidWebScheme:url]) { + return YES; + } else { + NSLog(@"Unsupported web URL scheme received: %@. Not opening URL in web view.", url.absoluteString); + } + } + return NO; +} + ++ (BOOL)URLHasValidWebScheme:(NSURL *)url { + return ([ABKUIUtils string:[url.scheme lowercaseString] isEqualToString:@"http"] || + [ABKUIUtils string:[url.scheme lowercaseString] isEqualToString:@"https"]); +} + ++ (BOOL)URLHasSystemScheme:(NSURL *)url { + static dispatch_once_t once; + static NSSet *systemSchemes; + dispatch_once(&once, ^{ + systemSchemes = [NSSet setWithArray:@[ + @"mailto", + @"tel", + @"facetime", + @"facetime-audio", + @"sms" + ]]; + }); + + return [systemSchemes containsObject:[url.scheme lowercaseString]]; +} + ++ (void)openURLWithSystem:(NSURL *)url { + if (![NSThread isMainThread]) { + dispatch_sync(dispatch_get_main_queue(), ^{ + [self openURL:url]; + }); + } else { + [self openURL:url]; + } +} + ++ (void)openURL:(NSURL *)url { + if (@available(iOS 13.0, *)) { + UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; + if (windowScene) { + [windowScene openURL:url options:nil completionHandler:nil]; + return; + } + } + + /* + if (@available(iOS 10.0, *)) { + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + return; + } + + [[UIApplication sharedApplication] openURL:url]; + */ + + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; +} + ++ (UIViewController *)topmostViewControllerWithRootViewController:(UIViewController *)viewController { + while (viewController.presentedViewController) { + viewController = viewController.presentedViewController; + } + return viewController; +} + ++ (void)displayModalWebViewWithURL:(NSURL *)URL + topmostViewController:(UIViewController *)topmostViewController { + ABKModalWebViewController *webViewController = [[ABKModalWebViewController alloc] init]; + webViewController.url = URL; + [topmostViewController presentViewController:webViewController animated:YES completion:nil]; +} + ++ (NSURL *)getEncodedURIFromString:(NSString *)uriString { + if (![ABKUIUtils objectIsValidAndNotEmpty:uriString]) { + return nil; + } + uriString = [ABKUIURLUtils trim:uriString]; + NSURL *parsedUrl = [NSURL URLWithString:uriString]; + // If the uriString is an invalid uri, e.g. an uri with unicode, URLWithString: will return nil. + if (!parsedUrl) { + // When the uriString has unicode, we have to escape those characters + uriString = [uriString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + parsedUrl = [NSURL URLWithString:uriString]; + } + return parsedUrl; +} + ++ (NSString *)trim:(NSString *)string { + if ([string isKindOfClass:[NSString class]]) { + return [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + } + + NSLog(@"Calling `trim` with invalid class: %@, value: %@. Returning nil.", + [string class], string); + return nil; +} + +@end diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.h b/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.h new file mode 100644 index 0000000000..c0a554ad0e --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.h @@ -0,0 +1,95 @@ +#import +#import + +typedef NS_ENUM(NSInteger, ABKChannel); + +#define ABK_CGFLT_EQ(lhs, rhs) (fabs(lhs - rhs) < 10 * FLT_EPSILON * fabs(lhs + rhs)) + +@interface ABKUIUtils : NSObject + +/*! + * The currently active UIWindowScene. + */ +@property (class, nonatomic, readonly) UIWindowScene *activeWindowScene API_AVAILABLE(ios(13.0)); + +/*! + * The currently active application UIWindow. + */ +@property (class, nonatomic, readonly) UIWindow *activeApplicationWindow; + +/*! + * The currently active application UIViewController. + */ +@property (class, nonatomic, readonly) UIViewController *activeApplicationViewController; + +/*! + * The current application status bar hidden state. + */ +@property (class, readonly) BOOL applicationStatusBarHidden; + +/*! + * The current application status bar style. + */ +@property (class, readonly) UIStatusBarStyle applicationStatusBarStyle; + +/*! + * Given a class and a channel, this method searches across multiple locations and returns the appropriate + * bundle. + * @param bundleClass The class associated with the bundle. + * @param channel The channel associated with the bundle. + * @returns The bundle if available, nil otherwise. + */ ++ (NSBundle *)bundle:(Class)bundleClass channel:(ABKChannel)channel; + ++ (UIImage *)imageNamed:(NSString *)name bundle:(Class)bundleClass channel:(ABKChannel)channel; + ++ (NSString *)getLocalizedString:(NSString *)key inAppboyBundle:(NSBundle *)appboyBundle table:(NSString *)table; ++ (BOOL)objectIsValidAndNotEmpty:(id)object; ++ (Class)getModalFeedViewControllerClass; ++ (BOOL)isNotchedPhone; + ++ (UIInterfaceOrientation)getInterfaceOrientation; ++ (CGSize)getStatusBarSize; ++ (UIColor *)dynamicColorForLightColor:(UIColor *)lightColor + darkColor:(UIColor *)darkColor; ++ (BOOL)string:(NSString *)string1 isEqualToString:(NSString *)string2; + +/*! + * Verifies that one of the responders in the responder chain is kind of class aClass. + * @param responder The start of the UIResponder chain. + * @param aClass The UIResponder subclass looked for in the responder chain. + * @return YES if aClass is found in the responder chain, NO otherwise. + */ ++ (BOOL)responderChainOf:(UIResponder *)responder hasKindOfClass:(Class)aClass; + +/*! + * Verifies that one of the responders in the responder chain is prefixed by prefix. + * @param responder The start of the UIResponder chain. + * @param prefix The prefix looked for in the responder chain. + * @return YES if a class prefixed by prefix is found in the responder chain, NO otherwise. + */ ++ (BOOL)responderChainOf:(UIResponder *)responder hasClassPrefixedWith:(NSString *)prefix; + +/*! + * Creates an instance of the font associated with the text style and scaled appropriately for the + * user's selected content size category. + * + * @warning On iOS 10 / tvOS 10 and below, this method does not apply the text style to the + * resulting font. The font size is chosen according to https://apple.co/3snncd9 (Large / Default). + * + * @param textStyle The text style to use + * @param weight The weight of the font + * @return The font corresponding to the text style with weight applied to it. + */ ++ (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)textStyle weight:(UIFontWeight)weight; + +/*! + * Enables `adjustsFontForContentSizeCategory` on the label if available (iOS 10+). + * + * This method has no effect on iOS / tvOS versions prior to 10.0. + * + * @param label Any object conforming to `UIContentSizeCategoryAdjusting` + */ ++ (void)enableAdjustsFontForContentSizeCategory:(id)label; + +@end diff --git a/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.m b/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.m new file mode 100644 index 0000000000..c17ac02b69 --- /dev/null +++ b/Sources/BrazeUICompat/ABKUIUtils/ABKUIUtils.m @@ -0,0 +1,340 @@ +#import "ABKUIUtils.h" +#import "ABKSDWebImageProxy.h" + +@import BrazeKitCompat; + +static NSString *const LocalizedAppboyStringNotFound = @"not found"; +static NSUInteger const iPhoneXHeight = 2436.0; // iPhone 12 mini simulator is also this size +static NSUInteger const iPhoneXRHeight = 1792.0; +static NSUInteger const iPhoneXSMaxHeight = 2688.0; +static NSUInteger const iPhoneXRScaledHeight = 1624.0; +static NSUInteger const iPhone12 = 2532.0; // iPhone 12 pro is also this size +static NSUInteger const iPhone12ProMax = 2778.0; +static NSUInteger const iPhone12Mini = 2340.0; + +// Bundles +static NSString * const ABKUIPodCCBundleName = @"AppboyUI.ContentCards.bundle"; +static NSString * const ABKUIPodIAMBundleName = @"AppboyUI.InAppMessage.bundle"; +static NSString * const ABKUIPodNFBundleName = @"AppboyUI.NewsFeed.bundle"; + +@implementation ABKUIUtils + +#pragma mark - Bundle Helper + ++ (NSBundle *)bundle:(Class)bundleClass channel:(ABKChannel)channel { + NSBundle *bundle; + + // SPM +#if SWIFT_PACKAGE + // SWIFTPM_MODULE_BUNDLE crashes whith the current setup. + // SwiftPM generate alternative names for the bundle, we handle them both here. + bundle = [self bundleForName:@"braze_swift_sdk_BrazeUICompat.bundle" class:bundleClass]; + if (bundle != nil) { + return bundle; + } + bundle = [self bundleForName:@"braze-swift-sdk_BrazeUICompat.bundle" class:bundleClass]; + if (bundle != nil) { + return bundle; + } +#endif + +#if COCOAPODS + bundle = [self bundleForName:@"BrazeUICompat.bundle" class: bundleClass]; + if (bundle != nil) { + return bundle; + } +#endif + + return [NSBundle bundleForClass:bundleClass]; +} + ++ (nullable NSBundle *)bundleForName:(NSString *)name class:(Class)bundleClass { + NSURL *bundleURL = [[NSBundle bundleForClass:bundleClass].resourceURL URLByAppendingPathComponent:name]; + + if ([bundleURL checkResourceIsReachableAndReturnError:nil]) { + return [NSBundle bundleWithURL:bundleURL]; + } + + return nil; +} + +#pragma mark - View Hierarchy Helpers + +// Used in unit tests to mock the UIApplication instance used. ++ (UIApplication *)application { + return UIApplication.sharedApplication; +} + ++ (UIWindowScene *)activeWindowScene { + UIWindowScene *windowScene; + UIWindowScene *activeWindowScene; + + // Loop over the connected window scenes to find the last foreground active + // one. If no scene is currently in foreground state, fallback to last window + // scene in hierarchy. + for (UIScene *scene in [self application].connectedScenes) { + if (![scene isKindOfClass:[UIWindowScene class]]) { + continue; + } + + windowScene = (UIWindowScene *)scene; + if (scene.activationState == UISceneActivationStateForegroundActive) { + activeWindowScene = windowScene; + } + } + return activeWindowScene ?: windowScene; +} + ++ (UIWindow *)activeApplicationWindow { + if (@available(iOS 13.0, tvOS 13.0, *)) { + UIWindow *window = [self selectApplicationWindow:ABKUIUtils.activeWindowScene.windows]; + if (window) { + return window; + } + } + + return [self selectApplicationWindow:[self application].windows]; +} + ++ (UIViewController *)activeApplicationViewController { + return ABKUIUtils.activeApplicationWindow.rootViewController; +} + ++ (BOOL)applicationStatusBarHidden { + UIViewController *viewController = self.activeApplicationViewController; + while (viewController.childViewControllerForStatusBarHidden) { + viewController = viewController.childViewControllerForStatusBarHidden; + } + return viewController.prefersStatusBarHidden; +} + ++ (UIStatusBarStyle)applicationStatusBarStyle { + UIViewController *viewController = self.activeApplicationViewController; + while (viewController.childViewControllerForStatusBarStyle) { + viewController = viewController.childViewControllerForStatusBarStyle; + } + return viewController.preferredStatusBarStyle; +} + +/*! + * Selects the window most likely to be the application window among an array of windows. + * + * @discussion The application window should most likely be the bottommost window with a windowLevel + * set to UIWindowLevelNormal (excluding a potential ABKInAppMessageWindow currently + * being displayed). If no window respecting that condition is found, fallback to the first + * window in the hierarchy. + * + * @param windows An array of UIWindow + * @returns The UIWindow most likely to be the application window, nil if windows param is empty + */ ++ (UIWindow *)selectApplicationWindow:(NSArray *)windows { + // Dynamically gets ABKInAppMessageWindow class as it is part of AppboyUI + Class ABKInAppMessageWindow = NSClassFromString(@"ABKInAppMessageWindow"); + + // Holds all windows excluding any `ABKInAppMessageWindow` + NSMutableArray *filteredWindows = [NSMutableArray array]; + + for (UIWindow *window in windows) { + // Ignores ABKInAppMessageWindow + if (ABKInAppMessageWindow && [window isKindOfClass:[ABKInAppMessageWindow class]]) { + continue; + } + // Assumes that the application window has a windowLevel set to + // UIWindowLevelNormal + if (window.windowLevel == UIWindowLevelNormal) { + return window; + } + + [filteredWindows addObject:window]; + } + + // Fallback to first window in hierarchy + return filteredWindows.firstObject; +} + +#pragma mark - Methods + ++ (NSString *)getLocalizedString:(NSString *)key inAppboyBundle:(NSBundle *)appboyBundle table:(NSString *)table { + // Check if the app has a customized localization for the given key + NSString *localizedString = [[NSBundle mainBundle] localizedStringForKey:key + value:LocalizedAppboyStringNotFound + table:nil]; + if ([ABKUIUtils string:localizedString isEqualToString:LocalizedAppboyStringNotFound]) { + // Check Braze's localization in the given bundle + for (NSString *language in [[NSBundle mainBundle] preferredLocalizations]) { + if ([[appboyBundle localizations] containsObject:language]) { + NSBundle *languageBundle = [NSBundle bundleWithPath:[appboyBundle pathForResource:language ofType:@"lproj"]]; + localizedString = [languageBundle localizedStringForKey:key + value:LocalizedAppboyStringNotFound + table:table]; + break; + } + } + if ([ABKUIUtils string:localizedString isEqualToString:LocalizedAppboyStringNotFound]) { + // Couldn't find Braze's localization for the given key, fetch the default value for the key + // from Base.lproj. + NSBundle *appboyBaseBundle = [NSBundle bundleWithPath:[appboyBundle pathForResource:@"Base" ofType:@"lproj"]]; + localizedString = [appboyBaseBundle localizedStringForKey:key + value:LocalizedAppboyStringNotFound + table:table]; + } + } + return localizedString; +} + ++ (BOOL)objectIsValidAndNotEmpty:(id)object { + if (object != nil && object != [NSNull null]) { + if ([object isKindOfClass:[NSArray class]] || [object isKindOfClass:[NSDictionary class]]) { + return [object count] > 0; + } + if ([object isKindOfClass:[NSString class]]) { + return [object length] > 0; + } + if ([object isKindOfClass:[NSURL class]]) { + NSString *string = [(NSURL *)object absoluteString]; + return [string length] != 0; + } + return YES; + } + return NO; +} + ++ (Class)getModalFeedViewControllerClass { + return NSClassFromString(@"ABKNewsFeedViewController"); +} + ++ (BOOL)isNotchedPhone { + return ([[UIScreen mainScreen] nativeBounds].size.height == iPhoneXHeight || + [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXRHeight || + [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXSMaxHeight || + [[UIScreen mainScreen] nativeBounds].size.height == iPhoneXRScaledHeight || + [[UIScreen mainScreen] nativeBounds].size.height == iPhone12 || + [[UIScreen mainScreen] nativeBounds].size.height == iPhone12ProMax || + [[UIScreen mainScreen] nativeBounds].size.height == iPhone12Mini); +} + ++ (UIImage *)imageNamed:(NSString *)name bundle:(Class)bundleClass channel:(ABKChannel)channel { + NSBundle *bundle = [ABKUIUtils bundle:bundleClass channel:channel]; + return [UIImage imageNamed:name inBundle:bundle compatibleWithTraitCollection:nil]; +} + ++ (UIInterfaceOrientation)getInterfaceOrientation { + if (@available(iOS 13.0, *)) { + UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; + if (windowScene) { + return windowScene.interfaceOrientation; + } + } + return UIApplication.sharedApplication.statusBarOrientation; +} + ++ (CGSize)getStatusBarSize { + if (@available(iOS 13.0, *)) { + UIWindowScene *windowScene = ABKUIUtils.activeWindowScene; + if (windowScene) { + return windowScene.statusBarManager.statusBarFrame.size; + } + } + return UIApplication.sharedApplication.statusBarFrame.size; +} + +#pragma mark - Dark Theme + ++ (UIColor *)dynamicColorForLightColor:(UIColor *)lightColor + darkColor:(UIColor *)darkColor { + if (lightColor == nil || darkColor == nil) { + return lightColor; + } + +#if !TARGET_OS_TV + if (@available(iOS 13.0, *)) { + // Crashes if either darkColor or lightColor is nil + return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) { + if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) { + return darkColor; + } else { + return lightColor; + } + }]; + } else { + return lightColor; + } +#else + return lightColor; +#endif +} + +/*! + * Unlike NSString's :isEqualToString method, this method returns true rather than throwing an exception if the first or both inputs are nil + * OR the first or both inputs are NSNull. + */ ++ (BOOL)string:(NSString *)string1 isEqualToString:(NSString *)string2 { + if ((string1 == nil && string2 == nil) || ([string1 isKindOfClass:[NSNull class]] && [string2 isKindOfClass:[NSNull class]])) { + return YES; + } + if (string1 == nil || [string1 isKindOfClass:[NSNull class]] || string2 == nil || [string2 isKindOfClass:[NSNull class]]) { + return NO; + } + return [string1 isEqualToString:string2]; +} + ++ (BOOL)responderChainOf:(UIResponder *)responder hasKindOfClass:(Class)aClass { + UIResponder *resp = responder; + + while (resp && ![resp isKindOfClass:aClass]) { + resp = resp.nextResponder; + } + + return resp != nil; +} + ++ (BOOL)responderChainOf:(UIResponder *)responder hasClassPrefixedWith:(NSString *)prefix { + UIResponder *resp = responder; + + while (resp && ![NSStringFromClass(resp.class) hasPrefix:prefix]) { + resp = resp.nextResponder; + } + + return resp != nil; +} + ++ (UIFont *)preferredFontForTextStyle:(UIFontTextStyle)textStyle weight:(UIFontWeight)weight { + if (@available(iOS 11.0, tvOS 11.0, *)) { + UIFontMetrics *metrics = [UIFontMetrics metricsForTextStyle:textStyle]; + UIFontDescriptor *descriptor = [UIFontDescriptor preferredFontDescriptorWithTextStyle:textStyle]; + UIFont *font = [UIFont systemFontOfSize:descriptor.pointSize weight:weight]; + return [metrics scaledFontForFont:font]; + } else { + // https://apple.co/3snncd9 (Large / Default) + static dispatch_once_t once; + static NSDictionary *textStyleMap; + dispatch_once(&once, ^{ + textStyleMap = @{ + UIFontTextStyleTitle1: @(28.0), + UIFontTextStyleTitle2: @(22.0), + UIFontTextStyleTitle3: @(20.0), + UIFontTextStyleHeadline: @(17.0), + UIFontTextStyleBody: @(17.0), + UIFontTextStyleCallout: @(16.0), + UIFontTextStyleSubheadline: @(15.0), + UIFontTextStyleFootnote: @(13.0), + UIFontTextStyleCaption1: @(12.0), + UIFontTextStyleCaption2: @(11.0) + }; + }); + + return [UIFont systemFontOfSize:[textStyleMap[textStyle] doubleValue] + weight:weight]; + } +} + ++ (void)enableAdjustsFontForContentSizeCategory:(id)label { + if (@available(iOS 10.0, tvOS 10.0, *)) { + id adjustableLabel = label; + if ([adjustableLabel respondsToSelector:@selector(setAdjustsFontForContentSizeCategory:)]) { + adjustableLabel.adjustsFontForContentSizeCategory = YES; + } + } +} + +@end diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKBannerContentCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKBannerContentCardCell.h new file mode 120000 index 0000000000..0f1956c423 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKBannerContentCardCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKBannerContentCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKBaseContentCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKBaseContentCardCell.h new file mode 120000 index 0000000000..f5061c19d0 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKBaseContentCardCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKBaseContentCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKCaptionedImageContentCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKCaptionedImageContentCardCell.h new file mode 120000 index 0000000000..9e92b672de --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKCaptionedImageContentCardCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKCaptionedImageContentCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKClassicContentCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKClassicContentCardCell.h new file mode 120000 index 0000000000..ca7a941d99 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKClassicContentCardCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKClassicContentCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKClassicImageContentCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKClassicImageContentCardCell.h new file mode 120000 index 0000000000..a5875863f3 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKClassicImageContentCardCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKClassicImageContentCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsTableViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsTableViewController.h new file mode 120000 index 0000000000..17bb1a2f9d --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsTableViewController.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/ABKContentCardsTableViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsViewController.h new file mode 120000 index 0000000000..9dfa55e1b3 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsViewController.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/ABKContentCardsViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsWebViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsWebViewController.h new file mode 120000 index 0000000000..c896b39bbb --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKContentCardsWebViewController.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/ABKContentCardsWebViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKControlTableViewCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKControlTableViewCell.h new file mode 120000 index 0000000000..49be23419a --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKControlTableViewCell.h @@ -0,0 +1 @@ +../../ABKContentCards/ViewControllers/Cells/ABKControlTableViewCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKFeedWebViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKFeedWebViewController.h new file mode 120000 index 0000000000..b93ee25a50 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKFeedWebViewController.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/ABKFeedWebViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageFullViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageFullViewController.h new file mode 120000 index 0000000000..0b845d8423 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageFullViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageFullViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLBaseViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLBaseViewController.h new file mode 120000 index 0000000000..35daf2f039 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLBaseViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLBaseViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLFullViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLFullViewController.h new file mode 120000 index 0000000000..909e3c63a5 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLFullViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLFullViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLViewController.h new file mode 120000 index 0000000000..88244a98c0 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageHTMLViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageHTMLViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageImmersiveViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageImmersiveViewController.h new file mode 120000 index 0000000000..5fdc18ce6b --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageImmersiveViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageImmersiveViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageModalViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageModalViewController.h new file mode 120000 index 0000000000..1dd060a85b --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageModalViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageModalViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageSlideupViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageSlideupViewController.h new file mode 120000 index 0000000000..da72425390 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageSlideupViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageSlideupViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIButton.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIButton.h new file mode 120000 index 0000000000..708a2009f2 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIButton.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ABKInAppMessageUIButton.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIController.h new file mode 120000 index 0000000000..acfc19d3d3 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ABKInAppMessageUIController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIDelegate.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIDelegate.h new file mode 120000 index 0000000000..84e901b6e9 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageUIDelegate.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ABKInAppMessageUIDelegate.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageView.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageView.h new file mode 120000 index 0000000000..15d9292079 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageView.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ABKInAppMessageView.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageViewController.h new file mode 120000 index 0000000000..96d99f6008 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageViewController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindow.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindow.h new file mode 120000 index 0000000000..0e08e668c5 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindow.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ABKInAppMessageWindow.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindowController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindowController.h new file mode 120000 index 0000000000..f5effcf525 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKInAppMessageWindowController.h @@ -0,0 +1 @@ +../../ABKInAppMessage/ViewControllers/ABKInAppMessageWindowController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNFBannerCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNFBannerCardCell.h new file mode 120000 index 0000000000..2aca9c5124 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNFBannerCardCell.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/Cells/ABKNFBannerCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNFBaseCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNFBaseCardCell.h new file mode 120000 index 0000000000..c5e2573379 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNFBaseCardCell.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/Cells/ABKNFBaseCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNFCaptionedMessageCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNFCaptionedMessageCardCell.h new file mode 120000 index 0000000000..5ea5921c09 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNFCaptionedMessageCardCell.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/Cells/ABKNFCaptionedMessageCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNFClassicCardCell.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNFClassicCardCell.h new file mode 120000 index 0000000000..2d5c4ed5f1 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNFClassicCardCell.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/Cells/ABKNFClassicCardCell.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedTableViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedTableViewController.h new file mode 120000 index 0000000000..90fd4d99cf --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedTableViewController.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/ABKNewsFeedTableViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedViewController.h b/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedViewController.h new file mode 120000 index 0000000000..050ed18fc1 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKNewsFeedViewController.h @@ -0,0 +1 @@ +../../ABKNewsFeed/ViewControllers/ABKNewsFeedViewController.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKSDWebImageImageDelegate.h b/Sources/BrazeUICompat/include/AppboyUI/ABKSDWebImageImageDelegate.h new file mode 120000 index 0000000000..83bda7e72c --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKSDWebImageImageDelegate.h @@ -0,0 +1 @@ +../../ABKUIUtils/ABKSDWebImageImageDelegate.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKUIURLUtils.h b/Sources/BrazeUICompat/include/AppboyUI/ABKUIURLUtils.h new file mode 120000 index 0000000000..c78489f057 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKUIURLUtils.h @@ -0,0 +1 @@ +../../ABKUIUtils/ABKUIURLUtils.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/ABKUIUtils.h b/Sources/BrazeUICompat/include/AppboyUI/ABKUIUtils.h new file mode 120000 index 0000000000..bc4195c645 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/ABKUIUtils.h @@ -0,0 +1 @@ +../../ABKUIUtils/ABKUIUtils.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/AppboyContentCards.h b/Sources/BrazeUICompat/include/AppboyUI/AppboyContentCards.h new file mode 120000 index 0000000000..afa5ffeea0 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/AppboyContentCards.h @@ -0,0 +1 @@ +../../ABKContentCards/AppboyContentCards.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/AppboyInAppMessage.h b/Sources/BrazeUICompat/include/AppboyUI/AppboyInAppMessage.h new file mode 120000 index 0000000000..27471fd9c2 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/AppboyInAppMessage.h @@ -0,0 +1 @@ +../../ABKInAppMessage/AppboyInAppMessage.h \ No newline at end of file diff --git a/Sources/BrazeUICompat/include/AppboyUI/AppboyNewsFeed.h b/Sources/BrazeUICompat/include/AppboyUI/AppboyNewsFeed.h new file mode 120000 index 0000000000..f598cd5944 --- /dev/null +++ b/Sources/BrazeUICompat/include/AppboyUI/AppboyNewsFeed.h @@ -0,0 +1 @@ +../../ABKNewsFeed/AppboyNewsFeed.h \ No newline at end of file