diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..1ea06e4d52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,95 @@ +name: 🐞 Bug report +description: File a Bug Report for unexpected or incorrect SDK Behavior +title: '[Bug]: ' +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please consider contacting support@braze.com for faster integration troubleshooting and to avoid leaking private information to our public Github issues. + - type: dropdown + id: platform + attributes: + label: Platform + multiple: false + options: + - iOS + - Mac Catalyst + - Other + validations: + required: true + - type: input + id: platform_version + attributes: + label: Platform Version + placeholder: ex. iOS 15.4 + validations: + required: true + - type: input + id: sdk_version + attributes: + label: Braze SDK Version + placeholder: ex. 5.0.0 + validations: + required: true + - type: input + id: xcode_version + attributes: + label: Xcode Version + placeholder: ex. Xcode 13.3 + validations: + required: true + - type: dropdown + id: processor + attributes: + label: Computer Processor + multiple: false + options: + - Intel + - Apple (M1) + validations: + required: true + - type: input + id: repro_rate + attributes: + label: Repro Rate + description: How often can you reproduce this bug? + placeholder: ex. 100% of the time + validations: + required: true + - type: textarea + id: repro_steps + attributes: + label: Steps To Reproduce + description: Please provide a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) + value: | + Example: + 1. Add `braze.logCustomEvent(name: "custom_event")` in `AppDelegate.swift`. + 2. Run the app. + validations: + required: true + - type: textarea + id: expected_behavior + attributes: + label: Expected Behavior + description: What was supposed to happen? + validations: + required: true + - type: textarea + id: actual_behavior + attributes: + label: Actual Incorrect Behavior + description: What incorrect behavior happened instead? + validations: + required: true + - type: textarea + id: verbose_logs + attributes: + label: Verbose Logs + description: Please copy and paste verbose log output. This will be automatically formatted into code, so no need for backticks. + render: shell + - type: textarea + id: other_info + attributes: + label: Additional Information + description: Anything else you'd like to share? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..037775e650 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Braze Support + url: https://support.braze.com/ + about: Contact Braze Support for company or campaign-specific troubleshooting + - name: Security Issues + url: https://www.braze.com/docs/developer_guide/disclosures/security_and_vulnerability_disclosure/ + about: Please report security vulnerabilities here. diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000000..0def041a42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,39 @@ +name: ✅ Feature Request +description: Request New SDK Features +title: '[Feature]: ' +labels: ["feature-request"] +body: + - type: markdown + attributes: + value: | + Did you know: You can also submit feature requests in our [Public Roadmap Portal](https://dashboard.braze.com/resources/roadmap) + - type: textarea + id: problem + attributes: + label: What problem are you facing? + description: Help us understand what you're unable to accomplish, or what's difficult with your integration + placeholder: | + ex: I am unable to accomplish XYZ today, since the SDK does not allow me to... + validations: + required: true + - type: textarea + id: workarounds + attributes: + label: Workarounds + description: Are there any workarounds you can use? How complicated are they? + validations: + required: true + - type: textarea + id: ideal_solution + attributes: + label: Ideal Solution + description: What would your ideal solution look like? + validations: + required: false + - type: textarea + id: other_information + attributes: + label: Other Information + description: Any additional information you'd like to share? + validations: + required: false diff --git a/.github/assets/logo-dark.png b/.github/assets/logo-dark.png new file mode 100644 index 0000000000..b1aa7416a2 Binary files /dev/null and b/.github/assets/logo-dark.png differ diff --git a/.github/assets/logo-light.png b/.github/assets/logo-light.png new file mode 100644 index 0000000000..14b1b02e8d Binary files /dev/null and b/.github/assets/logo-light.png differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..51960a2e3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..3b61ed5d14 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## 5.0.0 (Early Access) + +We are excited to announce the initial release of the Braze Swift SDK! + +The Braze Swift SDK is set to replace the [current Braze iOS SDK](https://github.com/Appboy/appboy-ios-sdk/) and provides a more modern API, simpler integration, and better performance. + +### Current limitations + +The following features are not supported yet: +- Objective-C integration +- tvOS integration +- News Feed +- Content Cards diff --git a/Examples/Analytics/AppDelegate.swift b/Examples/Analytics/AppDelegate.swift new file mode 100644 index 0000000000..dacc66e459 --- /dev/null +++ b/Examples/Analytics/AppDelegate.swift @@ -0,0 +1,25 @@ +import UIKit +import BrazeKit + +// MARK: - Configure Braze + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + static var braze: Braze? = nil + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Setup Braze + var configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.logger.level = .info + let braze = Braze(configuration: configuration) + AppDelegate.braze = braze + + window?.makeKeyAndVisible() + return true + } + +} diff --git a/Examples/Analytics/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Analytics/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/Analytics/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Analytics/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Analytics/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/Analytics/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Analytics/Assets.xcassets/Contents.json b/Examples/Analytics/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Analytics/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Analytics/AuthenticationManager.swift b/Examples/Analytics/AuthenticationManager.swift new file mode 100644 index 0000000000..2b5c0aa55c --- /dev/null +++ b/Examples/Analytics/AuthenticationManager.swift @@ -0,0 +1,20 @@ +import Foundation +import BrazeKit + +class AuthenticationManager { + + struct User { + let id: String + let email: String + let birthday: Date + } + + func userDidLogin(_ user: User) { + AppDelegate.braze?.changeUser(userId: user.id) + let brazeUser = AppDelegate.braze?.user + brazeUser?.set(email: user.email) + brazeUser?.set(dateOfBirth: user.birthday) + brazeUser?.setCustomAttribute(key: "last_login_date", value: Date()) + } + +} diff --git a/Examples/Analytics/Base.lproj/LaunchScreen.storyboard b/Examples/Analytics/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/Examples/Analytics/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Analytics/CheckoutViewController.swift b/Examples/Analytics/CheckoutViewController.swift new file mode 100644 index 0000000000..dd7635decd --- /dev/null +++ b/Examples/Analytics/CheckoutViewController.swift @@ -0,0 +1,36 @@ +import UIKit + +class CheckoutViewController: UIViewController { + + /// The internal checkout identifier + var checkoutId: String = "" + + /// The list of identifiers for the products to checkout + var productIds: [String] = [] + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + AppDelegate.braze?.logCustomEvent( + name: "open_checkout_controller", + properties: [ + "checkout_id": checkoutId, + "product_ids": productIds, + ] + ) + } + + func userDidPurchase(productId: String) { + let price = self.price(productId: productId) + AppDelegate.braze?.logPurchase( + productId: productId, + currency: "USD", + price: price, + properties: ["checkout_id": checkoutId] + ) + } + + private func price(productId: String) -> Double { + [0.5, 8.0, 14.99, 0, 999.999].randomElement()! + } + +} diff --git a/Examples/Analytics/Info.plist b/Examples/Analytics/Info.plist new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/Examples/Analytics/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Examples/Analytics/Readme.swift b/Examples/Analytics/Readme.swift new file mode 100644 index 0000000000..fab0ca02aa --- /dev/null +++ b/Examples/Analytics/Readme.swift @@ -0,0 +1,73 @@ +import UIKit + +let readme = + """ + This sample presents how to use the analytics features of the SDK. + + See files: + - AppDelegate.swift + - Configure Braze + - AuthenticationManager.swift + - Identify the user + - CheckoutViewController.swift + - Log custom events + - Log purchases + """ + +let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + ("Authenticate user", "Identify the user on Braze", { _ in authenticateUser() }), + ("Present checkout", #"Log "open_checkout_controller" custom event"#, presentCheckout), + ( + "Present checkout and purchase a product", + #"Log "open_checkout_controller" custom event and a purchase"#, + presentCheckoutAndPurchase + ), +] + +// MARK: - Internal + +let authenticationManager = AuthenticationManager() + +func authenticateUser() { + let user = AuthenticationManager.User( + id: UUID().uuidString, + email: "user@example.com", + birthday: Date(timeIntervalSince1970: 0) + ) + authenticationManager.userDidLogin(user) +} + +func presentCheckout(_ viewController: ReadmeViewController) { + let (navigationController, _) = createCheckoutViewController() + viewController.present(navigationController, animated: true, completion: nil) +} + +func presentCheckoutAndPurchase(_ viewController: ReadmeViewController) { + let (navigationController, checkoutViewController) = createCheckoutViewController() + viewController.present(navigationController, animated: true) { + checkoutViewController.userDidPurchase(productId: UUID().uuidString) + } +} + +func createCheckoutViewController() -> (UINavigationController, CheckoutViewController) { + let productsIds = [ + UUID().uuidString, + UUID().uuidString, + UUID().uuidString + ] + + let checkoutViewController = CheckoutViewController() + checkoutViewController.checkoutId = UUID().uuidString + checkoutViewController.productIds = productsIds + checkoutViewController.title = "CheckoutViewController" + + if #available(iOS 13.0, *) { + checkoutViewController.view.backgroundColor = .systemGroupedBackground + } else { + checkoutViewController.view.backgroundColor = .white + } + + let navigationController = UINavigationController(rootViewController: checkoutViewController) + + return (navigationController, checkoutViewController) +} diff --git a/Examples/Common/Common.swift b/Examples/Common/Common.swift new file mode 100644 index 0000000000..2b11c9bcac --- /dev/null +++ b/Examples/Common/Common.swift @@ -0,0 +1,5 @@ +#warning("Replace with your Braze api key") +let brazeApiKey = "BRAZE_API_KEY" + +#warning("Replace with your Braze endpoint") +let brazeEndpoint = "BRAZE_ENDPOINT" diff --git a/Examples/Common/ReadmeViewController.swift b/Examples/Common/ReadmeViewController.swift new file mode 100644 index 0000000000..4a8180b4ad --- /dev/null +++ b/Examples/Common/ReadmeViewController.swift @@ -0,0 +1,79 @@ +import UIKit + +final class ReadmeViewController: UITableViewController { + + let readme: String + + let actions: [(String, String, (ReadmeViewController) -> Void)] + + init(readme: String, actions: [(String, String, (ReadmeViewController) -> Void)]) { + self.readme = readme + self.actions = actions + super.init(style: .grouped) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - UITableViewDataSource + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + actions.count + } + + override func tableView( + _ tableView: UITableView, + cellForRowAt indexPath: IndexPath + ) -> UITableViewCell { + let identifier = "cellIdentifier" + let cell = tableView.dequeueReusableCell(withIdentifier: identifier) + ?? UITableViewCell(style: .subtitle, reuseIdentifier: identifier) + cell.textLabel?.text = actions[indexPath.row].0 + cell.detailTextLabel?.text = actions[indexPath.row].1 + return cell + } + + override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let textView = UITextView() + if #available(iOS 13.0, *) { + textView.font = .monospacedSystemFont(ofSize: 12, weight: .regular) + } + textView.backgroundColor = .clear + textView.text = + """ + # Readme + + \(readme) + + \(actions.isEmpty ? "# No actions for this sample" : "# Actions") + """ + textView.textContainerInset = UIEdgeInsets(top: 64, left: 10, bottom: 16, right: 10) + return textView + } + + // MARK: - UITableViewDelegate + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let action = actions[indexPath.row].2 + action(self) + tableView.deselectRow(at: indexPath, animated: true) + } +} + +// MARK: - AutoReadme + +private var _window: UIWindow? = { + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = ReadmeViewController(readme: readme, actions: actions) + return window +}() + +extension AppDelegate { + + var window: UIWindow? { + get { _window } + set { _window = newValue } + } + +} diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..448cb04ae7 --- /dev/null +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -0,0 +1,1765 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 664913D427DD46ED0075B701 /* SDWebImageGIFViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 664913D327DD46ED0075B701 /* SDWebImageGIFViewProvider.swift */; }; + 6664E32528283F670071BF73 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32428283F670071BF73 /* BrazeKit */; }; + 6664E32728283F6C0071BF73 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32628283F6C0071BF73 /* BrazeKit */; }; + 6664E32928283F710071BF73 /* BrazeNotificationService in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32828283F710071BF73 /* BrazeNotificationService */; }; + 6664E32B28283F760071BF73 /* BrazePushStory in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32A28283F760071BF73 /* BrazePushStory */; }; + 6664E32D28283F7E0071BF73 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32C28283F7E0071BF73 /* BrazeKit */; }; + 6664E32F28283F7E0071BF73 /* BrazeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E32E28283F7E0071BF73 /* BrazeUI */; }; + 6664E33128283F830071BF73 /* BrazeKit in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E33028283F830071BF73 /* BrazeKit */; }; + 6664E33328283F830071BF73 /* BrazeLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 6664E33228283F830071BF73 /* BrazeLocation */; }; + 6683115C27CEC3E800A0A366 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6683115B27CEC3E800A0A366 /* AppDelegate.swift */; }; + 6683116527CEC3E800A0A366 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6683116427CEC3E800A0A366 /* Assets.xcassets */; }; + 6683116827CEC3E800A0A366 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 6683116627CEC3E800A0A366 /* LaunchScreen.storyboard */; }; + 6683116E27CEC40500A0A366 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6683116D27CEC40500A0A366 /* Readme.swift */; }; + 6683116F27CEC42000A0A366 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */; }; + 6683117027CEC42000A0A366 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F427C15B4B00C296A1 /* Common.swift */; }; + 66C7E6AB27DFEF6C00883CB9 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 66C7E6AA27DFEF6C00883CB9 /* SDWebImage */; }; + 66C7E6B327DFF83600883CB9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C7E6B227DFF83600883CB9 /* AppDelegate.swift */; }; + 66C7E6BC27DFF83700883CB9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66C7E6BB27DFF83700883CB9 /* Assets.xcassets */; }; + 66C7E6BF27DFF83700883CB9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 66C7E6BD27DFF83700883CB9 /* LaunchScreen.storyboard */; }; + 66C7E6C527DFF85B00883CB9 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66C7E6C427DFF85B00883CB9 /* Readme.swift */; }; + 66C7E6C627DFF88F00883CB9 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */; }; + 66C7E6C727DFF88F00883CB9 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F427C15B4B00C296A1 /* Common.swift */; }; + 66CC14E227C15A5700C296A1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14E127C15A5700C296A1 /* AppDelegate.swift */; }; + 66CC14EB27C15A5800C296A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66CC14EA27C15A5800C296A1 /* Assets.xcassets */; }; + 66CC14EE27C15A5800C296A1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 66CC14EC27C15A5800C296A1 /* LaunchScreen.storyboard */; }; + 66CC14F527C15B4B00C296A1 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F427C15B4B00C296A1 /* Common.swift */; }; + 66CC14F727C15C9100C296A1 /* AuthenticationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F627C15C9100C296A1 /* AuthenticationManager.swift */; }; + 66CC14F927C15D1100C296A1 /* CheckoutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F827C15D1100C296A1 /* CheckoutViewController.swift */; }; + 66CC150D27C18DD800C296A1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC150C27C18DD800C296A1 /* AppDelegate.swift */; }; + 66CC151627C18DD800C296A1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 66CC151527C18DD800C296A1 /* Assets.xcassets */; }; + 66CC151927C18DD800C296A1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 66CC151727C18DD800C296A1 /* LaunchScreen.storyboard */; }; + 66CC154427C1904F00C296A1 /* Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC14F427C15B4B00C296A1 /* Common.swift */; }; + 66CC154A27C19A1C00C296A1 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */; }; + 66CC154B27C19A1C00C296A1 /* ReadmeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */; }; + 66CC155327C1A73F00C296A1 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC155227C1A73F00C296A1 /* NotificationService.swift */; }; + 66CC155727C1A73F00C296A1 /* PushNotificationsServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 66CC155027C1A73F00C296A1 /* PushNotificationsServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 66CC156B27C1AAF900C296A1 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC156A27C1AAF900C296A1 /* NotificationViewController.swift */; }; + 66CC157227C1AAF900C296A1 /* PushNotificationsContentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 66CC156327C1AAF900C296A1 /* PushNotificationsContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 66CC157E27C1BC1600C296A1 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC157D27C1BC1600C296A1 /* Readme.swift */; }; + 66CC158027C1BC7F00C296A1 /* Readme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66CC157F27C1BC7F00C296A1 /* Readme.swift */; }; + 66CC158427C1C06E00C296A1 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66CC158227C1C06E00C296A1 /* UserNotifications.framework */; }; + 66CC158527C1C06E00C296A1 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66CC158327C1C06E00C296A1 /* UserNotificationsUI.framework */; }; + 66CC158627C1C08800C296A1 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66CC158227C1C06E00C296A1 /* UserNotifications.framework */; }; + 66CC158727C1C08800C296A1 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 66CC158327C1C06E00C296A1 /* UserNotificationsUI.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 66CC155527C1A73F00C296A1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 66CC14B327C157FF00C296A1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 66CC154F27C1A73F00C296A1; + remoteInfo = PushNotificationsServiceExtension; + }; + 66CC157027C1AAF900C296A1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 66CC14B327C157FF00C296A1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 66CC156227C1AAF900C296A1; + remoteInfo = PushNotificationsContentExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 664913CF27DD361F0075B701 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC155B27C1A73F00C296A1 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 66CC155727C1A73F00C296A1 /* PushNotificationsServiceExtension.appex in Embed App Extensions */, + 66CC157227C1AAF900C296A1 /* PushNotificationsContentExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 664913D327DD46ED0075B701 /* SDWebImageGIFViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDWebImageGIFViewProvider.swift; sourceTree = ""; }; + 6683115927CEC3E800A0A366 /* InAppMessages.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = InAppMessages.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6683115B27CEC3E800A0A366 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 6683116427CEC3E800A0A366 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 6683116727CEC3E800A0A366 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 6683116927CEC3E800A0A366 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 6683116D27CEC40500A0A366 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + 66C7E6B027DFF83600883CB9 /* Location.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Location.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 66C7E6B227DFF83600883CB9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 66C7E6BB27DFF83700883CB9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 66C7E6BE27DFF83700883CB9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 66C7E6C027DFF83700883CB9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 66C7E6C427DFF85B00883CB9 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + 66CC14E127C15A5700C296A1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 66CC14EA27C15A5800C296A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 66CC14ED27C15A5800C296A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 66CC14EF27C15A5800C296A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 66CC14F427C15B4B00C296A1 /* Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Common.swift; sourceTree = ""; }; + 66CC14F627C15C9100C296A1 /* AuthenticationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationManager.swift; sourceTree = ""; }; + 66CC14F827C15D1100C296A1 /* CheckoutViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckoutViewController.swift; sourceTree = ""; }; + 66CC14FF27C181CD00C296A1 /* Analytics.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Analytics.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 66CC150A27C18DD800C296A1 /* PushNotifications.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PushNotifications.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 66CC150C27C18DD800C296A1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 66CC151527C18DD800C296A1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 66CC151827C18DD800C296A1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 66CC151A27C18DD800C296A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 66CC154827C1940200C296A1 /* PushNotifications.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PushNotifications.entitlements; sourceTree = ""; }; + 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReadmeViewController.swift; sourceTree = ""; }; + 66CC155027C1A73F00C296A1 /* PushNotificationsServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PushNotificationsServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 66CC155227C1A73F00C296A1 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 66CC155427C1A73F00C296A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 66CC156327C1AAF900C296A1 /* PushNotificationsContentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PushNotificationsContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 66CC156A27C1AAF900C296A1 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + 66CC156F27C1AAF900C296A1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 66CC157827C1AB4200C296A1 /* PushNotificationsContentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PushNotificationsContentExtension.entitlements; sourceTree = ""; }; + 66CC157D27C1BC1600C296A1 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + 66CC157F27C1BC7F00C296A1 /* Readme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Readme.swift; sourceTree = ""; }; + 66CC158227C1C06E00C296A1 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/System/Library/Frameworks/UserNotifications.framework; sourceTree = DEVELOPER_DIR; }; + 66CC158327C1C06E00C296A1 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.2.sdk/System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = DEVELOPER_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6683115627CEC3E800A0A366 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6664E32F28283F7E0071BF73 /* BrazeUI in Frameworks */, + 6664E32D28283F7E0071BF73 /* BrazeKit in Frameworks */, + 66C7E6AB27DFEF6C00883CB9 /* SDWebImage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66C7E6AD27DFF83600883CB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6664E33328283F830071BF73 /* BrazeLocation in Frameworks */, + 6664E33128283F830071BF73 /* BrazeKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC14DC27C15A5700C296A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6664E32528283F670071BF73 /* BrazeKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC150727C18DD800C296A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6664E32728283F6C0071BF73 /* BrazeKit in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC154D27C1A73F00C296A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC158627C1C08800C296A1 /* UserNotifications.framework in Frameworks */, + 6664E32928283F710071BF73 /* BrazeNotificationService in Frameworks */, + 66CC158727C1C08800C296A1 /* UserNotificationsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC156027C1AAF900C296A1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC158427C1C06E00C296A1 /* UserNotifications.framework in Frameworks */, + 6664E32B28283F760071BF73 /* BrazePushStory in Frameworks */, + 66CC158527C1C06E00C296A1 /* UserNotificationsUI.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 663887CA27CEBF9A00205E71 /* Products */ = { + isa = PBXGroup; + children = ( + 66CC14FF27C181CD00C296A1 /* Analytics.app */, + 66CC150A27C18DD800C296A1 /* PushNotifications.app */, + 66CC155027C1A73F00C296A1 /* PushNotificationsServiceExtension.appex */, + 66CC156327C1AAF900C296A1 /* PushNotificationsContentExtension.appex */, + 6683115927CEC3E800A0A366 /* InAppMessages.app */, + 66C7E6B027DFF83600883CB9 /* Location.app */, + ); + name = Products; + sourceTree = ""; + }; + 6683115A27CEC3E800A0A366 /* InAppMessages */ = { + isa = PBXGroup; + children = ( + 6683116D27CEC40500A0A366 /* Readme.swift */, + 6683115B27CEC3E800A0A366 /* AppDelegate.swift */, + 664913D327DD46ED0075B701 /* SDWebImageGIFViewProvider.swift */, + 6683116427CEC3E800A0A366 /* Assets.xcassets */, + 6683116627CEC3E800A0A366 /* LaunchScreen.storyboard */, + 6683116927CEC3E800A0A366 /* Info.plist */, + ); + path = InAppMessages; + sourceTree = ""; + }; + 66C7E6B127DFF83600883CB9 /* Location */ = { + isa = PBXGroup; + children = ( + 66C7E6C427DFF85B00883CB9 /* Readme.swift */, + 66C7E6B227DFF83600883CB9 /* AppDelegate.swift */, + 66C7E6BB27DFF83700883CB9 /* Assets.xcassets */, + 66C7E6BD27DFF83700883CB9 /* LaunchScreen.storyboard */, + 66C7E6C027DFF83700883CB9 /* Info.plist */, + ); + path = Location; + sourceTree = ""; + }; + 66CC14B227C157FF00C296A1 = { + isa = PBXGroup; + children = ( + 66CC14F327C15B3500C296A1 /* Common */, + 66CC14E027C15A5700C296A1 /* Analytics */, + 66CC150B27C18DD800C296A1 /* PushNotifications */, + 66CC155127C1A73F00C296A1 /* PushNotificationsServiceExtension */, + 66CC156927C1AAF900C296A1 /* PushNotificationsContentExtension */, + 6683115A27CEC3E800A0A366 /* InAppMessages */, + 66C7E6B127DFF83600883CB9 /* Location */, + 663887CA27CEBF9A00205E71 /* Products */, + 66CC158127C1C06E00C296A1 /* Frameworks */, + ); + sourceTree = ""; + }; + 66CC14E027C15A5700C296A1 /* Analytics */ = { + isa = PBXGroup; + children = ( + 66CC157D27C1BC1600C296A1 /* Readme.swift */, + 66CC14E127C15A5700C296A1 /* AppDelegate.swift */, + 66CC14F627C15C9100C296A1 /* AuthenticationManager.swift */, + 66CC14F827C15D1100C296A1 /* CheckoutViewController.swift */, + 66CC14EA27C15A5800C296A1 /* Assets.xcassets */, + 66CC14EC27C15A5800C296A1 /* LaunchScreen.storyboard */, + 66CC14EF27C15A5800C296A1 /* Info.plist */, + ); + path = Analytics; + sourceTree = ""; + }; + 66CC14F327C15B3500C296A1 /* Common */ = { + isa = PBXGroup; + children = ( + 66CC14F427C15B4B00C296A1 /* Common.swift */, + 66CC154927C19A1C00C296A1 /* ReadmeViewController.swift */, + ); + path = Common; + sourceTree = ""; + }; + 66CC150B27C18DD800C296A1 /* PushNotifications */ = { + isa = PBXGroup; + children = ( + 66CC157F27C1BC7F00C296A1 /* Readme.swift */, + 66CC150C27C18DD800C296A1 /* AppDelegate.swift */, + 66CC154827C1940200C296A1 /* PushNotifications.entitlements */, + 66CC151527C18DD800C296A1 /* Assets.xcassets */, + 66CC151727C18DD800C296A1 /* LaunchScreen.storyboard */, + 66CC151A27C18DD800C296A1 /* Info.plist */, + ); + path = PushNotifications; + sourceTree = ""; + }; + 66CC155127C1A73F00C296A1 /* PushNotificationsServiceExtension */ = { + isa = PBXGroup; + children = ( + 66CC155227C1A73F00C296A1 /* NotificationService.swift */, + 66CC155427C1A73F00C296A1 /* Info.plist */, + ); + path = PushNotificationsServiceExtension; + sourceTree = ""; + }; + 66CC156927C1AAF900C296A1 /* PushNotificationsContentExtension */ = { + isa = PBXGroup; + children = ( + 66CC156A27C1AAF900C296A1 /* NotificationViewController.swift */, + 66CC157827C1AB4200C296A1 /* PushNotificationsContentExtension.entitlements */, + 66CC156F27C1AAF900C296A1 /* Info.plist */, + ); + path = PushNotificationsContentExtension; + sourceTree = ""; + }; + 66CC158127C1C06E00C296A1 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 66CC158227C1C06E00C296A1 /* UserNotifications.framework */, + 66CC158327C1C06E00C296A1 /* UserNotificationsUI.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6683115827CEC3E800A0A366 /* InAppMessages */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6683116C27CEC3E800A0A366 /* Build configuration list for PBXNativeTarget "InAppMessages" */; + buildPhases = ( + 6683115527CEC3E800A0A366 /* Sources */, + 6683115627CEC3E800A0A366 /* Frameworks */, + 6683115727CEC3E800A0A366 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = InAppMessages; + packageProductDependencies = ( + 66C7E6AA27DFEF6C00883CB9 /* SDWebImage */, + 6664E32C28283F7E0071BF73 /* BrazeKit */, + 6664E32E28283F7E0071BF73 /* BrazeUI */, + ); + productName = InAppMessages; + productReference = 6683115927CEC3E800A0A366 /* InAppMessages.app */; + productType = "com.apple.product-type.application"; + }; + 66C7E6AF27DFF83600883CB9 /* Location */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66C7E6C327DFF83700883CB9 /* Build configuration list for PBXNativeTarget "Location" */; + buildPhases = ( + 66C7E6AC27DFF83600883CB9 /* Sources */, + 66C7E6AD27DFF83600883CB9 /* Frameworks */, + 66C7E6AE27DFF83600883CB9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Location; + packageProductDependencies = ( + 6664E33028283F830071BF73 /* BrazeKit */, + 6664E33228283F830071BF73 /* BrazeLocation */, + ); + productName = Location; + productReference = 66C7E6B027DFF83600883CB9 /* Location.app */; + productType = "com.apple.product-type.application"; + }; + 66CC14DE27C15A5700C296A1 /* Analytics */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66CC14F027C15A5800C296A1 /* Build configuration list for PBXNativeTarget "Analytics" */; + buildPhases = ( + 66CC14DB27C15A5700C296A1 /* Sources */, + 66CC14DC27C15A5700C296A1 /* Frameworks */, + 66CC14DD27C15A5700C296A1 /* Resources */, + 664913CF27DD361F0075B701 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Analytics; + packageProductDependencies = ( + 6664E32428283F670071BF73 /* BrazeKit */, + ); + productName = Analytics; + productReference = 66CC14FF27C181CD00C296A1 /* Analytics.app */; + productType = "com.apple.product-type.application"; + }; + 66CC150927C18DD800C296A1 /* PushNotifications */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66CC151D27C18DD800C296A1 /* Build configuration list for PBXNativeTarget "PushNotifications" */; + buildPhases = ( + 66CC150627C18DD800C296A1 /* Sources */, + 66CC150727C18DD800C296A1 /* Frameworks */, + 66CC150827C18DD800C296A1 /* Resources */, + 66CC155B27C1A73F00C296A1 /* Embed App Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 66CC155627C1A73F00C296A1 /* PBXTargetDependency */, + 66CC157127C1AAF900C296A1 /* PBXTargetDependency */, + ); + name = PushNotifications; + packageProductDependencies = ( + 6664E32628283F6C0071BF73 /* BrazeKit */, + ); + productName = PushNotifications; + productReference = 66CC150A27C18DD800C296A1 /* PushNotifications.app */; + productType = "com.apple.product-type.application"; + }; + 66CC154F27C1A73F00C296A1 /* PushNotificationsServiceExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66CC155827C1A73F00C296A1 /* Build configuration list for PBXNativeTarget "PushNotificationsServiceExtension" */; + buildPhases = ( + 66CC154C27C1A73F00C296A1 /* Sources */, + 66CC154D27C1A73F00C296A1 /* Frameworks */, + 66CC154E27C1A73F00C296A1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PushNotificationsServiceExtension; + packageProductDependencies = ( + 6664E32828283F710071BF73 /* BrazeNotificationService */, + ); + productName = PushNotificationsServiceExtension; + productReference = 66CC155027C1A73F00C296A1 /* PushNotificationsServiceExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 66CC156227C1AAF900C296A1 /* PushNotificationsContentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 66CC157327C1AAF900C296A1 /* Build configuration list for PBXNativeTarget "PushNotificationsContentExtension" */; + buildPhases = ( + 66CC155F27C1AAF900C296A1 /* Sources */, + 66CC156027C1AAF900C296A1 /* Frameworks */, + 66CC156127C1AAF900C296A1 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PushNotificationsContentExtension; + packageProductDependencies = ( + 6664E32A28283F760071BF73 /* BrazePushStory */, + ); + productName = PushNotificationsContentExtension; + productReference = 66CC156327C1AAF900C296A1 /* PushNotificationsContentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 66CC14B327C157FF00C296A1 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1330; + LastUpgradeCheck = 1320; + TargetAttributes = { + 6683115827CEC3E800A0A366 = { + CreatedOnToolsVersion = 13.2.1; + }; + 66C7E6AF27DFF83600883CB9 = { + CreatedOnToolsVersion = 13.3; + }; + 66CC14DE27C15A5700C296A1 = { + CreatedOnToolsVersion = 13.2.1; + }; + 66CC150927C18DD800C296A1 = { + CreatedOnToolsVersion = 13.2.1; + }; + 66CC154F27C1A73F00C296A1 = { + CreatedOnToolsVersion = 13.2.1; + }; + 66CC156227C1AAF900C296A1 = { + CreatedOnToolsVersion = 13.2.1; + }; + }; + }; + buildConfigurationList = 66CC14B627C157FF00C296A1 /* Build configuration list for PBXProject "Examples" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 66CC14B227C157FF00C296A1; + packageReferences = ( + 664913D027DD46B50075B701 /* XCRemoteSwiftPackageReference "SDWebImage" */, + 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */, + ); + productRefGroup = 663887CA27CEBF9A00205E71 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 66CC14DE27C15A5700C296A1 /* Analytics */, + 66CC150927C18DD800C296A1 /* PushNotifications */, + 66CC154F27C1A73F00C296A1 /* PushNotificationsServiceExtension */, + 66CC156227C1AAF900C296A1 /* PushNotificationsContentExtension */, + 6683115827CEC3E800A0A366 /* InAppMessages */, + 66C7E6AF27DFF83600883CB9 /* Location */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6683115727CEC3E800A0A366 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6683116827CEC3E800A0A366 /* LaunchScreen.storyboard in Resources */, + 6683116527CEC3E800A0A366 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66C7E6AE27DFF83600883CB9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66C7E6BF27DFF83700883CB9 /* LaunchScreen.storyboard in Resources */, + 66C7E6BC27DFF83700883CB9 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC14DD27C15A5700C296A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC14EE27C15A5800C296A1 /* LaunchScreen.storyboard in Resources */, + 66CC14EB27C15A5800C296A1 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC150827C18DD800C296A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC151927C18DD800C296A1 /* LaunchScreen.storyboard in Resources */, + 66CC151627C18DD800C296A1 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC154E27C1A73F00C296A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC156127C1AAF900C296A1 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6683115527CEC3E800A0A366 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6683116E27CEC40500A0A366 /* Readme.swift in Sources */, + 6683116F27CEC42000A0A366 /* ReadmeViewController.swift in Sources */, + 664913D427DD46ED0075B701 /* SDWebImageGIFViewProvider.swift in Sources */, + 6683117027CEC42000A0A366 /* Common.swift in Sources */, + 6683115C27CEC3E800A0A366 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66C7E6AC27DFF83600883CB9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66C7E6C727DFF88F00883CB9 /* Common.swift in Sources */, + 66C7E6C627DFF88F00883CB9 /* ReadmeViewController.swift in Sources */, + 66C7E6B327DFF83600883CB9 /* AppDelegate.swift in Sources */, + 66C7E6C527DFF85B00883CB9 /* Readme.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC14DB27C15A5700C296A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC154A27C19A1C00C296A1 /* ReadmeViewController.swift in Sources */, + 66CC157E27C1BC1600C296A1 /* Readme.swift in Sources */, + 66CC14F727C15C9100C296A1 /* AuthenticationManager.swift in Sources */, + 66CC14F527C15B4B00C296A1 /* Common.swift in Sources */, + 66CC14F927C15D1100C296A1 /* CheckoutViewController.swift in Sources */, + 66CC14E227C15A5700C296A1 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC150627C18DD800C296A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC154427C1904F00C296A1 /* Common.swift in Sources */, + 66CC158027C1BC7F00C296A1 /* Readme.swift in Sources */, + 66CC154B27C19A1C00C296A1 /* ReadmeViewController.swift in Sources */, + 66CC150D27C18DD800C296A1 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC154C27C1A73F00C296A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC155327C1A73F00C296A1 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 66CC155F27C1AAF900C296A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 66CC156B27C1AAF900C296A1 /* NotificationViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 66CC155627C1A73F00C296A1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 66CC154F27C1A73F00C296A1 /* PushNotificationsServiceExtension */; + targetProxy = 66CC155527C1A73F00C296A1 /* PBXContainerItemProxy */; + }; + 66CC157127C1AAF900C296A1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 66CC156227C1AAF900C296A1 /* PushNotificationsContentExtension */; + targetProxy = 66CC157027C1AAF900C296A1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 6683116627CEC3E800A0A366 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 6683116727CEC3E800A0A366 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 66C7E6BD27DFF83700883CB9 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 66C7E6BE27DFF83700883CB9 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 66CC14EC27C15A5800C296A1 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 66CC14ED27C15A5800C296A1 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; + 66CC151727C18DD800C296A1 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 66CC151827C18DD800C296A1 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 6683116A27CEC3E800A0A366 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = InAppMessages/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.InAppMessages; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6683116B27CEC3E800A0A366 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = InAppMessages/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.InAppMessages; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 66C7E6C127DFF83700883CB9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Location/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.Location; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 66C7E6C227DFF83700883CB9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Location/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.Location; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 66CC14B727C157FF00C296A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + 66CC14B827C157FF00C296A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + 66CC14F127C15A5800C296A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Analytics/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.Analytics; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 66CC14F227C15A5800C296A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Analytics/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.Analytics; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 66CC151B27C18DD800C296A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = PushNotifications/PushNotifications.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotifications/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 66CC151C27C18DD800C296A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = PushNotifications/PushNotifications.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotifications/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 66CC155927C1A73F00C296A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotificationsServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PushNotificationsServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications.PushNotificationsServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 66CC155A27C1A73F00C296A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotificationsServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PushNotificationsServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications.PushNotificationsServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 66CC157427C1AAF900C296A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = PushNotificationsContentExtension/PushNotificationsContentExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotificationsContentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PushNotificationsContentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications.PushNotificationsContentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 66CC157527C1AAF900C296A1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_ENTITLEMENTS = PushNotificationsContentExtension/PushNotificationsContentExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 5GLZKGNWQ3; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = PushNotificationsContentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = PushNotificationsContentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.braze.PushNotifications.PushNotificationsContentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6683116C27CEC3E800A0A366 /* Build configuration list for PBXNativeTarget "InAppMessages" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6683116A27CEC3E800A0A366 /* Debug */, + 6683116B27CEC3E800A0A366 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66C7E6C327DFF83700883CB9 /* Build configuration list for PBXNativeTarget "Location" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66C7E6C127DFF83700883CB9 /* Debug */, + 66C7E6C227DFF83700883CB9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CC14B627C157FF00C296A1 /* Build configuration list for PBXProject "Examples" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CC14B727C157FF00C296A1 /* Debug */, + 66CC14B827C157FF00C296A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CC14F027C15A5800C296A1 /* Build configuration list for PBXNativeTarget "Analytics" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CC14F127C15A5800C296A1 /* Debug */, + 66CC14F227C15A5800C296A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CC151D27C18DD800C296A1 /* Build configuration list for PBXNativeTarget "PushNotifications" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CC151B27C18DD800C296A1 /* Debug */, + 66CC151C27C18DD800C296A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CC155827C1A73F00C296A1 /* Build configuration list for PBXNativeTarget "PushNotificationsServiceExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CC155927C1A73F00C296A1 /* Debug */, + 66CC155A27C1A73F00C296A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 66CC157327C1AAF900C296A1 /* Build configuration list for PBXNativeTarget "PushNotificationsContentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 66CC157427C1AAF900C296A1 /* Debug */, + 66CC157527C1AAF900C296A1 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 664913D027DD46B50075B701 /* XCRemoteSwiftPackageReference "SDWebImage" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SDWebImage/SDWebImage"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; + 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/braze-inc/braze-swift-sdk"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 6664E32428283F670071BF73 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeKit; + }; + 6664E32628283F6C0071BF73 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeKit; + }; + 6664E32828283F710071BF73 /* BrazeNotificationService */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeNotificationService; + }; + 6664E32A28283F760071BF73 /* BrazePushStory */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazePushStory; + }; + 6664E32C28283F7E0071BF73 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeKit; + }; + 6664E32E28283F7E0071BF73 /* BrazeUI */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeUI; + }; + 6664E33028283F830071BF73 /* BrazeKit */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeKit; + }; + 6664E33228283F830071BF73 /* BrazeLocation */ = { + isa = XCSwiftPackageProductDependency; + package = 6664E32328283F5F0071BF73 /* XCRemoteSwiftPackageReference "braze-swift-sdk" */; + productName = BrazeLocation; + }; + 66C7E6AA27DFEF6C00883CB9 /* SDWebImage */ = { + isa = XCSwiftPackageProductDependency; + package = 664913D027DD46B50075B701 /* XCRemoteSwiftPackageReference "SDWebImage" */; + productName = SDWebImage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 66CC14B327C157FF00C296A1 /* Project object */; +} diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000000..919434a625 --- /dev/null +++ b/Examples/Examples.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000000..18d981003d --- /dev/null +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme new file mode 100644 index 0000000000..9813141fa9 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Analytics.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/InAppMessages.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/InAppMessages.xcscheme new file mode 100644 index 0000000000..a4c3b1bf79 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/InAppMessages.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Location.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Location.xcscheme new file mode 100644 index 0000000000..78f0d7b6a2 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/Location.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme new file mode 100644 index 0000000000..5432e8c5fe --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotifications.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsContentExtension.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsContentExtension.xcscheme new file mode 100644 index 0000000000..312b236fc1 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsContentExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsServiceExtension.xcscheme b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsServiceExtension.xcscheme new file mode 100644 index 0000000000..712a6c60e7 --- /dev/null +++ b/Examples/Examples.xcodeproj/xcshareddata/xcschemes/PushNotificationsServiceExtension.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/InAppMessages/AppDelegate.swift b/Examples/InAppMessages/AppDelegate.swift new file mode 100644 index 0000000000..52fc274f18 --- /dev/null +++ b/Examples/InAppMessages/AppDelegate.swift @@ -0,0 +1,61 @@ +import UIKit +import BrazeKit +import BrazeUI + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + static var braze: Braze? = nil + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Setup Braze + var configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.logger.level = .info + let braze = Braze(configuration: configuration) + AppDelegate.braze = braze + + // - InAppMessage UI + BrazeUI.gifViewProvider = .sdWebImage + let inAppMessageUI = BrazeInAppMessageUI() + inAppMessageUI.delegate = self + braze.inAppMessagePresenter = inAppMessageUI + + window?.makeKeyAndVisible() + return true + } + +} + +extension AppDelegate: BrazeInAppMessageUIDelegate { + + func inAppMessage( + _ ui: BrazeInAppMessageUI, + prepareWith context: inout BrazeInAppMessageUI.PresentationContext + ) { + // Customize the in-app message presentation here using the context + } + + func inAppMessage( + _ ui: BrazeInAppMessageUI, + shouldProcess clickAction: Braze.InAppMessage.ClickAction, + buttonId: String?, + message: Braze.InAppMessage, + view: InAppMessageView + ) -> Bool { + // Intercept the in-app message click action here + return true + } + + func inAppMessage( + _ ui: BrazeInAppMessageUI, + didPresent message: Braze.InAppMessage, + view: InAppMessageView + ) { + // Executed when `message` is presented to the user + } + +} + diff --git a/Examples/InAppMessages/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/InAppMessages/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/InAppMessages/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/InAppMessages/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/InAppMessages/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/InAppMessages/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/InAppMessages/Assets.xcassets/Contents.json b/Examples/InAppMessages/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/InAppMessages/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/InAppMessages/Base.lproj/LaunchScreen.storyboard b/Examples/InAppMessages/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/Examples/InAppMessages/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/InAppMessages/Info.plist b/Examples/InAppMessages/Info.plist new file mode 100644 index 0000000000..0c67376eba --- /dev/null +++ b/Examples/InAppMessages/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/Examples/InAppMessages/Readme.swift b/Examples/InAppMessages/Readme.swift new file mode 100644 index 0000000000..0814cc2334 --- /dev/null +++ b/Examples/InAppMessages/Readme.swift @@ -0,0 +1,50 @@ +import UIKit +import BrazeKit + +let readme = + """ + This sample presents how to use the Braze provided in-app message UI: + + - AppDelegate.swift: + - In-app message UI configuration + - In-app message UI delegate + - SDWebImageGIFViewProvider.swift: + - Use SDWebImage to provide gif support to the in-app message UI + """ + +let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + ( + "Present local slideup in-app message", + "", + localSlideup + ), + ( + "Present local modal in-app message", + "", + localModal + ) +] + +// MARK: - Internal + +func localSlideup(_ viewController: ReadmeViewController) { + let slideup: Braze.InAppMessage = .slideup( + .init( + graphic: .icon(""), + message: "Local slideup in-app message" + ) + ) + AppDelegate.braze?.inAppMessagePresenter?.present(message: slideup) +} + +func localModal(_ viewController: ReadmeViewController) { + let modal: Braze.InAppMessage = .modal( + .init( + graphic: .icon(""), + header: "Header text", + message: "Local modal in-app message" + ) + ) + AppDelegate.braze?.inAppMessagePresenter?.present(message: modal) +} + diff --git a/Examples/InAppMessages/SDWebImageGIFViewProvider.swift b/Examples/InAppMessages/SDWebImageGIFViewProvider.swift new file mode 100644 index 0000000000..bfa9f25245 --- /dev/null +++ b/Examples/InAppMessages/SDWebImageGIFViewProvider.swift @@ -0,0 +1,20 @@ +import BrazeUI +import SDWebImage + +extension GIFViewProvider { + + /// A gif view provider using [SDWebImage](https://github.com/SDWebImage/SDWebImage) as a + /// rendering library. + static let sdWebImage = Self( + view: { SDAnimatedImageView(image: image(for: $0)) }, + updateView: { ($0 as? SDAnimatedImageView)?.image = image(for: $1) } + ) + + private static func image(for url: URL?) -> UIImage? { + guard let url = url else { return nil } + return url.pathExtension == "gif" + ? SDAnimatedImage(contentsOfFile: url.path) + : UIImage(contentsOfFile: url.path) + } + +} diff --git a/Examples/Location/AppDelegate.swift b/Examples/Location/AppDelegate.swift new file mode 100644 index 0000000000..1d5ae9e914 --- /dev/null +++ b/Examples/Location/AppDelegate.swift @@ -0,0 +1,30 @@ +import UIKit +import BrazeKit +import BrazeLocation + +// MARK: - Configure Braze + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + static var braze: Braze? = nil + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Setup Braze + var configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.logger.level = .info + configuration.location.brazeLocation = BrazeLocation() + configuration.location.automaticLocationCollection = true + configuration.location.geofencesEnabled = true + configuration.location.automaticGeofenceRequests = true + let braze = Braze(configuration: configuration) + AppDelegate.braze = braze + + window?.makeKeyAndVisible() + return true + } + +} diff --git a/Examples/Location/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Location/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/Location/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Location/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Location/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..5a3257a7d0 --- /dev/null +++ b/Examples/Location/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,93 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Location/Assets.xcassets/Contents.json b/Examples/Location/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/Location/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Location/Base.lproj/LaunchScreen.storyboard b/Examples/Location/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/Examples/Location/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Location/Info.plist b/Examples/Location/Info.plist new file mode 100644 index 0000000000..1a3b4aac87 --- /dev/null +++ b/Examples/Location/Info.plist @@ -0,0 +1,10 @@ + + + + + NSLocationAlwaysAndWhenInUseUsageDescription + Location Authorization - Always / When in Use + NSLocationWhenInUseUsageDescription + Location Authorization - When in Use + + diff --git a/Examples/Location/Readme.swift b/Examples/Location/Readme.swift new file mode 100644 index 0000000000..786899b4d7 --- /dev/null +++ b/Examples/Location/Readme.swift @@ -0,0 +1,26 @@ +import CoreLocation + +let readme = + """ + This sample presents a complete BrazeLocation integration. + + - AppDelegate.swift: + - Configure the braze instance with BrazeLocation + """ + +let actions: [(String, String, (ReadmeViewController) -> Void)] = [ + (#"Request "always" authorization"#, "", requestAlwaysAuthorization), + (#"Request "when in use" authorization"#, "", requestWhenInUseAuthorization), +] + +// MARK: - Internal + +let locationManager = CLLocationManager() + +func requestAlwaysAuthorization(_ viewController: ReadmeViewController) { + locationManager.requestAlwaysAuthorization() +} + +func requestWhenInUseAuthorization(_ viewController: ReadmeViewController) { + locationManager.requestWhenInUseAuthorization() +} diff --git a/Examples/Package.swift b/Examples/Package.swift new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Examples/PushNotifications/AppDelegate.swift b/Examples/PushNotifications/AppDelegate.swift new file mode 100644 index 0000000000..4b9b4793c9 --- /dev/null +++ b/Examples/PushNotifications/AppDelegate.swift @@ -0,0 +1,97 @@ +import UIKit +import UserNotifications +import BrazeKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + static var braze: Braze? = nil + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Setup Braze + var configuration = Braze.Configuration(apiKey: brazeApiKey, endpoint: brazeEndpoint) + configuration.logger.level = .info + configuration.push.appGroup = "group.com.braze.PushNotifications.PushStories" + let braze = Braze(configuration: configuration) + AppDelegate.braze = braze + + // Push notifications support + application.registerForRemoteNotifications() + let center = UNUserNotificationCenter.current() + center.setNotificationCategories(Braze.Notifications.categories) + center.delegate = self + center.requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in + print("Notification authorization, granted: \(granted), error: \(String(describing: error))") + } + + window?.makeKeyAndVisible() + + return true + } + + // MARK: - Push Notification support + + // - Register the device token with Braze + + func application( + _ application: UIApplication, + didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data + ) { + AppDelegate.braze?.notifications.register(deviceToken: deviceToken) + } + + // - Add support for silent notification + + func application( + _ application: UIApplication, + didReceiveRemoteNotification userInfo: [AnyHashable : Any], + fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void + ) { + if let braze = AppDelegate.braze, braze.notifications.handleBackgroundNotification( + userInfo: userInfo, + fetchCompletionHandler: completionHandler + ) { + return + } + completionHandler(.noData) + } + +} + +extension AppDelegate: UNUserNotificationCenterDelegate { + + // - Add support for push notifications + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void + ) { + if let braze = AppDelegate.braze, braze.notifications.handleUserNotification( + response: response, + withCompletionHandler: completionHandler + ) { + return + } + completionHandler() + } + + // - Add support for displaying push notification when the app is currently running in the + // foreground + + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + if #available(iOS 14, *) { + completionHandler([.list, .banner]) + } else { + completionHandler(.alert) + } + } + +} diff --git a/Examples/PushNotifications/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/PushNotifications/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000000..eb87897008 --- /dev/null +++ b/Examples/PushNotifications/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/PushNotifications/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/PushNotifications/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..9221b9bb1a --- /dev/null +++ b/Examples/PushNotifications/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/PushNotifications/Assets.xcassets/Contents.json b/Examples/PushNotifications/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Examples/PushNotifications/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/PushNotifications/Base.lproj/LaunchScreen.storyboard b/Examples/PushNotifications/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000000..865e9329f3 --- /dev/null +++ b/Examples/PushNotifications/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/PushNotifications/Info.plist b/Examples/PushNotifications/Info.plist new file mode 100644 index 0000000000..ca9a074acb --- /dev/null +++ b/Examples/PushNotifications/Info.plist @@ -0,0 +1,10 @@ + + + + + UIBackgroundModes + + remote-notification + + + diff --git a/Examples/PushNotifications/PushNotifications.entitlements b/Examples/PushNotifications/PushNotifications.entitlements new file mode 100644 index 0000000000..9cad402b51 --- /dev/null +++ b/Examples/PushNotifications/PushNotifications.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.braze.PushNotifications.PushStories + + + diff --git a/Examples/PushNotifications/Readme.swift b/Examples/PushNotifications/Readme.swift new file mode 100644 index 0000000000..4383602668 --- /dev/null +++ b/Examples/PushNotifications/Readme.swift @@ -0,0 +1,19 @@ + +let readme = + """ + This sample presents a complete push notification integration supporting: + + - PushNotifications/AppDelegate.swift: + - Silent push notifications + - Foreground push notifications + - Action buttons + - Display push notifications when app is already open + + - PushNotificationsServiceExtension: + - Rich push notification support (image, gif, audio, video) + + - PushNotificationsContentExtension: + - Braze Push Story implementation + """ + +let actions: [(String, String, (ReadmeViewController) -> Void)] = [] diff --git a/Examples/PushNotificationsContentExtension/Info.plist b/Examples/PushNotificationsContentExtension/Info.plist new file mode 100644 index 0000000000..7dc0e7de08 --- /dev/null +++ b/Examples/PushNotificationsContentExtension/Info.plist @@ -0,0 +1,29 @@ + + + + + Braze + + AppGroup + group.com.braze.PushNotifications.PushStories + + NSExtension + + NSExtensionAttributes + + UNNotificationExtensionCategory + ab_cat_push_story_v2 + UNNotificationExtensionDefaultContentHidden + + UNNotificationExtensionInitialContentSizeRatio + 0.6 + UNNotificationExtensionUserInteractionEnabled + + + NSExtensionPointIdentifier + com.apple.usernotifications.content-extension + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationViewController + + + diff --git a/Examples/PushNotificationsContentExtension/NotificationViewController.swift b/Examples/PushNotificationsContentExtension/NotificationViewController.swift new file mode 100644 index 0000000000..e4d429b3dc --- /dev/null +++ b/Examples/PushNotificationsContentExtension/NotificationViewController.swift @@ -0,0 +1,3 @@ +import BrazePushStory + +class NotificationViewController: BrazePushStory.NotificationViewController {} diff --git a/Examples/PushNotificationsContentExtension/PushNotificationsContentExtension.entitlements b/Examples/PushNotificationsContentExtension/PushNotificationsContentExtension.entitlements new file mode 100644 index 0000000000..0f4c7da6cc --- /dev/null +++ b/Examples/PushNotificationsContentExtension/PushNotificationsContentExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.braze.PushNotifications.PushStories + + + diff --git a/Examples/PushNotificationsServiceExtension/Info.plist b/Examples/PushNotificationsServiceExtension/Info.plist new file mode 100644 index 0000000000..57421ebf9b --- /dev/null +++ b/Examples/PushNotificationsServiceExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/Examples/PushNotificationsServiceExtension/NotificationService.swift b/Examples/PushNotificationsServiceExtension/NotificationService.swift new file mode 100644 index 0000000000..71d7005a43 --- /dev/null +++ b/Examples/PushNotificationsServiceExtension/NotificationService.swift @@ -0,0 +1,3 @@ +import BrazeNotificationService + +class NotificationService: BrazeNotificationService.NotificationService {} diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 0000000000..e8b519c0be --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,3 @@ +# Braze Examples + +Open `Examples.xcodeproj` and explore the different feature integrations. Follow along our [Integration Path](https://braze-inc.github.io/braze-swift-sdk/tutorials/00-integration-path) tutorials for full context. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 120000 index 0000000000..286839c777 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Sources/BrazeKitResources/Resources/braze.license \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000000..5350d0e7e8 --- /dev/null +++ b/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version:5.5 + +import PackageDescription + +let package = Package( + name: "braze-swift-sdk", + defaultLocalization: "en", + platforms: [.iOS(.v10)], + products: [ + .library( + name: "BrazeKit", + targets: ["BrazeKit", "BrazeKitResources"] + ), + .library(name: "BrazeUI", targets: ["BrazeUI"]), + .library(name: "BrazeLocation", targets: ["BrazeLocation"]), + .library(name: "BrazeNotificationService", targets: ["BrazeNotificationService"]), + .library(name: "BrazePushStory", targets: ["BrazePushStory"]), + ], + dependencies: [ + /* ${dependencies-start} */ + /* ${dependencies-end} */ + ], + targets: [ + .binaryTarget( + name: "BrazeKit", + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.0.0/BrazeKit.zip", + checksum: "38175fe6f34aeb55a5c0585b0f7f2d1c34bf1624540e2b811f6ffac7d9850d4d" + ), + .target( + name: "BrazeKitResources", + resources: [.process("Resources")] + ), + .target( + name: "BrazeUI", + dependencies: [ + .target(name: "BrazeKit"), + ], + resources: [.process("Resources")] + ), + .binaryTarget( + name: "BrazeLocation", + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.0.0/BrazeLocation.zip", + checksum: "c846f1f1fbb7c60545cd4283e672e0b2d48dfbfa75f3dc954e9a7e50bb0d5a32" + ), + .binaryTarget( + name: "BrazeNotificationService", + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.0.0/BrazeNotificationService.zip", + checksum: "08dfcfda3585c6df85e169ee3022a14b29e17fe9b5c744911d351069ecbe4078" + ), + .binaryTarget( + name: "BrazePushStory", + url: "https://github.com/braze-inc/braze-swift-sdk/releases/download/5.0.0/BrazePushStory.zip", + checksum: "eeda9db7055d5c27ee10a4453a5afa9d078a318f02829794497633eba631f917" + ), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000000..789d30c50a --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +

+ Braze Logo + Braze Logo +

+ +# Braze Swift SDK (Early Access) + +- [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) + +## Version Information +- The Braze Swift SDK supports + - iOS 10.0+ + - Mac Catalyst 13.0+ +- Xcode 13.2.1 (13C100) or newer + +## Libraries + +- `BrazeKit` - Braze Main SDK library providing support for analytics and push notifications. +- `BrazeUI` - Braze-provided user interface library for in-app messages. +- `BrazeLocation` - Braze location library providing support for location analytics and geofence monitoring. +- `BrazeNotificationService` - Braze notification service extension library providing support for [Rich Push notifications](https://www.braze.com/docs/user_guide/message_building_by_channel/push/ios/rich_notifications/). +- `BrazePushStory` - Braze notification content extension library providing support for [Push Stories](https://www.braze.com/docs/user_guide/message_building_by_channel/push/advanced_push_options/push_stories/). + +## Examples + +Explore our [examples project](/Examples) which showcases multiple features' integrations. + +## Questions? + +If you have questions, please contact [support@braze.com](mailto:support@braze.com). diff --git a/Sources/BrazeKitResources/Noop.swift b/Sources/BrazeKitResources/Noop.swift new file mode 100644 index 0000000000..7854ba3b33 --- /dev/null +++ b/Sources/BrazeKitResources/Noop.swift @@ -0,0 +1,3 @@ +// A valid Swift module must have at least one source file. +// BrazeKitResources does not include any logic and this file exist only to clear the requirement +// stated above. diff --git a/Sources/BrazeKitResources/Resources/Localization/Base.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/Base.lproj/Localizable.strings new file mode 100755 index 0000000000..521ff8a6e2 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/Base.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Accept"; +"braze.push.action.decline" = "Decline"; +"braze.push.action.confirm" = "Confirm"; +"braze.push.action.cancel" = "Cancel"; +"braze.push.action.yes" = "Yes"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "More"; +"braze.push.action.next" = "Next"; +"braze.push.action.gotoapp" = "Go To App"; + +"braze.webview.no-connection" = "Cannot establish network connection. Please try again later."; diff --git a/Sources/BrazeKitResources/Resources/Localization/ar.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/ar.lproj/Localizable.strings new file mode 100755 index 0000000000..4205c77d59 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/ar.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "قبول"; +"braze.push.action.decline" = "رفض"; +"braze.push.action.confirm" = "تم"; +"braze.push.action.cancel" = "إلغاء"; +"braze.push.action.yes" = "نعم"; +"braze.push.action.no" = "لا"; +"braze.push.action.more" = "المزيد"; +"braze.push.action.next" = "التالي"; +"braze.push.action.gotoapp" = "الذهاب الى التطبيق"; + +"braze.webview.no-connection" = "لا يمكن إجراء الاتصال بالشبكة. يرجى تكرار المحاولة لاحقا."; diff --git a/Sources/BrazeKitResources/Resources/Localization/cs.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/cs.lproj/Localizable.strings new file mode 100755 index 0000000000..68ba6fdcb6 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/cs.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Přijmout"; +"braze.push.action.decline" = "Odmítnout"; +"braze.push.action.confirm" = "Potvrdit"; +"braze.push.action.cancel" = "Zrušit"; +"braze.push.action.yes" = "Ano"; +"braze.push.action.no" = "Ne"; +"braze.push.action.more" = "Víc"; +"braze.push.action.next" = "Další"; +"braze.push.action.gotoapp" = "Přejít do aplikace"; + +"braze.webview.no-connection" = "Nelze navázat síťové připojení. Prosím zkuste to znovu později."; diff --git a/Sources/BrazeKitResources/Resources/Localization/da.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/da.lproj/Localizable.strings new file mode 100755 index 0000000000..4b053170fa --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/da.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Godkende"; +"braze.push.action.decline" = "Forfalde"; +"braze.push.action.confirm" = "Bekræfte"; +"braze.push.action.cancel" = "Slutning"; +"braze.push.action.yes" = "Ja"; +"braze.push.action.no" = "Nej"; +"braze.push.action.more" = "Mere"; +"braze.push.action.next" = "Fortsæt"; +"braze.push.action.gotoapp" = "Gå til appen"; + +"braze.webview.no-connection" = "Kan ikke etablere netværksforbindelse. Prøv venligst senere."; diff --git a/Sources/BrazeKitResources/Resources/Localization/de.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/de.lproj/Localizable.strings new file mode 100755 index 0000000000..6e31c24507 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/de.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Zustimmen"; +"braze.push.action.decline" = "Ablehnen"; +"braze.push.action.confirm" = "Bestätigen"; +"braze.push.action.cancel" = "Abbrechen"; +"braze.push.action.yes" = "Ja"; +"braze.push.action.no" = "Nein"; +"braze.push.action.more" = "Mehr"; +"braze.push.action.next" = "Weiter"; +"braze.push.action.gotoapp" = "Zur App"; + +"braze.webview.no-connection" = "Netzwerkverbindung kann nicht aufgebaut werden. Bitte später noch einmal versuchen."; diff --git a/Sources/BrazeKitResources/Resources/Localization/en.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/en.lproj/Localizable.strings new file mode 100755 index 0000000000..521ff8a6e2 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/en.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Accept"; +"braze.push.action.decline" = "Decline"; +"braze.push.action.confirm" = "Confirm"; +"braze.push.action.cancel" = "Cancel"; +"braze.push.action.yes" = "Yes"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "More"; +"braze.push.action.next" = "Next"; +"braze.push.action.gotoapp" = "Go To App"; + +"braze.webview.no-connection" = "Cannot establish network connection. Please try again later."; diff --git a/Sources/BrazeKitResources/Resources/Localization/es-419.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/es-419.lproj/Localizable.strings new file mode 100755 index 0000000000..5e99818259 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/es-419.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aceptar"; +"braze.push.action.decline" = "Declinar"; +"braze.push.action.confirm" = "Confirmar"; +"braze.push.action.cancel" = "Cancelar"; +"braze.push.action.yes" = "Si"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "Más"; +"braze.push.action.next" = "Siguiente"; +"braze.push.action.gotoapp" = "Acceder a la aplicación"; + +"braze.webview.no-connection" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeKitResources/Resources/Localization/es-MX.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/es-MX.lproj/Localizable.strings new file mode 100755 index 0000000000..5e99818259 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/es-MX.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aceptar"; +"braze.push.action.decline" = "Declinar"; +"braze.push.action.confirm" = "Confirmar"; +"braze.push.action.cancel" = "Cancelar"; +"braze.push.action.yes" = "Si"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "Más"; +"braze.push.action.next" = "Siguiente"; +"braze.push.action.gotoapp" = "Acceder a la aplicación"; + +"braze.webview.no-connection" = "No se puede establecer conexión con la red. Por favor, vuelva a intentarlo más tarde."; diff --git a/Sources/BrazeKitResources/Resources/Localization/es.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/es.lproj/Localizable.strings new file mode 100755 index 0000000000..46441edcd6 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/es.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aceptar"; +"braze.push.action.decline" = "Declinar"; +"braze.push.action.confirm" = "Confirmar"; +"braze.push.action.cancel" = "Cancelar"; +"braze.push.action.yes" = "Si"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "Más"; +"braze.push.action.next" = "Siguiente"; +"braze.push.action.gotoapp" = "Ir al App"; + +"braze.webview.no-connection" = "No se puede establecer conexión de red. Por favor inténtelo más tarde."; diff --git a/Sources/BrazeKitResources/Resources/Localization/et.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/et.lproj/Localizable.strings new file mode 100755 index 0000000000..100b9a13fd --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/et.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Nõustu"; +"braze.push.action.decline" = "Keeldu"; +"braze.push.action.confirm" = "Kinnita"; +"braze.push.action.cancel" = "Tühista"; +"braze.push.action.yes" = "Jah"; +"braze.push.action.no" = "Ei"; +"braze.push.action.more" = "Veel"; +"braze.push.action.next" = "Järgmine"; +"braze.push.action.gotoapp" = "Mine rakendusse"; + +"braze.webview.no-connection" = "Võrguühenduse loomine ebaõnnestus. Palun proovige hiljem uuesti."; diff --git a/Sources/BrazeKitResources/Resources/Localization/fi.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/fi.lproj/Localizable.strings new file mode 100755 index 0000000000..4a32de8f42 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/fi.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Hyväksya"; +"braze.push.action.decline" = "Peruuttaa"; +"braze.push.action.confirm" = "Hyväksya"; +"braze.push.action.cancel" = "Peruuttaa"; +"braze.push.action.yes" = "Kyllä"; +"braze.push.action.no" = "Ei"; +"braze.push.action.more" = "Lisää"; +"braze.push.action.next" = "Seuraava"; +"braze.push.action.gotoapp" = "Siirry sovellukseen"; + +"braze.webview.no-connection" = "Verkkoyhteyttä ei voida luoda. Yritä myöhemmin uudelleen."; diff --git a/Sources/BrazeKitResources/Resources/Localization/fil.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/fil.lproj/Localizable.strings new file mode 100755 index 0000000000..ee9cb36f4e --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/fil.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Tanggapin"; +"braze.push.action.decline" = "Tanggihan"; +"braze.push.action.confirm" = "Kumpirmahin"; +"braze.push.action.cancel" = "Kanselahin"; +"braze.push.action.yes" = "Oo"; +"braze.push.action.no" = "Hindi"; +"braze.push.action.more" = "Higit Pa"; +"braze.push.action.next" = "Susunod"; +"braze.push.action.gotoapp" = "Magpunta Sa App"; + +"braze.webview.no-connection" = "Hindi makapagtatag ng koneksyon sa network. angyaring subukan muli mamaya."; diff --git a/Sources/BrazeKitResources/Resources/Localization/fr.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/fr.lproj/Localizable.strings new file mode 100755 index 0000000000..72fcd3592e --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/fr.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Accepter"; +"braze.push.action.decline" = "Refuser"; +"braze.push.action.confirm" = "Confirmer"; +"braze.push.action.cancel" = "Annuler"; +"braze.push.action.yes" = "Oui"; +"braze.push.action.no" = "Non"; +"braze.push.action.more" = "Plus"; +"braze.push.action.next" = "Suivant"; +"braze.push.action.gotoapp" = "Lancer l'application"; + +"braze.webview.no-connection" = "Impossible d'établir la connexion réseau. Veuillez réessayer ultérieurement."; diff --git a/Sources/BrazeKitResources/Resources/Localization/he.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/he.lproj/Localizable.strings new file mode 100644 index 0000000000..b19e510c0b --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/he.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "קבל"; +"braze.push.action.decline" = "סרב"; +"braze.push.action.confirm" = "אשר"; +"braze.push.action.cancel" = "בטל"; +"braze.push.action.yes" = "כן"; +"braze.push.action.no" = "לא"; +"braze.push.action.more" = "עוד"; +"braze.push.action.next" = "הבא"; +"braze.push.action.gotoapp" = "עבור אל האפליקציה"; + +"braze.webview.no-connection" = ".לא ניתן לקבוע חיבור רשת.בבקשה נסה שוב בקרוב"; diff --git a/Sources/BrazeKitResources/Resources/Localization/hi.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/hi.lproj/Localizable.strings new file mode 100755 index 0000000000..00c76da126 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/hi.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "स्वीकार करें"; +"braze.push.action.decline" = "अस्वीकार करें"; +"braze.push.action.confirm" = "पुष्टि करें"; +"braze.push.action.cancel" = "रद्द करें"; +"braze.push.action.yes" = "हाँ"; +"braze.push.action.no" = "नहीं"; +"braze.push.action.more" = "अधिक"; +"braze.push.action.next" = "अगला"; +"braze.push.action.gotoapp" = "ऐप पर जाएं"; + +"braze.webview.no-connection" = "नेटवर्क कनेक्शन स्थापित नहीं हो रहा है।. कृपया बाद में दोबारा प्रयास करें।."; diff --git a/Sources/BrazeKitResources/Resources/Localization/id.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/id.lproj/Localizable.strings new file mode 100755 index 0000000000..4ea53451d7 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/id.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Setuju"; +"braze.push.action.decline" = "Tidak setuju"; +"braze.push.action.confirm" = "Memastikan"; +"braze.push.action.cancel" = "Membatal"; +"braze.push.action.yes" = "Ya"; +"braze.push.action.no" = "Tidak"; +"braze.push.action.more" = "Lebih"; +"braze.push.action.next" = "Berikutnya"; +"braze.push.action.gotoapp" = "Buka Aplikasi"; + +"braze.webview.no-connection" = "Tidak bisa melakukan koneksi jaringan. Coba lagi nanti."; diff --git a/Sources/BrazeKitResources/Resources/Localization/it.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/it.lproj/Localizable.strings new file mode 100755 index 0000000000..f255a79c7a --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/it.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Accetta"; +"braze.push.action.decline" = "Rifiuta"; +"braze.push.action.confirm" = "Conferma"; +"braze.push.action.cancel" = "Annulla"; +"braze.push.action.yes" = "Sì"; +"braze.push.action.no" = "No"; +"braze.push.action.more" = "Altro"; +"braze.push.action.next" = "Avanti"; +"braze.push.action.gotoapp" = "Vai all'app"; + +"braze.webview.no-connection" = "Impossibile stabilire una connessione di rete. Riprovare più tardi."; diff --git a/Sources/BrazeKitResources/Resources/Localization/ja.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/ja.lproj/Localizable.strings new file mode 100755 index 0000000000..93a39186f1 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/ja.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "承認"; +"braze.push.action.decline" = "拒否"; +"braze.push.action.confirm" = "確認"; +"braze.push.action.cancel" = "キャンセル"; +"braze.push.action.yes" = "はい"; +"braze.push.action.no" = "いいえ"; +"braze.push.action.more" = "もっと見る"; +"braze.push.action.next" = "次へ"; +"braze.push.action.gotoapp" = "アプリを開く"; + +"braze.webview.no-connection" = "ネットワークに接続できません。後でもう一度試してください。"; diff --git a/Sources/BrazeKitResources/Resources/Localization/km.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/km.lproj/Localizable.strings new file mode 100755 index 0000000000..59df04b7ad --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/km.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "ព្រមទទួល"; +"braze.push.action.decline" = "បដិសេធ"; +"braze.push.action.confirm" = "បញ្ជាក់អះអាង"; +"braze.push.action.cancel" = "បោះបង់"; +"braze.push.action.yes" = "បាទ"; +"braze.push.action.no" = "ទេ"; +"braze.push.action.more" = "ច្រើនបន្ថែមទៀត"; +"braze.push.action.next" = "បន្ទាប់"; +"braze.push.action.gotoapp" = "ចូលទៅកូនកម្មវិធី"; + +"braze.webview.no-connection" = "មិនអាចបង្កើតបណ្តាញតភ្ជាប់បានទេ. សូមព្យាយាមម្តងទៀតនៅពេលក្រោយ."; diff --git a/Sources/BrazeKitResources/Resources/Localization/ko.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/ko.lproj/Localizable.strings new file mode 100755 index 0000000000..64fe14f7cf --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/ko.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "승인"; +"braze.push.action.decline" = "거절"; +"braze.push.action.confirm" = "확인"; +"braze.push.action.cancel" = "취소"; +"braze.push.action.yes" = "네"; +"braze.push.action.no" = "아니오"; +"braze.push.action.more" = "더보기"; +"braze.push.action.next" = "다음"; +"braze.push.action.gotoapp" = "앱으로 이동"; + +"braze.webview.no-connection" = "네트워크 연결을 할 수 없습니다. 나중에 다시 시도해 주십시오."; diff --git a/Sources/BrazeKitResources/Resources/Localization/lo.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/lo.lproj/Localizable.strings new file mode 100755 index 0000000000..31f1f9fe83 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/lo.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "ຍອມ​ຮັບ"; +"braze.push.action.decline" = "ປະຕິເສດ"; +"braze.push.action.confirm" = "ຢືນຢັນ"; +"braze.push.action.cancel" = "ຍົກເລີກ"; +"braze.push.action.yes" = "ແມ່ນ"; +"braze.push.action.no" = "ບໍ່"; +"braze.push.action.more" = "ເພີ່ມເຕີມ"; +"braze.push.action.next" = "ຕໍ່ໄປ"; +"braze.push.action.gotoapp" = "ເຂົ້າສູ່ແອັບ"; + +"braze.webview.no-connection" = "ບໍ່​ສາ​ມາດ​ຕັ້ງ​ການ​ເຊື່ອມ​ຕໍ່​ເຄືອ​ຂ່າຍ​ໄດ້. ກະ​ລຸ​ນາ​ລອງ​ໃໝ່​ພາຍ​ຫຼັງ."; diff --git a/Sources/BrazeKitResources/Resources/Localization/ms.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/ms.lproj/Localizable.strings new file mode 100755 index 0000000000..9c297c0040 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/ms.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Terima"; +"braze.push.action.decline" = "No"; +"braze.push.action.confirm" = "Pasti"; +"braze.push.action.cancel" = "Batal"; +"braze.push.action.yes" = "Ya"; +"braze.push.action.no" = "Tiada"; +"braze.push.action.more" = "Lebih"; +"braze.push.action.next" = "Berikut"; +"braze.push.action.gotoapp" = "Pergi ke app"; + +"braze.webview.no-connection" = "Tidak boleh membuat sambungan rangkaian. Sila cuba kemudian."; diff --git a/Sources/BrazeKitResources/Resources/Localization/my.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/my.lproj/Localizable.strings new file mode 100755 index 0000000000..72470de405 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/my.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "လက္ခံရယူသည္"; +"braze.push.action.decline" = "ျငင္းဆိုသည္"; +"braze.push.action.confirm" = "အတည္ျပဳသည္"; +"braze.push.action.cancel" = "ဖ်က္သိမ္းသည္"; +"braze.push.action.yes" = "ဟုတ္ပါသည္"; +"braze.push.action.no" = "မဟုတ္ပါ"; +"braze.push.action.more" = "ထပ္မံ၍။ ေနာက္ထပ္"; +"braze.push.action.next" = "နောက်သို့"; +"braze.push.action.gotoapp" = "အထူးအက်ပ်"; + +"braze.webview.no-connection" = "ကြန္ယက္ဆက္သြယ္ျခင္း မျပဳလုပ္ႏိုင္ပါ။. ေက်းဇူးျပဳ၍ ထပ္မံၾကိဳးစားၾကည္႕ပါ။."; diff --git a/Sources/BrazeKitResources/Resources/Localization/nb.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/nb.lproj/Localizable.strings new file mode 100755 index 0000000000..ffa3d810aa --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/nb.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Akseptere"; +"braze.push.action.decline" = "Avslå"; +"braze.push.action.confirm" = "Bekrefte"; +"braze.push.action.cancel" = "Kansellere"; +"braze.push.action.yes" = "Ja"; +"braze.push.action.no" = "Nej"; +"braze.push.action.more" = "Mer"; +"braze.push.action.next" = "Next"; +"braze.push.action.gotoapp" = "Go To App"; + +"braze.webview.no-connection" = "Kan ikke etablere nettverkstilkobling. Vennligst prøv igjen senere."; diff --git a/Sources/BrazeKitResources/Resources/Localization/nl.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/nl.lproj/Localizable.strings new file mode 100755 index 0000000000..64fa17b08c --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/nl.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aanvaarden"; +"braze.push.action.decline" = "Weigeren"; +"braze.push.action.confirm" = "Bevestigen"; +"braze.push.action.cancel" = "Annuleren"; +"braze.push.action.yes" = "Ja"; +"braze.push.action.no" = "Nee"; +"braze.push.action.more" = "Meer"; +"braze.push.action.next" = "Volgende"; +"braze.push.action.gotoapp" = "Naar de app gaan"; + +"braze.webview.no-connection" = "Kan geen netwerkverbinding maken. Probeer het later opnieuw."; diff --git a/Sources/BrazeKitResources/Resources/Localization/pl.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/pl.lproj/Localizable.strings new file mode 100755 index 0000000000..082b282cad --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/pl.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Zaakceptować"; +"braze.push.action.decline" = "Utrata"; +"braze.push.action.confirm" = "Utwierdzić"; +"braze.push.action.cancel" = "Anuluj"; +"braze.push.action.yes" = "Tak"; +"braze.push.action.no" = "Nie"; +"braze.push.action.more" = "Więcej"; +"braze.push.action.next" = "Następny"; +"braze.push.action.gotoapp" = "Przejdź do aplikacji"; + +"braze.webview.no-connection" = "Nie można ustanowić połączenia z siecią. Proszę spróbować ponownie później."; diff --git a/Sources/BrazeKitResources/Resources/Localization/pt-PT.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/pt-PT.lproj/Localizable.strings new file mode 100755 index 0000000000..7e2afc1da6 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/pt-PT.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aceitar"; +"braze.push.action.decline" = "Rejeitar"; +"braze.push.action.confirm" = "Confirmar"; +"braze.push.action.cancel" = "Cancelar"; +"braze.push.action.yes" = "Sim"; +"braze.push.action.no" = "Não"; +"braze.push.action.more" = "Mais"; +"braze.push.action.next" = "Próximo"; +"braze.push.action.gotoapp" = "Ir Para A Aplicação"; + +"braze.webview.no-connection" = "Não é possível estabelecer a ligação à rede. Por favor, tente mais tarde."; diff --git a/Sources/BrazeKitResources/Resources/Localization/pt.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/pt.lproj/Localizable.strings new file mode 100755 index 0000000000..392150dffb --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/pt.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Aceitar"; +"braze.push.action.decline" = "Rejeitar"; +"braze.push.action.confirm" = "Confirmar"; +"braze.push.action.cancel" = "Cancelar"; +"braze.push.action.yes" = "Sim"; +"braze.push.action.no" = "Não"; +"braze.push.action.more" = "Mais"; +"braze.push.action.next" = "Proxima"; +"braze.push.action.gotoapp" = "Abrir o applicativo"; + +"braze.webview.no-connection" = "Não é possível estabelecer uma conexão de rede. Tente novamente mais tarde."; diff --git a/Sources/BrazeKitResources/Resources/Localization/ru.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/ru.lproj/Localizable.strings new file mode 100755 index 0000000000..a22c27022b --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/ru.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "согласен"; +"braze.push.action.decline" = "отклонить"; +"braze.push.action.confirm" = "утверждать"; +"braze.push.action.cancel" = "отменить"; +"braze.push.action.yes" = "да"; +"braze.push.action.no" = "нет"; +"braze.push.action.more" = "ещё"; +"braze.push.action.next" = "следующий"; +"braze.push.action.gotoapp" = "открыть приложение"; + +"braze.webview.no-connection" = "Невозможно установить сетевое подключение. Пожалуйста, повторите попытку позже."; diff --git a/Sources/BrazeKitResources/Resources/Localization/sv.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/sv.lproj/Localizable.strings new file mode 100755 index 0000000000..1421318494 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/sv.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "Godkänna"; +"braze.push.action.decline" = "Avböja"; +"braze.push.action.confirm" = "Bekräfta"; +"braze.push.action.cancel" = "Avbryt"; +"braze.push.action.yes" = "Ja"; +"braze.push.action.no" = "Nej"; +"braze.push.action.more" = "Mer"; +"braze.push.action.next" = "Nästa"; +"braze.push.action.gotoapp" = "Gå till appen"; + +"braze.webview.no-connection" = "Det gick inte att skapa en nätverksanslutning. Försök igen senare."; diff --git a/Sources/BrazeKitResources/Resources/Localization/th.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/th.lproj/Localizable.strings new file mode 100755 index 0000000000..11cb253d38 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/th.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "ยอมรับ"; +"braze.push.action.decline" = "ปฏิเสธ"; +"braze.push.action.confirm" = "ยืนยัน"; +"braze.push.action.cancel" = "ยกเลิก"; +"braze.push.action.yes" = "ใช่"; +"braze.push.action.no" = "ไม่ใช่"; +"braze.push.action.more" = "เพิ่มเติม"; +"braze.push.action.next" = "ถัดไป"; +"braze.push.action.gotoapp" = "ไปยังแอป"; + +"braze.webview.no-connection" = "ไม่สามารถสร้างการเชื่อมต่อเครือข่าย. กรุณาลองใหม่ภายหลัง."; diff --git a/Sources/BrazeKitResources/Resources/Localization/uk.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/uk.lproj/Localizable.strings new file mode 100755 index 0000000000..15ae4b17be --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/uk.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "згоден"; +"braze.push.action.decline" = "відхилити"; +"braze.push.action.confirm" = "стверджувати"; +"braze.push.action.cancel" = "скасувати"; +"braze.push.action.yes" = "та"; +"braze.push.action.no" = "ні"; +"braze.push.action.more" = "ще"; +"braze.push.action.next" = "наступний"; +"braze.push.action.gotoapp" = "відкрити програму"; + +"braze.webview.no-connection" = "Не вдається встановити підключення до мережі. Будь-ласка спробуйте пізніше."; diff --git a/Sources/BrazeKitResources/Resources/Localization/vi.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/vi.lproj/Localizable.strings new file mode 100755 index 0000000000..9e419f4c8f --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/vi.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "bằng lòng"; +"braze.push.action.decline" = "từ chối"; +"braze.push.action.confirm" = "xác nhận"; +"braze.push.action.cancel" = "hủy bỏ"; +"braze.push.action.yes" = "vâng"; +"braze.push.action.no" = "không"; +"braze.push.action.more" = "thêm nửa"; +"braze.push.action.next" = "Tiếp theo"; +"braze.push.action.gotoapp" = "Mở Ứng dụng"; + +"braze.webview.no-connection" = "Không thể thiết lập kết nối mạng. Vui lòng thử lại sau."; diff --git a/Sources/BrazeKitResources/Resources/Localization/zh-HK.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/zh-HK.lproj/Localizable.strings new file mode 100755 index 0000000000..a275e6aece --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/zh-HK.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "接受"; +"braze.push.action.decline" = "謝絕"; +"braze.push.action.confirm" = "確認"; +"braze.push.action.cancel" = "取消"; +"braze.push.action.yes" = "是"; +"braze.push.action.no" = "否"; +"braze.push.action.more" = "更多"; +"braze.push.action.next" = "下一個"; +"braze.push.action.gotoapp" = "打開程式"; + +"braze.webview.no-connection" = "無法建立網路連線。請稍候再試。"; diff --git a/Sources/BrazeKitResources/Resources/Localization/zh-Hans.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/zh-Hans.lproj/Localizable.strings new file mode 100755 index 0000000000..965a52e667 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "接受"; +"braze.push.action.decline" = "谢绝"; +"braze.push.action.confirm" = "确认"; +"braze.push.action.cancel" = "取消"; +"braze.push.action.yes" = "是"; +"braze.push.action.no" = "否"; +"braze.push.action.more" = "更多"; +"braze.push.action.next" = "下一个"; +"braze.push.action.gotoapp" = "打开应用"; + +"braze.webview.no-connection" = "无法建立网络连接。请稍候再试。"; diff --git a/Sources/BrazeKitResources/Resources/Localization/zh-Hant.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/zh-Hant.lproj/Localizable.strings new file mode 100755 index 0000000000..a275e6aece --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/zh-Hant.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "接受"; +"braze.push.action.decline" = "謝絕"; +"braze.push.action.confirm" = "確認"; +"braze.push.action.cancel" = "取消"; +"braze.push.action.yes" = "是"; +"braze.push.action.no" = "否"; +"braze.push.action.more" = "更多"; +"braze.push.action.next" = "下一個"; +"braze.push.action.gotoapp" = "打開程式"; + +"braze.webview.no-connection" = "無法建立網路連線。請稍候再試。"; diff --git a/Sources/BrazeKitResources/Resources/Localization/zh-TW.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/zh-TW.lproj/Localizable.strings new file mode 100755 index 0000000000..a275e6aece --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/zh-TW.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "接受"; +"braze.push.action.decline" = "謝絕"; +"braze.push.action.confirm" = "確認"; +"braze.push.action.cancel" = "取消"; +"braze.push.action.yes" = "是"; +"braze.push.action.no" = "否"; +"braze.push.action.more" = "更多"; +"braze.push.action.next" = "下一個"; +"braze.push.action.gotoapp" = "打開程式"; + +"braze.webview.no-connection" = "無法建立網路連線。請稍候再試。"; diff --git a/Sources/BrazeKitResources/Resources/Localization/zh.lproj/Localizable.strings b/Sources/BrazeKitResources/Resources/Localization/zh.lproj/Localizable.strings new file mode 100755 index 0000000000..965a52e667 --- /dev/null +++ b/Sources/BrazeKitResources/Resources/Localization/zh.lproj/Localizable.strings @@ -0,0 +1,11 @@ +"braze.push.action.accept" = "接受"; +"braze.push.action.decline" = "谢绝"; +"braze.push.action.confirm" = "确认"; +"braze.push.action.cancel" = "取消"; +"braze.push.action.yes" = "是"; +"braze.push.action.no" = "否"; +"braze.push.action.more" = "更多"; +"braze.push.action.next" = "下一个"; +"braze.push.action.gotoapp" = "打开应用"; + +"braze.webview.no-connection" = "无法建立网络连接。请稍候再试。"; diff --git a/Sources/BrazeKitResources/Resources/braze.license b/Sources/BrazeKitResources/Resources/braze.license new file mode 100644 index 0000000000..466b967fcc --- /dev/null +++ b/Sources/BrazeKitResources/Resources/braze.license @@ -0,0 +1,9 @@ +Copyright (c) 2022 Braze, Inc. +All rights reserved. + +* Use of source code or binaries contained within Braze’s SDKs is permitted only to enable use of the Braze platform by customers of Braze. +* Modification of source code and inclusion in mobile apps is explicitly allowed provided that all other conditions are met. +* Neither the name of Braze nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. +* Redistribution of source code or binaries is disallowed except with specific prior written permission. Any such redistribution must retain the above copyright notice, this list of conditions and the following disclaimer. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Sources/BrazeKitResources/Resources/minizip-ng.license b/Sources/BrazeKitResources/Resources/minizip-ng.license new file mode 100644 index 0000000000..3b6c4e142e --- /dev/null +++ b/Sources/BrazeKitResources/Resources/minizip-ng.license @@ -0,0 +1,17 @@ +Condition of use and distribution are the same as zlib: + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgement in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. diff --git a/Sources/BrazeUI/BrazeUIMocks.swift b/Sources/BrazeUI/BrazeUIMocks.swift new file mode 100644 index 0000000000..254b818377 --- /dev/null +++ b/Sources/BrazeUI/BrazeUIMocks.swift @@ -0,0 +1,93 @@ +import Foundation +import UIKit + +#if DEBUG + + extension URL { + + /// Creates a mock png image, store it on disk and returns the file url. + /// - Parameters: + /// - width: The pixel width of the image. + /// - height: The pixel height of the image + /// - textSize: The size of the text at the center of the image, set to `nil` to automatically + /// infer the size from the image width and height. + /// - textColor: The text color. + /// - backgroundColor: The background color. + /// - Returns: An url for a mock image. + public static func mockImage( + width: CGFloat, + height: CGFloat, + textSize: CGFloat? = nil, + textColor: UIColor = .white, + backgroundColor: UIColor = .systemBlue + ) -> URL { + let frame = CGRect(x: 0, y: 0, width: width, height: height) + let textSize = textSize ?? min(floor(height / 6), floor(width / 12)) + let lineWidth = max(width / 50, 8) + let cornerLength = width / 20 + + // Draw image to png data + let format = UIGraphicsImageRendererFormat.default() + format.scale = 1 + let data = UIGraphicsImageRenderer(size: frame.size, format: format).pngData { ctx in + backgroundColor.set() + ctx.fill(frame) + + // Corners + func drawCorner(at pos: CGPoint) { + ctx.cgContext.setStrokeColor(UIColor.black.withAlphaComponent(0.3).cgColor) + ctx.cgContext.setLineWidth(lineWidth) + var pos = pos + pos.x -= cornerLength + ctx.cgContext.move(to: pos) + pos.x += 2 * cornerLength + ctx.cgContext.addLine(to: pos) + pos.x -= cornerLength + pos.y -= cornerLength + ctx.cgContext.move(to: pos) + pos.y += 2 * cornerLength + ctx.cgContext.addLine(to: pos) + ctx.cgContext.drawPath(using: .fillStroke) + } + drawCorner(at: .init(x: frame.minX, y: frame.minY)) + drawCorner(at: .init(x: frame.minX, y: frame.maxY)) + drawCorner(at: .init(x: frame.maxX, y: frame.minY)) + drawCorner(at: .init(x: frame.maxX, y: frame.maxY)) + + // Draw text + let font: UIFont + if #available(iOS 13.0, *) { + font = UIFont.monospacedSystemFont(ofSize: textSize, weight: .regular) + } else { + font = UIFont(name: "Courier", size: textSize)! + } + let style = NSMutableParagraphStyle() + style.alignment = .center + style.minimumLineHeight = frame.height / 2 + textSize / 2 + let text = NSAttributedString( + string: "\(Int(width))x\(Int(height))", + attributes: [ + .font: font, + .foregroundColor: textColor, + .paragraphStyle: style, + ] + ) + text.draw(in: frame) + } + + // Write to temporary cache + let cacheUrl = try! FileManager.default.url( + for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false + ) + let imageUrl = cacheUrl.appendingPathComponent("\(Int(width))x\(Int(height)).png") + try! data.write(to: imageUrl) + + return imageUrl + } + + } + +#endif diff --git a/Sources/BrazeUI/Dependencies/Align.swift b/Sources/BrazeUI/Dependencies/Align.swift new file mode 100644 index 0000000000..e8a93db2c1 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/Align.swift @@ -0,0 +1,667 @@ +// The MIT License (MIT) +// +// Copyright (c) 2017-2020 Alexander Grebenyuk (github.com/kean). + +#if os(iOS) || os(tvOS) + import UIKit + + protocol LayoutItem { // `UIView`, `UILayoutGuide` + var superview: UIView? { get } + } + + extension UIView: LayoutItem {} + extension UILayoutGuide: LayoutItem { + var superview: UIView? { owningView } + } +#elseif os(macOS) + import AppKit + + protocol LayoutItem { // `NSView`, `NSLayoutGuide` + var superview: NSView? { get } + } + + extension NSView: LayoutItem {} + extension NSLayoutGuide: LayoutItem { + var superview: NSView? { owningView } + } +#endif + +extension LayoutItem { // Align methods are available via `LayoutAnchors` + @nonobjc var anchors: LayoutAnchors { LayoutAnchors(base: self) } +} + +// MARK: - LayoutAnchors + +struct LayoutAnchors { + let base: Base +} + +extension LayoutAnchors where Base: LayoutItem { + + // MARK: Anchors + + var top: Anchor { Anchor(base, .top) } + var bottom: Anchor { Anchor(base, .bottom) } + var left: Anchor { Anchor(base, .left) } + var right: Anchor { Anchor(base, .right) } + var leading: Anchor { Anchor(base, .leading) } + var trailing: Anchor { Anchor(base, .trailing) } + + var centerX: Anchor { Anchor(base, .centerX) } + var centerY: Anchor { Anchor(base, .centerY) } + + var firstBaseline: Anchor { + Anchor(base, .firstBaseline) + } + var lastBaseline: Anchor { Anchor(base, .lastBaseline) } + + var width: Anchor { Anchor(base, .width) } + var height: Anchor { Anchor(base, .height) } + + // MARK: Anchor Collections + + var edges: AnchorCollectionEdges { AnchorCollectionEdges(item: base) } + var center: AnchorCollectionCenter { AnchorCollectionCenter(x: centerX, y: centerY) } + var size: AnchorCollectionSize { AnchorCollectionSize(width: width, height: height) } +} + +// MARK: - Anchors + +// phantom types +enum AnchorAxis { + class Horizontal {} + class Vertical {} +} + +enum AnchorType { + class Dimension {} + class Alignment {} + class Center: Alignment {} + class Edge: Alignment {} + class Baseline: Alignment {} +} + +/// An anchor represents one of the view's layout attributes (e.g. `left`, +/// `centerX`, `width`, etc). +/// +/// Instead of creating `NSLayoutConstraint` objects directly, start with a `UIView`, +/// `NSView`, or `UILayoutGuide` object you wish to constrain, and select one of +/// that object’s anchor properties. These properties correspond to the main +/// `NSLayoutConstraint.Attribute` values used in Auto Layout, and provide an +/// appropriate `Anchor` type for creating constraints to that attribute. For +/// example, `view.anchors.top` is represted by `Anchor`. +/// Use the anchor’s methods to construct your constraint. +/// +/// - note: `UIView` does not provide anchor properties for the layout margin attributes. +/// Instead, the `layoutMarginsGuide` property provides a `UILayoutGuide` object that +/// represents these margins. Use the guide’s anchor properties to create your constraints. +/// +/// When you create constraints using `Anchor` APIs, the constraints are activated +/// automatically and the target view has `translatesAutoresizingMaskIntoConstraints` +/// set to `false`. If you want to activate all the constraints at the same or +/// create them without activation, use `Constraints` type. +struct Anchor { // type and axis are phantom types + let item: LayoutItem + let attribute: NSLayoutConstraint.Attribute + let offset: CGFloat + let multiplier: CGFloat + + init( + _ item: LayoutItem, _ attribute: NSLayoutConstraint.Attribute, offset: CGFloat = 0, + multiplier: CGFloat = 1 + ) { + self.item = item + self.attribute = attribute + self.offset = offset + self.multiplier = multiplier + } + + /// Returns a new anchor offset by a given amount. + /// + /// - note: Consider using a convenience operator instead: `view.anchors.top + 10`. + func offsetting(by offset: CGFloat) -> Anchor { + Anchor(item, attribute, offset: self.offset + offset, multiplier: self.multiplier) + } + + /// Returns a new anchor with a given multiplier. + /// + /// - note: Consider using a convenience operator instead: `view.anchors.height * 2`. + func multiplied(by multiplier: CGFloat) -> Anchor { + Anchor( + item, attribute, offset: self.offset * multiplier, multiplier: self.multiplier * multiplier) + } +} + +func + (anchor: Anchor, offset: CGFloat) -> Anchor { + anchor.offsetting(by: offset) +} + +func - (anchor: Anchor, offset: CGFloat) -> Anchor { + anchor.offsetting(by: -offset) +} + +func * (anchor: Anchor, multiplier: CGFloat) -> Anchor { + anchor.multiplied(by: multiplier) +} + +// MARK: - Anchors (AnchorType.Alignment) + +extension Anchor where Type: AnchorType.Alignment { + /// Adds a constraint that defines the anchors' attributes as equal to each other. + @discardableResult func equal( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .equal) + } + + @discardableResult func greaterThanOrEqual( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .greaterThanOrEqual) + } + + @discardableResult func lessThanOrEqual( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .lessThanOrEqual) + } +} + +// MARK: - Anchors (AnchorType.Dimension) + +extension Anchor where Type: AnchorType.Dimension { + /// Adds a constraint that defines the anchors' attributes as equal to each other. + @discardableResult func equal( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .equal) + } + + @discardableResult func greaterThanOrEqual( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .greaterThanOrEqual) + } + + @discardableResult func lessThanOrEqual( + _ anchor: Anchor, constant: CGFloat = 0 + ) -> NSLayoutConstraint { + Constraints.add(self, anchor, constant: constant, relation: .lessThanOrEqual) + } +} + +// MARK: - Anchors (AnchorType.Dimension) + +extension Anchor where Type: AnchorType.Dimension { + @discardableResult func equal(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.add(item: item, attribute: attribute, relatedBy: .equal, constant: constant) + } + + @discardableResult func greaterThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.add( + item: item, attribute: attribute, relatedBy: .greaterThanOrEqual, constant: constant) + } + + @discardableResult func lessThanOrEqual(_ constant: CGFloat) -> NSLayoutConstraint { + Constraints.add( + item: item, attribute: attribute, relatedBy: .lessThanOrEqual, constant: constant) + } + + /// Clamps the dimension of a view to the given limiting range. + @discardableResult func clamp(to limits: ClosedRange) -> [NSLayoutConstraint] { + [greaterThanOrEqual(limits.lowerBound), lessThanOrEqual(limits.upperBound)] + } +} + +// MARK: - Anchors (AnchorType.Edge) + +extension Anchor where Type: AnchorType.Edge { + /// Pins the edge to the respected edges of the given container. + @discardableResult func pin(to container: LayoutItem? = nil, inset: CGFloat = 0) + -> NSLayoutConstraint + { + let isInverted = [.trailing, .right, .bottom].contains(attribute) + return Constraints.add( + self, toItem: container ?? item.superview!, attribute: attribute, + constant: (isInverted ? -inset : inset)) + } + + /// Adds spacing between the current anchors. + @discardableResult func spacing( + _ spacing: CGFloat, to anchor: Anchor, + relation: NSLayoutConstraint.Relation = .equal + ) -> NSLayoutConstraint { + let isInverted = + (attribute == .bottom && anchor.attribute == .top) + || (attribute == .right && anchor.attribute == .left) + || (attribute == .trailing && anchor.attribute == .leading) + return Constraints.add( + self, anchor, constant: isInverted ? -spacing : spacing, + relation: isInverted ? relation.inverted : relation) + } +} + +// MARK: - Anchors (AnchorType.Center) + +extension Anchor where Type: AnchorType.Center { + /// Aligns the axis with a superview axis. + @discardableResult func align(offset: CGFloat = 0) -> NSLayoutConstraint { + Constraints.add(self, toItem: item.superview!, attribute: attribute, constant: offset) + } +} + +// MARK: - AnchorCollectionEdges + +struct Alignment { + enum Horizontal { + case fill, center, leading, trailing + } + enum Vertical { + case fill, center, top, bottom + } + + let horizontal: Horizontal + let vertical: Vertical + + init(horizontal: Horizontal, vertical: Vertical) { + (self.horizontal, self.vertical) = (horizontal, vertical) + } + + static let fill = Alignment(horizontal: .fill, vertical: .fill) + static let center = Alignment(horizontal: .center, vertical: .center) + static let topLeading = Alignment(horizontal: .leading, vertical: .top) + static let leading = Alignment(horizontal: .leading, vertical: .fill) + static let bottomLeading = Alignment(horizontal: .leading, vertical: .bottom) + static let bottom = Alignment(horizontal: .fill, vertical: .bottom) + static let bottomTrailing = Alignment(horizontal: .trailing, vertical: .bottom) + static let trailing = Alignment(horizontal: .trailing, vertical: .fill) + static let topTrailing = Alignment(horizontal: .trailing, vertical: .top) + static let top = Alignment(horizontal: .fill, vertical: .top) +} + +struct AnchorCollectionEdges { + let item: LayoutItem + var isAbsolute = false + + // By default, edges use locale-specific `.leading` and `.trailing` + func absolute() -> AnchorCollectionEdges { + AnchorCollectionEdges(item: item, isAbsolute: true) + } + + #if os(iOS) || os(tvOS) + typealias Axis = NSLayoutConstraint.Axis + #else + typealias Axis = NSLayoutConstraint.Orientation + #endif + + // MARK: Core API + + @discardableResult func equal(_ item2: LayoutItem, insets: EdgeInsets = .zero) + -> [NSLayoutConstraint] + { + pin(to: item2, insets: insets) + } + + @discardableResult func lessThanOrEqual(_ item2: LayoutItem, insets: EdgeInsets = .zero) + -> [NSLayoutConstraint] + { + pin(to: item2, insets: insets, axis: nil, alignment: .center, isCenteringEnabled: false) + } + + @discardableResult func equal(_ item2: LayoutItem, insets: CGFloat) -> [NSLayoutConstraint] { + pin(to: item2, insets: EdgeInsets(top: insets, left: insets, bottom: insets, right: insets)) + } + + @discardableResult func lessThanOrEqual(_ item2: LayoutItem, insets: CGFloat) + -> [NSLayoutConstraint] + { + pin( + to: item2, insets: EdgeInsets(top: insets, left: insets, bottom: insets, right: insets), + axis: nil, alignment: .center, isCenteringEnabled: false) + } + + // MARK: Semantic API + + /// Pins the edges to the edges of the given item. By default, pins the edges + /// to the superview. + /// + /// - parameter target: The target view, by default, uses the superview. + /// - parameter insets: Insets the reciever's edges by the given insets. + /// - parameter axis: If provided, creates constraints only along the given + /// axis. For example, if you pass axis `.horizontal`, only the `.leading`, + /// `.trailing` (and `.centerX` if needed) attributes are used. `nil` by default + /// - parameter alignment: `.fill` by default, see `Alignment` for a list of + /// the available options. + @discardableResult func pin( + to item2: LayoutItem? = nil, insets: CGFloat, axis: Axis? = nil, alignment: Alignment = .fill + ) -> [NSLayoutConstraint] { + pin( + to: item2, insets: EdgeInsets(top: insets, left: insets, bottom: insets, right: insets), + axis: axis, alignment: alignment) + } + + /// Pins the edges to the edges of the given item. By default, pins the edges + /// to the superview. + /// + /// - parameter target: The target view, by default, uses the superview. + /// - parameter insets: Insets the reciever's edges by the given insets. + /// - parameter axis: If provided, creates constraints only along the given + /// axis. For example, if you pass axis `.horizontal`, only the `.leading`, + /// `.trailing` (and `.centerX` if needed) attributes are used. `nil` by default + /// - parameter alignment: `.fill` by default, see `Alignment` for a list of + /// the available options. + @discardableResult func pin( + to item2: LayoutItem? = nil, insets: EdgeInsets = .zero, axis: Axis? = nil, + alignment: Alignment = .fill + ) -> [NSLayoutConstraint] { + pin(to: item2, insets: insets, axis: axis, alignment: alignment, isCenteringEnabled: true) + } + + private func pin( + to item2: LayoutItem?, insets: EdgeInsets, axis: Axis?, alignment: Alignment, + isCenteringEnabled: Bool + ) -> [NSLayoutConstraint] { + let item2 = item2 ?? item.superview! + let left: NSLayoutConstraint.Attribute = isAbsolute ? .left : .leading + let right: NSLayoutConstraint.Attribute = isAbsolute ? .right : .trailing + var constraints = [NSLayoutConstraint]() + + func constrain( + attribute: NSLayoutConstraint.Attribute, relation: NSLayoutConstraint.Relation, + constant: CGFloat + ) { + constraints.append( + Constraints.add( + item: item, attribute: attribute, relatedBy: relation, toItem: item2, + attribute: attribute, multiplier: 1, constant: constant)) + } + + if axis == nil || axis == .horizontal { + constrain( + attribute: left, + relation: alignment.horizontal == .fill || alignment.horizontal == .leading + ? .equal : .greaterThanOrEqual, constant: insets.left) + constrain( + attribute: right, + relation: alignment.horizontal == .fill || alignment.horizontal == .trailing + ? .equal : .lessThanOrEqual, constant: -insets.right) + if alignment.horizontal == .center && isCenteringEnabled { + constrain(attribute: .centerX, relation: .equal, constant: 0) + } + } + if axis == nil || axis == .vertical { + constrain( + attribute: .top, + relation: alignment.vertical == .fill || alignment.vertical == .top + ? .equal : .greaterThanOrEqual, constant: insets.top) + constrain( + attribute: .bottom, + relation: alignment.vertical == .fill || alignment.vertical == .bottom + ? .equal : .lessThanOrEqual, constant: -insets.bottom) + if alignment.vertical == .center && isCenteringEnabled { + constrain(attribute: .centerY, relation: .equal, constant: 0) + } + } + return constraints + } +} + +// MARK: - AnchorCollectionCenter + +struct AnchorCollectionCenter { + let x: Anchor + let y: Anchor + + // MARK: Core API + + @discardableResult func equal(_ item2: Item, offset: CGPoint = .zero) + -> [NSLayoutConstraint] + { + [ + x.equal(item2.anchors.centerX, constant: offset.x), + y.equal(item2.anchors.centerY, constant: offset.y), + ] + } + + @discardableResult func greaterThanOrEqual( + _ item2: Item, offset: CGPoint = .zero + ) -> [NSLayoutConstraint] { + [ + x.greaterThanOrEqual(item2.anchors.centerX, constant: offset.x), + y.greaterThanOrEqual(item2.anchors.centerY, constant: offset.y), + ] + } + + @discardableResult func lessThanOrEqual(_ item2: Item, offset: CGPoint = .zero) + -> [NSLayoutConstraint] + { + [ + x.lessThanOrEqual(item2.anchors.centerX, constant: offset.x), + y.lessThanOrEqual(item2.anchors.centerY, constant: offset.y), + ] + } + + // MARK: Semantic API + + /// Centers the view in the superview. + @discardableResult func align() -> [NSLayoutConstraint] { + [x.align(), y.align()] + } + + /// Makes the axis equal to the other collection of axis. + @discardableResult func align(with item: Item) -> [NSLayoutConstraint] { + [x.equal(item.anchors.centerX), y.equal(item.anchors.centerY)] + } +} + +// MARK: - AnchorCollectionSize + +struct AnchorCollectionSize { + let width: Anchor + let height: Anchor + + // MARK: Core API + + /// Set the size of item. + @discardableResult func equal(_ size: CGSize) -> [NSLayoutConstraint] { + [width.equal(size.width), height.equal(size.height)] + } + + /// Set the size of item. + @discardableResult func greaterThanOrEqul(_ size: CGSize) -> [NSLayoutConstraint] { + [width.greaterThanOrEqual(size.width), height.greaterThanOrEqual(size.height)] + } + + /// Set the size of item. + @discardableResult func lessThanOrEqual(_ size: CGSize) -> [NSLayoutConstraint] { + [width.lessThanOrEqual(size.width), height.lessThanOrEqual(size.height)] + } + + /// Makes the size of the item equal to the size of the other item. + @discardableResult func equal( + _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 + ) -> [NSLayoutConstraint] { + [ + width.equal(item.anchors.width * multiplier - insets.width), + height.equal(item.anchors.height * multiplier - insets.height), + ] + } + + @discardableResult func greaterThanOrEqual( + _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 + ) -> [NSLayoutConstraint] { + [ + width.greaterThanOrEqual(item.anchors.width * multiplier - insets.width), + height.greaterThanOrEqual(item.anchors.height * multiplier - insets.height), + ] + } + + @discardableResult func lessThanOrEqual( + _ item: Item, insets: CGSize = .zero, multiplier: CGFloat = 1 + ) -> [NSLayoutConstraint] { + [ + width.lessThanOrEqual(item.anchors.width * multiplier - insets.width), + height.lessThanOrEqual(item.anchors.height * multiplier - insets.height), + ] + } +} + +// MARK: - Constraints + +final class Constraints: Collection { + typealias Element = NSLayoutConstraint + typealias Index = Int + + subscript(position: Int) -> NSLayoutConstraint { + get { constraints[position] } + } + var startIndex: Int { constraints.startIndex } + var endIndex: Int { constraints.endIndex } + func index(after i: Int) -> Int { i + 1 } + + /// Returns all of the created constraints. + private(set) var constraints = [NSLayoutConstraint]() + + /// All of the constraints created in the given closure are automatically + /// activated at the same time. This is more efficient then installing them + /// one-be-one. More importantly, it allows to make changes to the constraints + /// before they are installed (e.g. change `priority`). + /// + /// - parameter activate: Set to `false` to disable automatic activation of + /// constraints. + @discardableResult init(activate: Bool = true, _ closure: () -> Void) { + Constraints.stack.append(self) + closure() // create constraints + Constraints.stack.removeLast() + if activate { NSLayoutConstraint.activate(constraints) } + } + + // MARK: Activate + + /// Activates each constraint in the reciever. + func activate() { + NSLayoutConstraint.activate(constraints) + } + + /// Deactivates each constraint in the reciever. + func deactivate() { + NSLayoutConstraint.deactivate(constraints) + } + + // MARK: Adding Constraints + + /// Creates and automatically installs a constraint. + static func add( + item item1: Any, attribute attr1: NSLayoutConstraint.Attribute, + relatedBy relation: NSLayoutConstraint.Relation = .equal, toItem item2: Any? = nil, + attribute attr2: NSLayoutConstraint.Attribute? = nil, multiplier: CGFloat = 1, + constant: CGFloat = 0 + ) -> NSLayoutConstraint { + precondition(Thread.isMainThread, "Align APIs can only be used from the main thread") + #if os(iOS) || os(tvOS) + (item1 as? UIView)?.translatesAutoresizingMaskIntoConstraints = false + #elseif os(macOS) + (item1 as? NSView)?.translatesAutoresizingMaskIntoConstraints = false + #endif + let constraint = NSLayoutConstraint( + item: item1, attribute: attr1, relatedBy: relation, toItem: item2, + attribute: attr2 ?? .notAnAttribute, multiplier: multiplier, constant: constant) + install(constraint) + return constraint + } + + /// Creates and automatically installs a constraint between two anchors. + static func add( + _ lhs: Anchor, _ rhs: Anchor, constant: CGFloat = 0, multiplier: CGFloat = 1, + relation: NSLayoutConstraint.Relation = .equal + ) -> NSLayoutConstraint { + add( + item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: rhs.item, + attribute: rhs.attribute, multiplier: (multiplier / lhs.multiplier) * rhs.multiplier, + constant: constant - lhs.offset + rhs.offset) + } + + /// Creates and automatically installs a constraint between an anchor and + /// a given item. + static func add( + _ lhs: Anchor, toItem item2: Any?, attribute attr2: NSLayoutConstraint.Attribute?, + constant: CGFloat = 0, multiplier: CGFloat = 1, relation: NSLayoutConstraint.Relation = .equal + ) -> NSLayoutConstraint { + add( + item: lhs.item, attribute: lhs.attribute, relatedBy: relation, toItem: item2, + attribute: attr2, multiplier: multiplier / lhs.multiplier, constant: constant - lhs.offset) + } + + private static var stack = [Constraints]() // this is what enabled constraint auto-installing + + private static func install(_ constraint: NSLayoutConstraint) { + if let group = stack.last { + group.constraints.append(constraint) + } else { + constraint.isActive = true + } + } +} + +extension Constraints { + @discardableResult convenience init( + for a: A, _ closure: (LayoutAnchors) -> Void + ) { + self.init { closure(a.anchors) } + } + + @discardableResult convenience init( + for a: A, _ b: B, _ closure: (LayoutAnchors, LayoutAnchors) -> Void + ) { + self.init { closure(a.anchors, b.anchors) } + } + + @discardableResult convenience init( + for a: A, _ b: B, _ c: C, + _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void + ) { + self.init { closure(a.anchors, b.anchors, c.anchors) } + } + + @discardableResult convenience init( + for a: A, _ b: B, _ c: C, _ d: D, + _ closure: (LayoutAnchors, LayoutAnchors, LayoutAnchors, LayoutAnchors) -> Void + ) { + self.init { closure(a.anchors, b.anchors, c.anchors, d.anchors) } + } +} + +// MARK: - Misc + +#if os(iOS) || os(tvOS) + typealias EdgeInsets = UIEdgeInsets +#elseif os(macOS) + typealias EdgeInsets = NSEdgeInsets + + extension NSEdgeInsets { + static let zero = NSEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + } +#endif + +extension NSLayoutConstraint.Relation { + var inverted: NSLayoutConstraint.Relation { + switch self { + case .greaterThanOrEqual: return .lessThanOrEqual + case .lessThanOrEqual: return .greaterThanOrEqual + case .equal: return self + @unknown default: return self + } + } +} + +extension EdgeInsets { + func inset(for attribute: NSLayoutConstraint.Attribute, edge: Bool = false) -> CGFloat { + switch attribute { + case .top: return top + case .bottom: return edge ? -bottom : bottom + case .left, .leading: return left + case .right, .trailing: return edge ? -right : right + default: return 0 + } + } +} diff --git a/Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift b/Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift new file mode 100644 index 0000000000..4e61d01b32 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/GIFViewProvider/GIFViewProvider.swift @@ -0,0 +1,56 @@ +import UIKit + +/// The gif view provider used for all BrazeUI components. +/// +/// By default, Braze displays animated gifs as static images. +/// See ``GIFViewProvider-swift.struct`` for details about how to add support for animated gifs. +public var gifViewProvider: GIFViewProvider = .default + +/// A type providing methods to create and update views supporting animated gif images. +/// +/// Braze does not provide animated gif support out of the box. Support can be added by wrapping a +/// third party or your own view in an instance of `GIFViewProvider`. +/// +/// Sample implementations for popular third party libraries are provided in +/// . +/// +/// Adding any of those libraries to your project allows you to enable animated gif support in +/// Braze's UI components. +/// For instance, a project including SDWebImage and using the compatible sample code can do: +/// ```swift +/// BrazeUI.gifViewProvider = .sdWebImage +/// ``` +public struct GIFViewProvider { + + /// Creates a view able to display static and animated gif images. + /// - Parameters: + /// - url: The local file url for the image. + public var view: (_ url: URL?) -> UIView + + /// Updates the passed view with a new image at `url`. + /// - Parameters: + /// - view: The view to update. + /// - url: The local file url for the image. + public var updateView: (_ view: UIView, _ url: URL?) -> Void + + /// Creates a gif view provider. + /// - Parameters: + /// - view: See ``view``. + /// - updateView: See ``updateView``. + public init( + view: @escaping (URL?) -> UIView, + updateView: @escaping (UIView, URL?) -> Void + ) { + self.view = view + self.updateView = updateView + } + + /// The default provider. + /// + /// This provider does not support animated images and display them as static images. + public static let `default` = Self( + view: { UIImageView(image: ($0?.path).flatMap(UIImage.init(contentsOfFile:))) }, + updateView: { ($0 as? UIImageView)?.image = ($1?.path).flatMap(UIImage.init(contentsOfFile:)) } + ) + +} diff --git a/Sources/BrazeUI/Dependencies/ImageSize.swift b/Sources/BrazeUI/Dependencies/ImageSize.swift new file mode 100644 index 0000000000..b0e4d6b317 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/ImageSize.swift @@ -0,0 +1,27 @@ +import Foundation +import ImageIO + +/// Returns the size of the image at the passed file url without fully loading the image in memory. +/// - Parameter url: The image file url. +/// - Returns: The size of the image if valid, `nil` otherwise. +func imageSize(url: URL) -> CGSize? { + guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [AnyHashable: Any], + let width = properties[kCGImagePropertyPixelWidth] as? Double, + let height = properties[kCGImagePropertyPixelHeight] as? Double + else { + return nil + } + + guard + let orientationRawValue = properties[kCGImagePropertyOrientation] as? UInt32, + let orientation = CGImagePropertyOrientation(rawValue: orientationRawValue) + else { + return CGSize(width: width, height: height) + } + + let swapDimensions = [.left, .leftMirrored, .right, .rightMirrored].contains(orientation) + return swapDimensions + ? CGSize(width: height, height: width) + : CGSize(width: width, height: height) +} diff --git a/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift b/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift new file mode 100644 index 0000000000..1defa95520 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/KeyboardFrameNotifier.swift @@ -0,0 +1,93 @@ +import UIKit + +/// KeyboardFrameNotifier listens to any changes to the keyboard frame reported by the system and +/// pass them down to any of its own subscribers. +/// +/// An instance of this class should be created as early as possible in order to receive accurate +/// frame updates. +/// `UIKit` does not offer any api to retrieve the current state of the software keyboard, only +/// updates. The keyboard frame is `.zero` until the keyboard frame notifier receives an update from +/// `UIKit`. +open class KeyboardFrameNotifier { + + /// The shared keyboard frame notifier. + public static let shared = KeyboardFrameNotifier() + + /// The current keyboard frame. + /// + /// The initial value is `.zero` until an update is received from `UIKit`. + var frame: CGRect = .zero + + /// The windows for which the keyboard can be displayed. + var windows: [UIWindow] { + if #available(iOS 13.0, tvOS 13.0, *) { + return + UIApplication.shared + .connectedScenes + .lazy + .compactMap { $0 as? UIWindowScene } + .first { $0.activationState == .foregroundActive }? + .windows ?? [] + } else { + return UIApplication.shared + .windows + } + } + + /// The subscriptions dictionary. + var subscriptions: [AnyHashable: (CGRect) -> Void] = [:] + + /// Creates a keyboard frame notifier. + /// - Parameter center: The notification center used to receive keyboard frame updates. + init(center: NotificationCenter = .default) { + center.addObserver( + self, + selector: #selector(willChangeFrame(_:)), + name: UIResponder.keyboardWillChangeFrameNotification, + object: nil + ) + center.addObserver( + self, + selector: #selector(didHide(_:)), + name: UIResponder.keyboardDidHideNotification, + object: nil + ) + } + + /// Subscribes for keyboard frame updates. + /// - Parameters: + /// - identifier: The value identifying the subscriptions. + /// - onFrame: The closure executed for each frame update. + func subscribe( + identifier: AnyHashable, + onFrame: @escaping (CGRect) -> Void + ) { + onFrame(frame) + subscriptions[identifier] = onFrame + } + + /// Unsubscribe from keyboard frame updates. + /// - Parameter identifier: The identifier used when subscribing. + func unsubscribe(identifier: AnyHashable) { + subscriptions[identifier] = nil + } + + @objc + func willChangeFrame(_ notification: Notification) { + guard let nsFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, + windows.count > 0 + else { + return + } + guard nsFrame.cgRectValue != .zero else { return } + frame = nsFrame.cgRectValue + subscriptions.values.forEach { $0(frame) } + } + + @objc + func didHide(_ notification: Notification) { + frame = .zero + subscriptions.values.forEach { $0(frame) } + } + +} diff --git a/Sources/BrazeUI/Dependencies/Localize.swift b/Sources/BrazeUI/Dependencies/Localize.swift new file mode 100644 index 0000000000..ca679ea9df --- /dev/null +++ b/Sources/BrazeUI/Dependencies/Localize.swift @@ -0,0 +1,17 @@ +import Foundation + +enum LocalizationSet: String { + case inAppMessage = "InAppMessageLocalizable" + case contentCard + case newsFeed +} + +func localize(_ key: String, for localizationSet: LocalizationSet) -> String { + // Look for a possible override in main bundle + let override = Bundle.main.localizedString(forKey: key, value: nil, table: nil) + if override != key { + return override + } + + return Bundle.module.localizedString(forKey: key, value: nil, table: localizationSet.rawValue) +} diff --git a/Sources/BrazeUI/Dependencies/Shadow.swift b/Sources/BrazeUI/Dependencies/Shadow.swift new file mode 100644 index 0000000000..063b021957 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/Shadow.swift @@ -0,0 +1,67 @@ +import UIKit + +/// Type representing a view's shadow. +public struct Shadow: Equatable { + public var color: UIColor + public var offset: CGSize + public var radius: CGFloat + public var opacity: Float + + /// Default shadow used by Braze's in-app messages. + public static let `default` = Self( + color: .black.withAlphaComponent(0.3), + offset: .zero, + radius: 4.0, + opacity: 1 + ) +} + +extension UIView { + + /// Syntactic sugar around the layer's shadow properties. + /// + /// When using this property to set the view's shadow, you can use ``updateShadow()`` to resize + /// the shadow to the view's current size. + var shadow: Shadow? { + get { + guard let color = layer.shadowColor.flatMap(UIColor.init) else { + return nil + } + return .init( + color: color, + offset: layer.shadowOffset, + radius: layer.shadowRadius, + opacity: layer.opacity + ) + } + set { + guard let value = newValue else { + layer.shadowColor = nil + layer.shadowOffset = .zero + layer.shadowRadius = 0 + layer.shadowOpacity = 0 + layer.shadowPath = nil + return + } + layer.shadowColor = value.color.cgColor + layer.shadowOffset = value.offset + layer.shadowRadius = value.radius + layer.shadowOpacity = value.opacity + updateShadow() + } + } + + /// Resizes the layer's shadow path to the view's current size. + func updateShadow() { + guard shadow != nil else { + return + } + let path = UIBezierPath( + roundedRect: bounds, + cornerRadius: layer.cornerRadius + ).cgPath + + layer.shadowPath = path + } + +} diff --git a/Sources/BrazeUI/Dependencies/UIKitExt.swift b/Sources/BrazeUI/Dependencies/UIKitExt.swift new file mode 100644 index 0000000000..3a4529f738 --- /dev/null +++ b/Sources/BrazeUI/Dependencies/UIKitExt.swift @@ -0,0 +1,115 @@ +import BrazeKit +import UIKit + +extension UIFont { + + /// Shorthand for `Braze.UIUtils.preferredFont(forTextStyle:weight:)`. + static func preferredFont( + textStyle: UIFont.TextStyle, + weight: UIFont.Weight + ) -> UIFont { + Braze.UIUtils.preferredFont(textStyle: textStyle, weight: weight) + } + +} + +extension UIButton { + + /// Adds a closure executed when pressed. + func addAction(_ action: @escaping () -> Void) { + @objc final class Receiver: NSObject { + let action: () -> Void + init(_ action: @escaping () -> Void) { self.action = action } + @objc func receive() { action() } + } + let receiver = Receiver(action) + self.addTarget(receiver, action: #selector(Receiver.receive), for: .touchUpInside) + objc_setAssociatedObject(self, UUID().uuidString, receiver, .OBJC_ASSOCIATION_RETAIN) + } + +} + +extension UIView { + + /// Position the view in a resizable container view and returns it. The wrapped view can adopt its + /// intrinsic content size without being stretched. + /// - Parameters: + /// - centerX: Horizontally center the view in the container view (default: `true`). + /// - centerY: Vertically center the view in the container view (default: `true`). + /// - Returns: The view wrapped in a resizable container view. + func boundedByIntrinsicContentSize( + centerX: Bool = true, + centerY: Bool = true + ) -> UIView { + let wrapper = UIView() + wrapper.addSubview(self) + if centerX { anchors.centerX.align() } + if centerY { anchors.centerY.align() } + anchors.edges.lessThanOrEqual(wrapper) + return wrapper + } + + /// The sequence of recursive subviews using a breadth-first search approach. + /// + /// Use it with the `lazy` modifier for efficient recursive subview search: + /// ```swift + /// view.bfsSubviews.lazy.first { $0 is UIButton } + /// ``` + var bfsSubviews: AnySequence { + AnySequence { () -> AnyIterator in + var subviews: [UIView] = self.subviews + return AnyIterator { + if subviews.isEmpty { return nil } + let view = subviews.removeFirst() + subviews.append(contentsOf: view.subviews) + return view + } + } + } + +} + +extension UIResponder { + + /// A sequence representing the instance's responder chain starting with the instance's next + /// responder. + var responders: AnySequence { + AnySequence { () -> AnyIterator in + var responder: UIResponder? = self + return AnyIterator { + responder = responder?.next + return responder + } + } + } + +} + +extension UIViewController { + + /// The "topmost" presented view controller. + var topmost: UIViewController? { + var controller = self + while let presented = controller.presentedViewController { + controller = presented + } + return controller + } + +} + +extension String { + + func attributed(_ setup: (NSMutableParagraphStyle) -> Void) -> NSAttributedString { + let attributedText = NSMutableAttributedString(string: self) + let style = NSMutableParagraphStyle() + setup(style) + attributedText.addAttribute( + .paragraphStyle, + value: style, + range: NSRange(location: 0, length: attributedText.length) + ) + return attributedText + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageExt.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageExt.swift new file mode 100644 index 0000000000..22fb771d42 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageExt.swift @@ -0,0 +1,148 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageThemeable { + + /// Retrieves the theme fitting the passed `traits`. + /// + /// When an in-app message campaign does not include a [Dark Mode] theme, the default light mode + /// theme is returned. + /// + /// [Dark Mode]: https://apple.co/2WBiaQ7 + public func theme(for traits: UITraitCollection) -> Braze.InAppMessage.Theme { + guard #available(iOS 12.0, *) else { + return themes.light + } + + switch traits.userInterfaceStyle { + case .light, .unspecified: + return themes.light + case .dark: + return themes.dark ?? themes.light + @unknown default: + return themes.light + } + } + +} + +extension Braze.InAppMessage.Button { + + /// Retrieves the button theme fitting the passed `traits`. + /// + /// When an in-app message campaign does not include a [Dark Mode] theme, the default light mode + /// theme is returned. + /// + /// [Dark Mode]: https://apple.co/2WBiaQ7 + public func theme(for traits: UITraitCollection) -> Braze.InAppMessage.ButtonTheme { + guard #available(iOS 12.0, *) else { + return themes.light + } + + switch traits.userInterfaceStyle { + case .light, .unspecified: + return themes.light + case .dark: + return themes.dark ?? themes.light + @unknown default: + return themes.light + } + } +} + +extension Braze.InAppMessage.Color { + + /// The `UIColor` representation of the in-app message color. + public var uiColor: UIColor { + UIColor(red: r, green: g, blue: b, alpha: a) + } + + /// A 1x1 pt image of the color. + public var image: UIImage { + let rect = CGRect(x: 0, y: 0, width: 1, height: 1) + return UIGraphicsImageRenderer(size: rect.size).image { + self.uiColor.set() + $0.fill(rect) + } + } + + /// Returns an instance of the color with its brightness adjusted by the amount. + /// - Parameter amount: A value between `-1` and `1` representing the brightness decrement or + /// increment to apply to the color. + /// - Returns: The color with its brightness adjusted. + func adjustingBrightness(by amount: CGFloat) -> Self { + Self( + red: max(0, min(r + amount, 1)), + green: max(0, min(g + amount, 1)), + blue: max(0, min(b + amount, 1)), + alpha: a + ) + } + +} + +extension Braze.InAppMessage.TextAlignment { + + /// Returns the `NSTextAlignment` enum case matching the in-app message text aligment. + /// + /// This function is _RTL-aware_ and uses the passed `traits` to choose the appropriate text + /// alignment. + /// - Parameter traits: The current traits. + /// - Returns: The matching `NSTextAlignment` case. + func nsTextAlignment(forTraits traits: UITraitCollection) -> NSTextAlignment { + switch (self, traits.layoutDirection) { + case (.leading, _): + return .natural + case (.center, _): + return .center + case (.trailing, .rightToLeft): + return .left + case (.trailing, _): + return .right + @unknown default: + return .natural + } + } + +} + +extension Braze.InAppMessage.Orientation { + + /// Returns whether the in-app message orientation is supported by the passed `traits`. + /// - Parameter traits: The current traits. + /// - Returns: A boolean value indicating if the in-app message orientation is supported by + /// `traits` + func supported(by traits: UITraitCollection?) -> Bool { + switch (self, traits?.horizontalSizeClass, traits?.verticalSizeClass) { + case (.any, _, _), + (_, .none, .none): + return true + case (.portrait, .compact, _), + (.portrait, .regular, .regular): + return true + case (.landscape, .regular, _): + return true + default: + return false + } + } + + /// Returns the interface orientation mask for the passed `traits`. + /// - Parameter traits: The current traits. + /// - Returns: The interface orientation mask corresponding to the in-app message orientation. + func mask(for traits: UITraitCollection?) -> UIInterfaceOrientationMask { + switch self { + case .any where traits?.userInterfaceIdiom == .phone: + return .allButUpsideDown + case .any: + return .all + case .portrait: + return .portrait + case .landscape: + return .landscape + @unknown default: + return .all + } + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift new file mode 100644 index 0000000000..26a68af912 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageMocks.swift @@ -0,0 +1,445 @@ +import BrazeKit +import Foundation + +#if DEBUG + + // MARK: - Slideups + + extension Braze.InAppMessage.Slideup { + + public static let mock = Self( + data: .mock, + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockShortText = Self( + data: .mock, + message: "Short" + ) + + public static let mockChevron = Self( + data: .mock, + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockIcon = Self( + data: .mock, + graphic: .icon(""), + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockImage = Self( + data: .mock, + graphic: .image(.mockImage(width: 150, height: 150, textSize: 30)), + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake." + ) + + public static let mockLong = Self( + data: .mock, + graphic: .icon(""), + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon." + ) + + public static let mockThemed = Self( + data: .mock, + graphic: .icon(""), + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", + themes: [ + "light": .init( + backgroundColor: 0xFF2D_3436, + textColor: 0xFFE0_56FD, + iconColor: 0xFF7F_FF00, + iconBackgroundColor: 0xFF4B_6584, + closeButtonColor: 0xFFE0_56FD + ) + ] + ) + + public static let mockTop = Self( + data: .mock, + graphic: .icon(""), + message: "Cupcake ipsum dolor sit amet. Topping dessert muffin fruitcake.", + slideFrom: .top + ) + + } + + // MARK: - Modals + + extension Braze.InAppMessage.Modal { + + public static let mock = Self( + data: .mock, + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon." + ) + + public static let mockOneButton = Self( + data: .mock, + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockPrimary + ] + ) + + public static let mockTwoButtons = Self( + data: .mock, + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockIcon = Self( + data: .mock, + graphic: .icon(""), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockImage = Self( + data: .mock, + graphic: .image(.mockImage(width: 1450, height: 500)), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockImageWrongAspectRatio = Self( + data: .mock, + graphic: .image(.mockImage(width: 1450, height: 650)), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockLeadingAligned = Self( + data: .mock, + graphic: .icon(""), + header: "Hello world!", + headerTextAlignment: .leading, + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + messageTextAlignment: .leading, + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockLong = Self( + data: .mock, + graphic: .icon(""), + header: "Hello world!", + message: + """ + Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon. Pie oat cake candy canes powder chocolate cupcake powder tart cupcake. Croissant halvah jujubes cotton candy biscuit tiramisu jujubes shortbread cheesecake. Sweet cupcake cupcake tart cake ice cream bear claw dragée cookie. Cake fruitcake donut macaroon gummi bears powder. Chocolate cake pie chupa chups fruitcake apple pie. Ice cream bonbon oat cake jelly-o biscuit sweet muffin. Caramels sweet danish chocolate wafer wafer cheesecake liquorice oat cake. Dessert shortbread donut pudding sesame snaps. Cookie powder chocolate bar cookie carrot cake pudding cake gummies cupcake. + + Jujubes bonbon bonbon lemon drops marzipan halvah carrot cake pastry. Donut chocolate bar chocolate cake halvah cake lollipop icing. Cake chupa chups carrot cake danish fruitcake. Chupa chups cake carrot cake dragée pastry. Dessert carrot cake macaroon chupa chups dragée carrot cake. Pudding sesame snaps toffee dragée carrot cake chupa chups sweet gummies. Soufflé croissant brownie dessert chupa chups tart brownie sugar plum. Tootsie roll danish dessert cake jelly cake tart tootsie roll marshmallow. Cookie ice cream danish muffin apple pie fruitcake sweet tart marshmallow. Dessert apple pie cotton candy biscuit muffin. Tiramisu candy candy cookie pastry. Brownie brownie cake jelly-o macaroon muffin oat cake donut. + """, + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockThemed = Self( + data: .mock, + graphic: .icon(""), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ], + themes: [ + "light": .init( + backgroundColor: 0xFF2D_3436, + textColor: 0xFFE0_56fD, + iconColor: 0xFF7F_FF00, + iconBackgroundColor: 0xFF4B_6584, + headerTextColor: 0xFFE0_56fD, + closeButtonColor: 0xFFE0_56fD + ) + ] + ) + + } + + extension Braze.InAppMessage.ModalImage { + + public static let mock = Self( + data: .mock, + imageUri: .mockImage(width: 600, height: 600), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockLargeImage = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 2000), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockExtraLargeImage = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 4000), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockNoButtons = Self( + data: .mock, + imageUri: .mockImage(width: 600, height: 600), + buttons: [] + ) + + public static let mockOneButton = Self( + data: .mock, + imageUri: .mockImage(width: 600, height: 600), + buttons: [ + .mockPrimary + ] + ) + + public static let mockThemed = Self( + data: .mock, + imageUri: .mockImage(width: 600, height: 600), + buttons: [ + .mockSecondary, + .mockPrimaryThemed, + ], + themes: [ + "light": .init( + closeButtonColor: 0xFFC0_00FF, + frameColor: 0xFF40_A0A0 + ) + ] + ) + + } + + // MARK: - Full + + extension Braze.InAppMessage.Full { + + public static let mock = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon." + ) + + public static let mockOneButton = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockPrimary + ] + ) + + public static let mockTwoButtons = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockLeadingAligned = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + headerTextAlignment: .leading, + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + messageTextAlignment: .leading, + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockLong = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + message: + """ + Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon. Pie oat cake candy canes powder chocolate cupcake powder tart cupcake. Croissant halvah jujubes cotton candy biscuit tiramisu jujubes shortbread cheesecake. Sweet cupcake cupcake tart cake ice cream bear claw dragée cookie. Cake fruitcake donut macaroon gummi bears powder. Chocolate cake pie chupa chups fruitcake apple pie. Ice cream bonbon oat cake jelly-o biscuit sweet muffin. Caramels sweet danish chocolate wafer wafer cheesecake liquorice oat cake. Dessert shortbread donut pudding sesame snaps. Cookie powder chocolate bar cookie carrot cake pudding cake gummies cupcake. + + Jujubes bonbon bonbon lemon drops marzipan halvah carrot cake pastry. Donut chocolate bar chocolate cake halvah cake lollipop icing. Cake chupa chups carrot cake danish fruitcake. Chupa chups cake carrot cake dragée pastry. Dessert carrot cake macaroon chupa chups dragée carrot cake. Pudding sesame snaps toffee dragée carrot cake chupa chups sweet gummies. Soufflé croissant brownie dessert chupa chups tart brownie sugar plum. Tootsie roll danish dessert cake jelly cake tart tootsie roll marshmallow. Cookie ice cream danish muffin apple pie fruitcake sweet tart marshmallow. Dessert apple pie cotton candy biscuit muffin. Tiramisu candy candy cookie pastry. Brownie brownie cake jelly-o macaroon muffin oat cake donut. + """, + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockThemed = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 1000), + header: "Hello world!", + message: + "Cupcake ipsum dolor sit amet topping. Cookie candy chupa chups jujubes pastry soufflé. Danish cake cheesecake liquorice wafer marshmallow macaroon.", + buttons: [ + .mockSecondary, + .mockPrimary, + ], + themes: [ + "light": .init( + backgroundColor: 0xFF2D_3436, + textColor: 0xFFE0_56fD, + iconColor: 0xFF7F_FF00, + iconBackgroundColor: 0xFF4B_6584, + headerTextColor: 0xFFE0_56fD, + closeButtonColor: 0xFFE0_56fD + ) + ] + ) + } + + extension Braze.InAppMessage.FullImage { + + public static let mock = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 2000) + ) + + public static let mockOneButton = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 2000), + buttons: [.mockPrimary] + ) + + public static let mockTwoButtons = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 2000), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockMinRecommendedSize = Self( + data: .mock, + imageUri: .mockImage(width: 600, height: 1000), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockRecommendedSize: Self = .mockTwoButtons + + public static let mockNonRecommendedSize = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 4000), + buttons: [ + .mockSecondary, + .mockPrimary, + ] + ) + + public static let mockThemed = Self( + data: .mock, + imageUri: .mockImage(width: 1200, height: 2000), + themes: [ + "light": .init( + backgroundColor: 0xFF2D_3436, + textColor: 0xFFE0_56FD, + iconColor: 0xFF7F_FF00, + iconBackgroundColor: 0xFF4B_6584, + closeButtonColor: 0xFFE0_56FD + ) + ] + ) + + } + + // MARK: - Buttons + + extension Braze.InAppMessage.Button { + + static let mockPrimary = Self( + id: 1, + text: "Yes please!", + clickAction: .none, + themes: ["light": .primary] + ) + + static let mockSecondary = Self( + id: 0, + text: "No thanks", + clickAction: .none, + themes: ["light": .secondary] + ) + + static let mockPrimaryThemed = Self( + id: 0, + text: "Yes please, but in purple!", + clickAction: .none, + themes: [ + "light": .init( + textColor: 0xFFF0_80FF, + borderColor: 0xFFFF_0080, + backgroundColor: 0xFF40_0060 + ) + ] + ) + + } + + // MARK: - ClickAction + + extension Braze.InAppMessage.Data { + + public static let mock = Self( + clickAction: .mock + ) + + } + + extension Braze.InAppMessage.ClickAction { + + public static let mock: Self = .uri(URL(string: "https://example.com")!, useWebView: false) + + } + +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift new file mode 100644 index 0000000000..f11d503e14 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUI.swift @@ -0,0 +1,265 @@ +import BrazeKit +import Foundation +import UIKit + +/// The Braze provided in-app message presenter UI. +/// +/// Assign an instance of this class to `braze.inAppMessagePresenter` to enable the presentation of +/// in-app messages to the user. +/// +/// To add gif support to the in-app message UI components, set a valid ``gifViewProvider``. +@objc +open class BrazeInAppMessageUI: NSObject, BrazeInAppMessagePresenter { + + // MARK: - Properties + + /// The currently visible message view. + public var messageView: InAppMessageView? { + window?.messageViewController?.messageView + } + + /// The stack of in-app messages awaiting display. + /// + /// When the conditions to display a message are not met at trigger time, the message is pushed + /// onto the stack. + public internal(set) var stack: [Braze.InAppMessage] = [] + + /// The object that act as the delegate for the in-app message UI. + /// + /// The delegate is not retained and must conform to ``BrazeInAppMessageUIDelegate``. + public weak var delegate: BrazeInAppMessageUIDelegate? + + /// Headless support (default: `false`). + /// + /// When enabled, in-app messages will be presented even when the app has no UIApplication (e.g. + /// unit-test target without host app) + var headless: Bool = false + + /// The keyboard frame notifier. + var keyboard = KeyboardFrameNotifier() + + /// The timer for dismissing the message view. + var dismissTimer: Timer? + + /// The window displaying the current in-app message view. + var window: Window? + + // MARK: - Presentation / BrazeInAppMessagePresenter conformance + + public func present(message: Braze.InAppMessage) { + guard validateMainThread(for: message), + validateHeadless(for: message, allowHeadless: headless), + validateFontAwesome(for: message), + validateNoMessagePresented(for: message, pushInStack: true) + else { + return + } + + let displayChoice = + delegate?.inAppMessage(self, displayChoiceForMessage: message) + ?? .now + + switch displayChoice { + case .discard: + message.context?.discard() + case .later: + stack.append(message) + case .now: + presentNow(message: message) + } + + } + + /// Presents the next in-app message in the stack if any. + public func presentNext() { + // We use `last` instead of `popLast()` to avoid potentially modifying `stack` from a non + // main thread. The message is removed from the stack in `presentNow`. + guard let next = stack.last else { + return + } + presentNow(message: next) + } + + func presentNow(message: Braze.InAppMessage) { + guard validateMainThread(for: message), + validateHeadless(for: message, allowHeadless: headless), + validateFontAwesome(for: message), + validateNoMessagePresented(for: message, pushInStack: false), + validateOrientation(for: message), + validateContext(for: message) + else { + return + } + + // Remove the message from the stack if needed + stack.removeAll { $0 == message } + + // Prepare / user customizations + var context = PresentationContext( + message: message, + attributes: .defaults(for: message), + customView: nil, + preferredOrientation: Braze.UIUtils.interfaceOrientation, + statusBarHideBehavior: .auto, + windowLevel: .normal, + preferencesProxy: Braze.UIUtils.activeRootViewController?.topmost + ) + if #available(iOS 13.0, tvOS 13.0, *) { + context.windowScene = Braze.UIUtils.activeWindowScene + } + delegate?.inAppMessage(self, prepareWith: &context) + + // Creates view hierarchy + // - Message View + let optMessageView = + context.customView + ?? createMessageView( + for: context.message, + attributes: context.attributes, + gifViewProvider: gifViewProvider + ) + guard let messageView = optMessageView else { + message.context?.discard() + message.context?.logError(flattened: Error.noMessageView.logDescription) + return + } + + // - View controller + let viewController = ViewController( + ui: self, + context: context, + messageView: messageView, + keyboard: keyboard + ) + + // - Window + let window: Window + if #available(iOS 13.0, tvOS 13.0, *), let windowScene = context.windowScene { + window = Window(windowScene: windowScene) + } else { + window = Window(frame: UIScreen.main.bounds) + } + window.windowLevel = context.windowLevel + window.rootViewController = viewController + self.window = window + + // Dismiss Timer + if case .auto(let interval) = message.messageClose { + dismissTimer?.invalidate() + dismissTimer = .scheduledTimer( + withTimeInterval: interval, + repeats: false + ) { [weak self] _ in self?.dismiss() } + } + + // Display + if #available(iOS 15.0, *) { + // - Use animation block to animate the status bar hidden state + UIView.animate(withDuration: message.animateIn ? 0.25 : 0) { + // - Use `isHidden` instead of `makeKeyAndVisible` to defer the choice of hiding the keyboard + // to the message view. See `InAppMessageView/makeKey`. `isHidden` just displays the window + // without touching the first responder. + window.isHidden = false + } + } else { + // - No animation block before iOS 15.0, it has undesired side effects + window.isHidden = false + } + + } + + /// Dismisses the current in-app message view. + /// - Parameter completion: Executed once the in-app message view has been dismissed or directly + /// when no in-app message view is currently presented. + public func dismiss(completion: (() -> Void)? = nil) { + messageView?.dismiss(completion: completion) ?? completion?() + } + + // MARK: - Utils + + func logError(for message: Braze.InAppMessage, error: Error) { + message.context?.logError(flattened: error.logDescription) ?? print(error.logDescription) + } + + func validateMainThread(for message: Braze.InAppMessage) -> Bool { + guard Thread.isMainThread else { + DispatchQueue.main.sync { + logError(for: message, error: .noMainThread) + } + return false + } + return true + } + + func validateHeadless(for message: Braze.InAppMessage, allowHeadless: Bool = false) + -> Bool + { + if allowHeadless { + return true + } + + if Braze.UIUtils.activeRootViewController == nil { + logError(for: message, error: .noAppRootViewController) + return false + } + + return true + } + + func validateNoMessagePresented(for message: Braze.InAppMessage, pushInStack push: Bool) + -> Bool + { + guard messageView == nil else { + if push { + stack.append(message) + } + + logError(for: message, error: .otherMessagePresented(push: push)) + return false + } + return true + } + + // Always return true, font-awesome missing is not a breaking error + func validateFontAwesome(for message: Braze.InAppMessage) -> Bool { + guard IconView.registerFontAwesomeIfNeeded() else { + logError(for: message, error: .noFontAwesome) + return true + } + return true + } + + func validateOrientation(for message: Braze.InAppMessage) -> Bool { + let traits = Braze.UIUtils.activeRootViewController?.traitCollection + guard message.orientation.supported(by: traits) else { + stack.removeAll { $0 == message } + message.context?.discard() + logError(for: message, error: .noMatchingOrientation) + return false + } + return true + } + + func validateContext(for message: Braze.InAppMessage) -> Bool { + guard let context = message.context else { + // No context -> not a Braze in-app message. + return true + } + + guard context.discarded == false else { + stack.removeAll { $0 == message } + logError(for: message, error: .messageContextDiscarded) + return false + } + + guard context.valid else { + stack.removeAll { $0 == message } + context.discard() + logError(for: message, error: .messageContextInvalid) + return false + } + + return true + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift new file mode 100644 index 0000000000..cddb8f16e7 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIDelegate.swift @@ -0,0 +1,160 @@ +import BrazeKit +import UIKit + +/// Methods for reacting to the in-app message UI lifecycle. +public protocol BrazeInAppMessageUIDelegate: AnyObject { + + /// Defines whether the in-app message will be displayed now, displayed later, or discarded. + /// + /// The default implementation returns the display choice + /// ``BrazeInAppMessageUI/DisplayChoice/now``. + /// + /// 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. + /// + /// When returning ``BrazeInAppMessageUI/DisplayChoice/later``, the message is put on the top of + /// the message ``BrazeInAppMessageUI/stack``. Use ``BrazeInAppMessageUI/presentNext()`` to + /// present the next in-app message in the stack. + /// + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - message: The message to be presented. + /// - Returns: The display choice for `message`. See ``BrazeInAppMessageUI/DisplayChoice`` for + /// possible values. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + displayChoiceForMessage message: Braze.InAppMessage + ) -> BrazeInAppMessageUI.DisplayChoice + + /// Called before the in-app message display. Offers ways to deeply customize the in-app message + /// presentation via the mutable `context`. + /// + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - context: The presentation context. See ``BrazeInAppMessageUI/PresentationContext`` for a + /// list of supported customizations. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + prepareWith context: inout BrazeInAppMessageUI.PresentationContext + ) + + /// Called before the in-app message presentation and after + /// ``inAppMessage(_:prepareWith:)-11fog``. + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - message: The message to be presented. + /// - view: The in-app message view. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + willPresent message: Braze.InAppMessage, + view: InAppMessageView + ) + + /// Called once the in-app message is fully visible to the user. + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - message: The message to be presented. + /// - view: The in-app message view. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + didPresent message: Braze.InAppMessage, + view: InAppMessageView + ) + + /// Called before any dismissal animation occurs. + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - message: The message to be presented. + /// - view: The in-app message view. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + willDismiss message: Braze.InAppMessage, + view: InAppMessageView + ) + + /// Called after any dismissal animation occurs and the in-app message is fully hidden from the + /// user. + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - message: The message to be presented. + /// - view: The in-app message view. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + didDismiss message: Braze.InAppMessage, + view: InAppMessageView + ) + + /// Defines whether Braze should process the message click action. + /// + /// When returning `true` (default return value), Braze processes the click action. + /// + /// - Important: When this method returns `true` and the click action is an url, Braze will + /// **still** execute the `BrazeDelegate.braze(_:shouldOpenURL:)` delegate method + /// offering a last opportunity to modify or replace Braze url handling behavior. + /// If your implementation only needs access to the `url`, prefer using + /// `BrazeDelegate.braze(_:shouldOpenURL:)` instead. + /// + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - clickAction: The click action. + /// - buttonId: The optional button identifier. + /// - message: The message to be presented. + /// - view: The in-app message view. + /// - Returns: `true` to let Braze process the click action, `false` otherwise. + func inAppMessage( + _ ui: BrazeInAppMessageUI, + shouldProcess clickAction: Braze.InAppMessage.ClickAction, + buttonId: String?, + message: Braze.InAppMessage, + view: InAppMessageView + ) -> Bool + +} + +// MARK: - Default implementation + +extension BrazeInAppMessageUIDelegate { + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, + displayChoiceForMessage message: Braze.InAppMessage + ) -> BrazeInAppMessageUI.DisplayChoice { + .now + } + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, prepareWith context: inout BrazeInAppMessageUI.PresentationContext + ) {} + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, willPresent message: Braze.InAppMessage, + view: InAppMessageView + ) {} + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, didPresent message: Braze.InAppMessage, + view: InAppMessageView + ) {} + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, willDismiss message: Braze.InAppMessage, + view: InAppMessageView + ) {} + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, didDismiss message: Braze.InAppMessage, + view: InAppMessageView + ) {} + + public func inAppMessage( + _ ui: BrazeInAppMessageUI, + shouldProcess clickAction: Braze.InAppMessage.ClickAction, + buttonId: String?, + message: Braze.InAppMessage, + view: InAppMessageView + ) -> Bool { + return true + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIError.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIError.swift new file mode 100644 index 0000000000..43562ab4c9 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIError.swift @@ -0,0 +1,170 @@ +import BrazeKit + +extension BrazeInAppMessageUI { + + public enum Error: Swift.Error, Hashable { + case noContextLogImpression + case noContextLogClick + case noContextProcessClickAction + + case noMainThread + case noMessageView + case noAppRootViewController + case noFontAwesome + case noMatchingOrientation + + case otherMessagePresented(push: Bool) + case messageContextDiscarded + case messageContextInvalid + + case htmlNoBaseUrl + case webViewNavigation(Braze.ErrorString) + case webViewScript(Braze.WebViewBridge.ScriptMessageHandler.Error) + case webViewScheme(Braze.WebViewBridge.SchemeHandler.Error) + case webViewQuery(Braze.WebViewBridge.QueryHandler.Error) + } + +} + +// MARK: - Messages + +extension BrazeInAppMessageUI.Error { + + var logDescription: String { + switch self { + case .noContextLogImpression: + return "Cannot log impression for non-braze in-app message." + case .noContextLogClick: + return "Cannot log click for non-braze in-app message." + case .noContextProcessClickAction: + return "Cannot process click action for non-braze in-app message." + + case .noMainThread: + return + "Unable to present message - BrazeInAppMessageUI apis must be called from the main thread." + case .noMessageView: + return + "Unable to present message - invalid in-app message view. The message has been discarded." + case .noAppRootViewController: + return "Unable to present message - unable to find the app root view controller." + case .noFontAwesome: + return "Unable to load FontAwesome - icons will not be rendered correctly." + case .noMatchingOrientation: + return + "Unable to present message - current orientation / size classes are not supported by the message." + + case .otherMessagePresented(let push): + return + "Unable to present message - another message is already presented.\(push ? " The message has been pushed to the top of the stack." : "")" + case .messageContextDiscarded: + return + "Unable to present message - message already discarded. The message has been removed from the stack." + case .messageContextInvalid: + return + "Unable to present message - message context is invalid. The message has been discarded and removed from the stack." + + case .htmlNoBaseUrl: + return "Unable to present html in-app message - no base url." + case .webViewNavigation(let error): + return "Unable to load html in web view - \(error.logDescription)" + case .webViewScript(let error): + return error.logDescription + case .webViewScheme(let error): + return error.logDescription + case .webViewQuery(let error): + return error.logDescription + } + } + + var flattened: Braze.ErrorString { + .init(logDescription) + } + +} + +extension Braze.WebViewBridge.ScriptMessageHandler.Error { + + var logDescription: String { + switch self { + case .invalidPayload(let payload): + return + """ + Unable to process JavaScript bridge action - payload received from JavaScript side is invalid. + - payload: \(String(describing: payload)) + """ + case .noBrazeInstance(let action, let args): + return + """ + Unable to process JavaScript bridge action - missing Braze instance. + - action: \(action) + - args: \(args) + """ + case .invalidArg(let action, let index, let arg, let type): + return + """ + Unable to process JavaScript bridge action - invalid argument. + - action: \(action ?? "") + - index: \(index) + - arg: \(arg) + - type: \(type) + """ + case .deprecation(let message): + return message + case .unknown(let error): + return error.logDescription + @unknown default: + return "@unknown error" + } + } + +} + +extension Braze.WebViewBridge.SchemeHandler.Error { + + var logDescription: String { + + switch self { + case .invalidCustomEvent(let url): + return + """ + Unable to process custom event from url. + - url: \(url) + """ + case .invalidAction(let url): + return + """ + Unable to process action from url. + - url: \(url) + """ + case .noBrazeInstance(let url): + return + """ + Unable to process action from url - missing Braze instance. + - url: \(url) + """ + @unknown default: + return "@unknown error" + } + + } + +} + +extension Braze.WebViewBridge.QueryHandler.Error { + + var logDescription: String { + + switch self { + case .invalidButtonId(let id): + return + """ + Unable to log button click - only 0 and 1 are valid button ids. + - id: \(String(describing: id)) + """ + @unknown default: + return "@unknown error" + } + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift new file mode 100644 index 0000000000..2685cd23a5 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIExt.swift @@ -0,0 +1,86 @@ +import BrazeKit + +extension BrazeInAppMessageUI { + + private typealias Slideup = Braze.InAppMessage.Slideup + private typealias Modal = Braze.InAppMessage.Modal + private typealias ModalImage = Braze.InAppMessage.ModalImage + private typealias Full = Braze.InAppMessage.Full + private typealias FullImage = Braze.InAppMessage.FullImage + private typealias Html = Braze.InAppMessage.Html + private typealias Control = Braze.InAppMessage.Control + + /// The different display choices supported when receiving an in-app message from the Braze SDK. + /// + /// See ``BrazeInAppMessageUIDelegate/inAppMessage(_:displayChoiceForMessage:)-1ghly``. + public enum DisplayChoice { + + /// The in-app message is displayed immediately. + case now + + /// The in-app message is **not displayed** and placed on top of the ``BrazeInAppMessageUI/stack``. + /// + /// Use ``BrazeInAppMessageUI/presentNext()`` to display the message at the top of the stack. + case later + + /// The in-app message is discarded. + case discard + } + + /// Creates and return an in-app message view when `message` and `attributes` are valid, + /// returns `nil` otherwise. + func createMessageView( + for message: Braze.InAppMessage, + attributes: ViewAttributes?, + gifViewProvider: GIFViewProvider + ) -> InAppMessageView? { + + switch (message, attributes) { + + case (.slideup(let slideup), .slideup(let attributes)): + return SlideupView( + message: slideup, + attributes: attributes, + gifViewProvider: gifViewProvider + ) + + case (.modal(let modal), .modal(let attributes)): + return ModalView( + message: modal, + attributes: attributes, + gifViewProvider: gifViewProvider + ) + + case (.modalImage(let modalImage), .modalImage(let attributes)): + return ModalImageView( + message: modalImage, + attributes: attributes, + gifViewProvider: gifViewProvider + ) + + case (.full(let full), .full(let attributes)): + return FullView( + message: full, + attributes: attributes, + gifViewProvider: gifViewProvider + ) + + case (.fullImage(let fullImage), .fullImage(let attributes)): + return FullImageView( + message: fullImage, + attributes: attributes, + gifViewProvider: gifViewProvider + ) + + case (.html(let html), .html(let attributes)): + return HtmlView(message: html, attributes: attributes) + + case (.control(let control), _): + return ControlView(message: control) + + default: + return nil + } + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift new file mode 100644 index 0000000000..a584a984a2 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIPresentationContext.swift @@ -0,0 +1,123 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// Presentation context for an in-app message. + /// + /// A writable instance of this type is passed to the ``BrazeInAppMessageUI/delegate`` via the + /// ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` method. + public struct PresentationContext { + + /// The message to be presented. + /// + /// The message can be modified before presentation in + /// ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog``: + /// ```swift + /// // Disable animations on all message types + /// context.message.animateIn = false + /// context.message.animateOut = false + /// + /// // Force slideup messages to animate from the top + /// context.message.slideup?.slideFrom = .top + /// + /// // Read custom text from extras and apply it to multiple message types. + /// if let customText = context.message.extras["custom_text"] as? String { + /// context.message.slideup?.message = customText + /// context.message.modal?.message = customText + /// context.message.full?.message = customText + /// } + /// ``` + /// + /// - Important: Customizing the campaign via the Braze platform is preferred to using this + /// property directly. + public var message: Braze.InAppMessage + + /// The attributes for the message view to be presented. + /// + /// The view attributes can be modified before presentation in + /// ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog``: + /// ```swift + /// // Increase slideup image size + /// context.attributes?.slideup?.imageSize = CGSize(width: 100, height: 100) + /// + /// // Remove modals corner radius + /// context.attributes?.modal?.cornerRadius = 0 + /// context.attributes?.modalImage?.cornerRadius = 0 + /// ``` + /// + /// `attributes` is `nil` when displaying a control in-app message. + /// + /// To modify the default attributes, modify the `defaults` property on the attribute type. + /// For instance, the previous image size increase can be applied to all slideup in-app message + /// by setting: + /// ```swift + /// BrazeInAppMessageUI.SlideupView.Attributes.defaults.imageSize = CGSize(width: 100, height: 100) + /// ``` + public var attributes: ViewAttributes? + + /// A user-provided custom in-app message view to be used in place of the Braze in-app message + /// view. + public var customView: InAppMessageView? + + /// The preferred orientation used to present the message (default: current interface + /// orientation) + /// + /// The orientation is applied only for the presentation of the message. Once the device + /// changes orientation, the message view adopts one of the orientation it's support. + /// Use `message.orientation` to modify the supported orientations. + /// + /// - Important: On smaller devices (iPhones, iPod Touch), setting a landscape orientation for + /// a modal or full in-app message may lead to truncated content. + public var preferredOrientation: UIInterfaceOrientation + + /// Defines the status bar hide behavior (default: `.auto`). + /// + /// When set to ``BrazeInAppMessageUI/StatusBarHideBehavior/auto``, the in-app message view is + /// responsible for hiding and displaying the status bar when appropriate. + public var statusBarHideBehavior: StatusBarHideBehavior = .auto + + /// The window level for the in-app message window (default: `.normal`). + public var windowLevel: UIWindow.Level = .normal + + /// The window scene used to present the message (default: current active window scene). + @available(iOS 13.0, tvOS 13.0, *) + public var windowScene: UIWindowScene? { + get { _windowScene as? UIWindowScene } + set { _windowScene = newValue } + } + var _windowScene: Any? + + /// The view controller used to proxy `UIViewController` based preferences. (default: topmost + /// presented view controller on the application root view controller) + /// + /// Preferences includes: + /// - `UIViewController.prefersHomeIndicatorAutoHidden` + /// - `UIViewController.preferredScreenEdgesDeferringSystemGestures` + /// - `UIViewController.prefersPointerLocked` + /// - `UIViewController.preferredStatusBarStyle` + /// - `UIViewController.preferredStatusBarUpdateAnimation` + /// - `UIViewController.prefersStatusBarHidden` + /// - Only when ``statusBarHideBehavior`` is set to `.auto` and the message view hasn't + /// requested any specific hidden state. + /// + /// Set this value to a view controller implementing some of the aforementioned properties and + /// methods to customize the in-app message view controller presentation. + public var preferencesProxy: UIViewController? + } + + /// Different behaviors supported to hide and display the status bar. + public enum StatusBarHideBehavior { + + /// The message view decides the status bar hidden state. + case auto + + /// Always hide the status bar. + case hidden + + /// Always display the status bar. + case visible + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift new file mode 100644 index 0000000000..e164eb8e8a --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIViewController.swift @@ -0,0 +1,176 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view controller presenting the in-app message view in the ``Window``. + /// + /// This view controller is responsible for: + /// - orientation handling + /// - keyboard avoidance (via its view being a ``ContainerView``) + /// - status bar hide behavior + /// - preferences proxying + open class ViewController: UIViewController { + + /// The message displayed by `messageView`. + var message: Braze.InAppMessage + + /// The message view diplayed. + var messageView: InAppMessageView + + /// The message view container view. + var containerView: ContainerView + + /// The message view presented state. + var presented: Bool = false + + /// The in-app message ui instance. + /// + /// Used by the in-app message view for delegate handling. + weak var ui: BrazeInAppMessageUI? + + /// The preferences proxy. + /// + /// See ``BrazeInAppMessageUI/PresentationContext/preferencesProxy``. + weak var preferencesProxy: UIViewController? + + /// The preferred orientation. + /// + /// The view controller will adopt the preferred orientation until the **device** orientation + /// changes. At that point, the view controller will adopt any orientation matching + /// `supportedOrientations`. + var preferredOrientation: UIInterfaceOrientation + + /// The orientations supported by the message. + var supportedOrientations: UIInterfaceOrientationMask + + /// The status bar hide behavior. + /// + /// See ``BrazeInAppMessageUI/PresentationContext/statusBarHideBehavior`.` + var statusBarHideBehavior: StatusBarHideBehavior + + /// The message view preferred status bar hidden state. + /// + /// The message view can set this value via ``InAppMessageView/prefersStatusBarHidden`` to + /// customize the status bar hidden state. + var messageViewPrefersStatusBarHidden: Bool? { + didSet { setNeedsStatusBarAppearanceUpdate() } + } + + // MARK: - Initialization + + /// Creates an in-app message view controller. + /// - Parameters: + /// - ui: The in-app message ui instance. + /// - context: The in-app message presentaiton context. + /// - messageView: The in-app message view. + /// - keyboard: The keybord frame notifier. + init( + ui: BrazeInAppMessageUI, + context: PresentationContext, + messageView: InAppMessageView, + keyboard: KeyboardFrameNotifier = .shared + ) { + let traits = context.preferencesProxy?.traitCollection + + self.ui = ui + message = context.message + self.messageView = messageView + preferencesProxy = context.preferencesProxy + preferredOrientation = context.preferredOrientation + supportedOrientations = context.message.orientation.mask(for: traits) + statusBarHideBehavior = context.statusBarHideBehavior + containerView = .init(keyboard: keyboard) + + super.init(nibName: nil, bundle: nil) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - LifeCycle + + open override func loadView() { + self.view = containerView + } + + open override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if presented { return } + + view.addSubview(messageView) + messageView.present(completion: nil) + presented = true + } + + // MARK: - Orientation + + open override var supportedInterfaceOrientations: UIInterfaceOrientationMask { + !isViewLoaded + ? preferredOrientationMask + : supportedOrientations + } + + var preferredOrientationMask: UIInterfaceOrientationMask { + switch preferredOrientation { + case .unknown: + return .all + case .portrait: + return .portrait + case .portraitUpsideDown: + return .portraitUpsideDown + case .landscapeLeft: + return .landscapeLeft + case .landscapeRight: + return .landscapeRight + @unknown default: + return .all + } + } + + // MARK: - Preferences + + open override var prefersStatusBarHidden: Bool { + switch statusBarHideBehavior { + case .auto: + return messageViewPrefersStatusBarHidden + ?? preferencesProxy?.prefersStatusBarHidden + ?? false + case .hidden: + return true + case .visible: + return false + } + } + + open override var childForStatusBarStyle: UIViewController? { + preferencesProxy + } + + open override var preferredStatusBarStyle: UIStatusBarStyle { + preferencesProxy?.preferredStatusBarStyle ?? .default + } + + open override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { + preferencesProxy?.preferredStatusBarUpdateAnimation ?? .fade + } + + @available(iOS 11.0, *) + open override var prefersHomeIndicatorAutoHidden: Bool { + preferencesProxy?.prefersHomeIndicatorAutoHidden ?? false + } + + @available(iOS 11.0, *) + open override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge { + preferencesProxy?.preferredScreenEdgesDeferringSystemGestures ?? [] + } + + @available(iOS 14.0, *) + open override var prefersPointerLocked: Bool { + preferencesProxy?.prefersPointerLocked ?? false + } + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageUIWindow.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIWindow.swift new file mode 100644 index 0000000000..0293981550 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageUIWindow.swift @@ -0,0 +1,35 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The window displaying Braze's in-app messages. + /// + /// The window capture and process touch events from: + /// - The ``InAppMessageView`` or one of its subviews + /// - Any view when displaying an html in-app message + /// + /// All other touch events are passed down to the next window by UIKit. + open class Window: UIWindow, BrazeInAppMessageWindowType { + + open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + guard let view = super.hitTest(point, with: event) else { + return nil + } + + let isWindowTouch = + view is InAppMessageView + || view.responders.lazy.contains { $0 is InAppMessageView } + || messageViewController?.message.html != nil + + return isWindowTouch ? view : nil + } + + /// The message view controller being displayed. + var messageViewController: ViewController? { + rootViewController as? ViewController + } + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift b/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift new file mode 100644 index 0000000000..ca026e9846 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/InAppMessageViewAttributes.swift @@ -0,0 +1,101 @@ +import BrazeKit + +extension BrazeInAppMessageUI { + + public enum ViewAttributes { + case slideup(SlideupView.Attributes) + case modal(ModalView.Attributes) + case modalImage(ModalImageView.Attributes) + case full(FullView.Attributes) + case fullImage(FullImageView.Attributes) + case html(HtmlView.Attributes) + + public var slideup: SlideupView.Attributes? { + get { + guard case .slideup(let slideup) = self else { return nil } + return slideup + } + set { + guard let slideup = newValue else { return } + self = .slideup(slideup) + } + } + + public var modal: ModalView.Attributes? { + get { + guard case .modal(let modal) = self else { return nil } + return modal + } + set { + guard let modal = newValue else { return } + self = .modal(modal) + } + } + + public var modalImage: ModalImageView.Attributes? { + get { + guard case .modalImage(let modalImage) = self else { return nil } + return modalImage + } + set { + guard let modalImage = newValue else { return } + self = .modalImage(modalImage) + } + } + + public var full: FullView.Attributes? { + get { + guard case .full(let full) = self else { return nil } + return full + } + set { + guard let full = newValue else { return } + self = .full(full) + } + } + + public var fullImage: FullImageView.Attributes? { + get { + guard case .fullImage(let fullImage) = self else { return nil } + return fullImage + } + set { + guard let fullImage = newValue else { return } + self = .fullImage(fullImage) + } + } + + public var html: HtmlView.Attributes? { + get { + guard case .html(let html) = self else { return nil } + return html + } + set { + guard let html = newValue else { return } + self = .html(html) + } + } + + public static func defaults(for message: Braze.InAppMessage) -> Self? { + switch message { + case .slideup: + return .slideup(.defaults) + case .modal: + return .modal(.defaults) + case .modalImage: + return .modalImage(.defaults) + case .full: + return .full(.defaults) + case .fullImage: + return .fullImage(.defaults) + case .html: + return .html(.defaults) + case .control: + return nil + @unknown default: + return nil + } + } + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift new file mode 100644 index 0000000000..e4a616a332 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIContainerView.swift @@ -0,0 +1,85 @@ +import UIKit + +extension BrazeInAppMessageUI { + + /// The in-app message container view. + /// + /// This view is keyboard aware and updates its frame to take all available space. When the + /// keyboard is hidden, the view edges extends past the safe area. + open class ContainerView: UIView { + + // MARK: - LifeCycle + + /// Creates and returns a container view initialized with a keyboard frame notifier. + /// - Parameter keyboard: The keyboard frame notifier. + public init(keyboard: KeyboardFrameNotifier = .shared) { + self.keyboard = keyboard + super.init(frame: .zero) + keyboard.subscribe(identifier: ObjectIdentifier(self)) { [weak self] _ in + self?.updateConstraintsForKeyboard() + } + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + keyboard.unsubscribe(identifier: ObjectIdentifier(self)) + } + + // MARK: - Layout + + open var layoutConstraintsInstalled = false + open var bottomConstraint: NSLayoutConstraint? + + open override func layoutSubviews() { + super.layoutSubviews() + installLayoutConstraintsIfNeeded() + } + + open func installLayoutConstraintsIfNeeded() { + if layoutConstraintsInstalled { return } + layoutConstraintsInstalled = true + + anchors.edges.pin(axis: .horizontal) + anchors.top.pin() + bottomConstraint = anchors.bottom.pin() + superview?.layoutIfNeeded() + + updateConstraintsForKeyboard() + } + + // MARK: - Keyboard + + public let keyboard: KeyboardFrameNotifier + + open func updateConstraintsForKeyboard() { + guard let window = window, + keyboard.windows.contains(window), + let superview = superview + else { + return + } + + let frame = window.convert(keyboard.frame, to: superview) + .intersection(superview.frame) + bottomConstraint?.constant = -frame.height + UIView.animate( + withDuration: 0.25, + delay: 0, + options: .beginFromCurrentState, + animations: { self.superview?.layoutIfNeeded() }, + completion: nil + ) + } + + // MARK: - Touch + + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + subviews.first?.point(inside: convert(point, to: subviews.first), with: event) ?? false + } + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIControlView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIControlView.swift new file mode 100644 index 0000000000..c900133e9a --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIControlView.swift @@ -0,0 +1,48 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for control in-app messages. + /// + /// Control in-app messages are automatically dismissed as soon as they are presented. + open class ControlView: UIView, InAppMessageView { + + /// The control in-app message. + public var message: Braze.InAppMessage.Control + + /// Creates and returns a control in-app message view. + /// - Parameter message: The message. + public init(message: Braze.InAppMessage.Control) { + self.message = message + super.init(frame: .zero) + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public var presented: Bool = false + + public func present(completion: (() -> Void)? = nil) { + willPresent() + presented = true + logImpression() + completion?() + didPresent() + + // Dismiss directly + dismiss() + } + + public func dismiss(completion: (() -> Void)? = nil) { + willDismiss() + presented = false + completion?() + didDismiss() + } + + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift new file mode 100644 index 0000000000..b703a6967d --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullImageView.swift @@ -0,0 +1,293 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for full image in-app messages. + /// + /// The full image view can be customized using the ``ModalImageView/attributes-swift.property`` + /// property. + /// By default, the full image view takes all available space on small screens and is displayed as + /// a modal on large screens (e.g. iPad, Desktop). + open class FullImageView: ModalImageView { + + // MARK: - Attributes + + /// The attributes supported by the full image in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The minimum spacing around the content view (used when displayed as modal). + public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + + /// The spacing around the content's view content. + public var padding = UIEdgeInsets(top: 0, left: 25, bottom: 30, right: 25) + + /// The content view corner radius. + public var cornerRadius = 8.0 + + /// The content view shadow. + public var shadow: Shadow? = Shadow.default + + /// The minimum width (used when displayed as modal). + public var minWidth = 320.0 + + /// The maximum width (used when displayed as modal). + public var maxWidth = 450.0 + + /// The maximum height (used when displayed as modal). + public var maxHeight = 720.0 + + /// Specify whether the full image in-app message view displays the image in a scroll view + /// for large images. + public var scrollLargeImages = false + + /// Specify whether the full image in-app message view can be dismissed from a tap with the + /// view's background. + public var dismissOnBackgroundTap = false + + /// Specify the preferred display mode. See + /// ``BrazeInAppMessageUI/FullImageView/DisplayMode-swift.enum``. + public var preferredDisplayMode: DisplayMode? + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((ModalImageView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((ModalImageView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((ModalImageView) -> Void)? + + /// The defaults full image view attributes. + /// + /// Modify this value directly to apply the customizations to all full image in-app messages + /// presented by the SDK. + public static var defaults = Self() + + } + + public var preferredDisplayMode: DisplayMode? { + didSet { updateForDisplayMode() } + } + + /// Display modes supported by the full image in-app message view. + public enum DisplayMode { + + /// Displays the view as a modal. + case modal + + /// Displays the view full screen. + case full + } + + public lazy var displayMode: DisplayMode = + (traitCollection.horizontalSizeClass == .compact + ? .full + : .modal) + { + didSet { updateForDisplayMode() } + } + + var modalMargin: UIEdgeInsets + var modalMaxHeight: Double + var modalCornerRadius: Double + + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if preferredDisplayMode == nil { + displayMode = traitCollection.horizontalSizeClass == .compact ? .full : .modal + } + } + + func updateForDisplayMode() { + let displayMode = preferredDisplayMode ?? self.displayMode + + switch displayMode { + case .modal: + // - attributes + attributes.margin = modalMargin + attributes.maxHeight = modalMaxHeight + attributes.cornerRadius = modalCornerRadius + + // - layout + maxWidthConstraint.isActive = true + + NSLayoutConstraint.deactivate(contentPositionConstraints) + contentPositionConstraints = + contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + + contentView.anchors.center.align() + + prefersStatusBarHidden = nil + + case .full: + // - attributes + attributes.margin = .zero + attributes.maxHeight = 10000 + attributes.cornerRadius = 0 + + // - layout + maxWidthConstraint.isActive = false + + NSLayoutConstraint.deactivate(contentPositionConstraints) + contentPositionConstraints = contentView.anchors.edges.pin() + + prefersStatusBarHidden = true + } + + } + + public init( + message: Braze.InAppMessage.FullImage, + attributes: Attributes = .defaults, + gifViewProvider: GIFViewProvider = .default, + presented: Bool = false + ) { + var modalImageAttrs = ModalImageView.Attributes() + modalImageAttrs.margin = attributes.margin + modalImageAttrs.padding = attributes.padding + modalImageAttrs.cornerRadius = attributes.cornerRadius + modalImageAttrs.shadow = attributes.shadow + modalImageAttrs.minWidth = attributes.minWidth + modalImageAttrs.maxWidth = attributes.maxWidth + modalImageAttrs.maxHeight = attributes.maxHeight + modalImageAttrs.scrollLargeImages = attributes.scrollLargeImages + modalImageAttrs.dismissOnBackgroundTap = attributes.dismissOnBackgroundTap + modalImageAttrs.onPresent = { attributes.onPresent?($0 as! FullImageView) } + modalImageAttrs.onLayout = { attributes.onLayout?($0 as! FullImageView) } + modalImageAttrs.onTheme = { attributes.onTheme?($0 as! FullImageView) } + modalMargin = attributes.margin + modalCornerRadius = attributes.cornerRadius + modalMaxHeight = attributes.maxHeight + preferredDisplayMode = attributes.preferredDisplayMode + + super.init( + message: .init( + data: message.data, + imageUri: message.imageUri, + buttons: message.buttons, + themes: message.themes + ), + attributes: modalImageAttrs, + gifViewProvider: gifViewProvider, + presented: presented + ) + + imageView.clipsToBounds = true + + updateForDisplayMode() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Presentation / InAppMessageView conformance + + public override func present(completion: (() -> Void)?) { + super.present(completion: completion) + updateForDisplayMode() + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct FullImageView_Previews: PreviewProvider { + typealias FullImageView = BrazeInAppMessageUI.FullImageView + + static var previews: some View { + Group { + variationFullPreviews + variationModalPreviews + dimensionsPreviews + // customPreviews + } + } + + @ViewBuilder + static var variationFullPreviews: some View { + FullImageView(message: .mock, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | Default") + + FullImageView(message: .mockOneButton, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | 1 Button") + + FullImageView(message: .mockTwoButtons, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | 2 Buttons") + } + + @ViewBuilder + static var variationModalPreviews: some View { + FullImageView(message: .mock, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | Default") + + FullImageView(message: .mockOneButton, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | 1 Button") + + FullImageView(message: .mockTwoButtons, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | 2 Buttons") + } + + @ViewBuilder + static var dimensionsPreviews: some View { + FullImageView(message: .mockRecommendedSize, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Recommended size") + + FullImageView(message: .mockMinRecommendedSize, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Min. recommended size") + + FullImageView(message: .mockNonRecommendedSize, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Non recommended size (extra large image)") + } + + static var customPreviews: some View { + var attributes = FullImageView.Attributes() + attributes.scrollLargeImages = true + + return FullImageView( + message: .mockNonRecommendedSize, + attributes: attributes, + presented: true + ) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Custom | Scroll Large Images") + } + + } +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift new file mode 100644 index 0000000000..bb1da01a13 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIFullView.swift @@ -0,0 +1,385 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for full in-app messages. + /// + /// The full view can be customized using the ``ModalView/attributes-swift.property`` property. + /// By default, the full view takes all available space on small screens and is displayed as a + /// modal on large screens (e.g. iPad, Desktop). + open class FullView: ModalView { + + // MARK: - Attributes + + /// The attributes supported by the full in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The minimum spacing around the content view (used when displayed as modal). + public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + + /// The spacing around the content's view content. + public var padding = UIEdgeInsets(top: 40, left: 25, bottom: 30, right: 25) + + /// The spacing between the header and message + public var labelsSpacing = 10.0 + + /// The spacing between the graphic, the labels scroll view and the buttons. + public var spacing = 20.0 + + /// The font for the header. + public var headerFont = UIFont.preferredFont(textStyle: .title3, weight: .bold) + + /// The font for the message. + public var messageFont = UIFont.preferredFont(forTextStyle: .subheadline) + + /// The content view corner radius. + public var cornerRadius = 8.0 + + /// The content view shadow. + public var shadow: Shadow? = Shadow.default + + /// The minimum width (used when displayed as modal). + public var minWidth = 320.0 + + /// The maximum width (used when displayed as modal). + public var maxWidth = 450.0 + + /// The maximum height (used when displayed as modal). + public var maxHeight = 720.0 + + /// Specify whether the full in-app message view can be dismissed from a tap with the + /// view's background. + public var dismissOnBackgroundTap = false + + /// Specify the preferred display mode. See + /// ``BrazeInAppMessageUI/FullView/DisplayMode-swift.enum``. + public var preferredDisplayMode: DisplayMode? + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((FullView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((FullView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((FullView) -> Void)? + + /// The defaults full view attributes. + /// + /// Modify this value directly to apply the customizations to all full in-app messages + /// presented by the SDK. + public static var defaults = Self() + + } + + public var preferredDisplayMode: DisplayMode? { + didSet { updateForDisplayMode() } + } + + /// Display modes supported by the full in-app message view. + public enum DisplayMode { + + /// Displays the view as a modal. + case modal + + /// Displays the view full screen. + case full + } + + public lazy var displayMode: DisplayMode = + (traitCollection.horizontalSizeClass == .compact + ? .full + : .modal) + { + didSet { updateForDisplayMode() } + } + + // MARK: - Views + + public var imageView: UIImageView { + graphicView as! UIImageView + } + + open override func installInternalConstraints() { + super.installInternalConstraints() + + imageConstraint?.isActive = false + imageConstraint = imageView.anchors.height.equal( + contentView.anchors.height.multiplied(by: 0.5)) + } + + var modalMargin: UIEdgeInsets + var modalMaxHeight: Double + var modalCornerRadius: Double + var modalPaddingBottom: Double + + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if preferredDisplayMode == nil { + displayMode = traitCollection.horizontalSizeClass == .compact ? .full : .modal + } + } + + func updateForDisplayMode() { + let displayMode = preferredDisplayMode ?? self.displayMode + + switch displayMode { + case .modal: + // - attributes + attributes.margin = modalMargin + attributes.maxHeight = modalMaxHeight + attributes.cornerRadius = modalCornerRadius + attributes.padding.bottom = modalPaddingBottom + + // - layout + maxWidthConstraint.isActive = true + + NSLayoutConstraint.deactivate(contentPositionConstraints) + contentPositionConstraints = + contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + + contentView.anchors.center.align() + + NSLayoutConstraint.deactivate(contentView.stackPositionConstraints) + contentView.stackPositionConstraints = contentView.stack.anchors.edges.pin() + contentView.stack.isLayoutMarginsRelativeArrangement = true + + prefersStatusBarHidden = nil + + case .full: + // - attributes + attributes.margin = .zero + attributes.maxHeight = 10000 + attributes.cornerRadius = 0 + attributes.padding.bottom = 0 + + // - layout + maxWidthConstraint.isActive = false + + NSLayoutConstraint.deactivate(contentPositionConstraints) + contentPositionConstraints = contentView.anchors.edges.pin() + + NSLayoutConstraint.deactivate(contentView.stackPositionConstraints) + contentView.stackPositionConstraints = + contentView.stack.anchors.edges.pin(axis: .horizontal) + + [ + contentView.stack.anchors.top.pin(), + contentView.stack.anchors.bottom.pin( + to: contentView.layoutMarginsGuide, + inset: attributes.padding.bottom + ), + ] + contentView.stack.isLayoutMarginsRelativeArrangement = false + + prefersStatusBarHidden = true + } + + } + + // MARK: - LifeCycle + + public init( + message: Braze.InAppMessage.Full, + attributes: Attributes = .defaults, + gifViewProvider: GIFViewProvider = .default, + presented: Bool = false + ) { + var modalViewAttrs = ModalView.Attributes() + modalViewAttrs.margin = attributes.margin + modalViewAttrs.padding = attributes.padding + modalViewAttrs.labelsSpacing = attributes.labelsSpacing + modalViewAttrs.spacing = attributes.spacing + modalViewAttrs.headerFont = attributes.headerFont + modalViewAttrs.messageFont = attributes.messageFont + modalViewAttrs.cornerRadius = attributes.cornerRadius + modalViewAttrs.shadow = attributes.shadow + modalViewAttrs.minWidth = attributes.minWidth + modalViewAttrs.maxWidth = attributes.maxWidth + modalViewAttrs.maxHeight = attributes.maxHeight + modalViewAttrs.dismissOnBackgroundTap = attributes.dismissOnBackgroundTap + modalViewAttrs.onPresent = { attributes.onPresent?($0 as! FullView) } + modalViewAttrs.onLayout = { attributes.onLayout?($0 as! FullView) } + modalViewAttrs.onTheme = { attributes.onTheme?($0 as! FullView) } + modalMargin = attributes.margin + modalCornerRadius = attributes.cornerRadius + modalMaxHeight = attributes.maxHeight + modalPaddingBottom = attributes.padding.bottom + preferredDisplayMode = attributes.preferredDisplayMode + + super.init( + message: .init( + data: message.data, + graphic: .image(message.imageUri), + header: message.header, + headerTextAlignment: message.headerTextAlignment, + message: message.message, + messageTextAlignment: message.messageTextAlignment, + buttons: message.buttons, + themes: message.themes + ), + attributes: modalViewAttrs, + gifViewProvider: gifViewProvider, + presented: presented + ) + + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + + // Bottom spacer view + contentView.stack.addArrangedSubview(UIView()) + + updateForDisplayMode() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Presentation / InAppMessageView conformance + + public override func present(completion: (() -> Void)?) { + super.present(completion: completion) + updateForDisplayMode() + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 14.0, *) + struct FullView_Previews: PreviewProvider { + typealias FullView = BrazeInAppMessageUI.FullView + + static var previews: some View { + Group { + // variationFullPreviews + // variationModalPreviews + // dimensionPreviews + // rightToLeftPreviews + themePreviews + } + } + + @ViewBuilder + static var variationFullPreviews: some View { + FullView(message: .mock, presented: true) + .preview() + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | Default") + + FullView(message: .mockOneButton, presented: true) + .preview() + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | 1 Button") + + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | 2 Buttons") + + FullView(message: .mockLeadingAligned, presented: true) + .preview() + .frame(maxHeight: 800) + .previewDisplayName("Var. Full | Leading alignment") + + FullView(message: .mockLong, presented: true) + .preview() + .frame(maxHeight: 500) + .previewDisplayName("Var. Full | Long (constrained)") + } + + @ViewBuilder + static var variationModalPreviews: some View { + FullView(message: .mock, presented: true) + .preview() + .frame(width: 540, height: 820) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | Default") + + FullView(message: .mockOneButton, presented: true) + .preview() + .frame(width: 540, height: 820) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | 1 Button") + + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(width: 540, height: 820) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | 2 Buttons") + + FullView(message: .mockLeadingAligned, presented: true) + .preview() + .frame(width: 540, height: 820) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | Leading alignment") + + FullView(message: .mockLong, presented: true) + .preview() + .frame(width: 540, height: 500) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Var. Modal | Long (constrained)") + } + + @ViewBuilder + static var dimensionPreviews: some View { + + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(width: 900, height: 900) + .environment(\.horizontalSizeClass, .compact) + .previewDisplayName("Dimensions | Full (no max)") + + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(width: 540, height: 820) + .environment(\.horizontalSizeClass, .regular) + .previewDisplayName("Dimensions | Modal (constrained)") + } + + // OpenRadar: https://archive.md/zr3l4 + // swift-snapshot-testing issue: https://archive.md/dnUQM + @ViewBuilder + static var rightToLeftPreviews: some View { + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(maxHeight: 800) + .environment(\.layoutDirection, .rightToLeft) + .previewDisplayName("RTL Support | Default") + } + + @ViewBuilder + static var themePreviews: some View { + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(maxHeight: 800) + .preferredColorScheme(.light) + .previewDisplayName("Theme | Light") + + FullView(message: .mockTwoButtons, presented: true) + .preview() + .frame(maxHeight: 800) + .preferredColorScheme(.dark) + .previewDisplayName("Theme | Dark") + + FullView(message: .mockThemed, presented: true) + .preview() + .frame(maxHeight: 800) + .previewDisplayName("Theme | Custom") + + } + } +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift new file mode 100644 index 0000000000..a681f50191 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIHtmlView.swift @@ -0,0 +1,451 @@ +import BrazeKit +import UIKit +import WebKit + +extension BrazeInAppMessageUI { + + /// The view for html in-app messages + /// + /// The html view can be customized using the ``attributes-swift.property`` property. + open class HtmlView: UIView, InAppMessageView { + + /// The html in-app message. + public var message: Braze.InAppMessage.Html + + // MARK: - Attributes + + /// The attributes supported by the html in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The animation used to present the view. + public var animation: Animation = .auto + + /// Closure allowing customization of the configuration used by the web view. + public var configure: ((WKWebViewConfiguration) -> Void)? + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((HtmlView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((HtmlView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((HtmlView) -> Void)? + + /// The defaults html view attributes. + /// + /// Modify this value directly to apply the customizations to all html in-app messages + /// presented by the SDK. + public static var defaults = Self() + } + + /// The view attributes. See ``Attributes-swift.struct``. + public let attributes: Attributes + + // MARK: - Animation + + /// The presentation animations supported by the html in-app message view. + public enum Animation { + + /// The animation chosen is automatically by the view. + /// + /// - ``slide`` is used for legacy (zip-based) html in-app messages. + /// - ``fade`` is used for html in-app messages with interactive preview on the dashboard. + case auto + + /// The view fades-in when presented and fades-out when dismissed. + case fade + + /// The view slides from the bottom of the screen when presented and slides back when + /// dismissed. + case slide + + func duration(legacy: Bool) -> TimeInterval { + switch self { + case .auto where legacy, .slide: + return 0.4 + case .auto, .fade: + return 0.25 + } + } + + func initialAlpha(legacy: Bool) -> CGFloat { + switch self { + case .auto where legacy, .slide: + return 1 + case .auto, .fade: + return 0 + } + } + } + + // MARK: - WebView + + /// The web view used to display the message. + /// + /// This property can be used after ``present(completion:)`` was called. + public var webView: WKWebView? + + public lazy var scriptMessageHandler: Braze.WebViewBridge.ScriptMessageHandler = + webViewScriptMessageHandler() + public lazy var schemeHandler: Braze.WebViewBridge.SchemeHandler = webViewSchemeHandler() + public lazy var queryHandler: Braze.WebViewBridge.QueryHandler = webViewQueryHandler() + + // MARK: - LifeCycle + + public init( + message: Braze.InAppMessage.Html, + attributes: Attributes = .defaults, + presented: Bool = false + ) { + self.message = message + self.attributes = attributes + self.presented = presented + super.init(frame: .zero) + } + + @available(*, unavailable) + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + // Cleanup userContentController because it: + // - strongly retain its scripts and script message handlers + // - seems to outlive the configuration / web view instance + // - manual cleanup here ensure proper deallocation of those objects + let userContentController = webView?.configuration.userContentController + userContentController?.removeAllUserScripts() + userContentController?.removeScriptMessageHandler( + forName: Braze.WebViewBridge.ScriptMessageHandler.name + ) + } + + // MARK: - Theme + + open override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + attributes.onTheme?(self) + } + + // MARK: - Layout + + open var presentationConstraintsInstalled = false + open var yConstraint: NSLayoutConstraint? + + open override func layoutSubviews() { + super.layoutSubviews() + installPresentationConstraintsIfNeeded() + attributes.onLayout?(self) + } + + open func installPresentationConstraintsIfNeeded() { + guard let superview = superview, + webView?.superview != nil, + !presentationConstraintsInstalled + else { + return + } + presentationConstraintsInstalled = true + + Constraints { + anchors.edges.pin() + + webView?.anchors.edges.pin(axis: .horizontal) + webView?.anchors.height.equal(anchors.height) + + switch attributes.animation { + case .auto where message.legacy, .slide: + yConstraint = webView?.anchors.top.pin() + webView?.anchors.top.equal(anchors.bottom).priority = .defaultHigh + case .auto, .fade: + break + } + } + + yConstraint?.isActive = presented + + setNeedsLayout() + superview.layoutIfNeeded() + } + + // MARK: - Presentation / InAppMessageView conformance + + private var presentationCompletion: (() -> Void)? + + public var presented: Bool = false { + didSet { + switch attributes.animation { + case .auto where message.legacy, .slide: + yConstraint?.isActive = presented + case .auto, .fade: + webView?.alpha = presented ? 1 : 0 + } + } + } + + public func present(completion: (() -> Void)? = nil) { + prefersStatusBarHidden = true + + setupWebView() + installPresentationConstraintsIfNeeded() + + willPresent() + attributes.onPresent?(self) + + UIView.performWithoutAnimation { + superview?.layoutIfNeeded() + } + + presentationCompletion = completion + loadMessage() + } + + public func dismiss(completion: (() -> Void)? = nil) { + willDismiss() + webView?.stopLoading() + + UIView.animate( + withDuration: message.animateOut + ? attributes.animation.duration(legacy: message.legacy) + : 0, + animations: { + self.presented = false + self.superview?.layoutIfNeeded() + }, + completion: { _ in + completion?() + self.didDismiss() + } + ) + } + + // MARK: - Helpers + + open func setupWebView() { + // Configuration + let configuration = WKWebViewConfiguration() + configuration.suppressesIncrementalRendering = true + configuration.allowsInlineMediaPlayback = true + + // - Customization + attributes.configure?(configuration) + + // - Script message handler + typealias ScriptMessageHandler = Braze.WebViewBridge.ScriptMessageHandler + configuration.userContentController.addUserScript(ScriptMessageHandler.script) + configuration.userContentController.add(scriptMessageHandler, name: ScriptMessageHandler.name) + + // WebView + let webView = WKWebView(frame: .zero, configuration: configuration) + webView.uiDelegate = self + webView.navigationDelegate = self + webView.allowsLinkPreview = false + webView.scrollView.bounces = false + webView.backgroundColor = .clear + webView.isOpaque = false + // Disable this optimization for mac catalyst (force webview in window bounds) + #if !targetEnvironment(macCatalyst) + if #available(iOS 11.0, *) { + // Make web view ignore the safe area allowing proper handling in html / css. See the usage + // section at https://developer.mozilla.org/en-US/docs/Web/CSS/env() (archived version: + // https://archive.is/EBSJD) for instructions. + webView.scrollView.contentInsetAdjustmentBehavior = .never + } + #endif + webView.alpha = attributes.animation.initialAlpha(legacy: message.legacy) + addSubview(webView) + self.webView = webView + } + + open func loadMessage() { + guard let baseUrl = message.baseUrl else { + logError(.htmlNoBaseUrl) + message.animateOut = false + dismiss() + return + } + + // Create directory if needed + try? FileManager.default.createDirectory( + at: baseUrl, + withIntermediateDirectories: true, + attributes: nil + ) + + // Write index.html + let index = baseUrl.appendingPathComponent("index.html") + try? message.message.write(to: index, atomically: true, encoding: .utf8) + + // Load + webView?.loadFileURL(index, allowingReadAccessTo: baseUrl) + } + + } + +} + +// MARK: - Navigation + +extension BrazeInAppMessageUI.HtmlView: WKNavigationDelegate { + + public func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + let isIframeLoad = + navigationAction.targetFrame != nil + && navigationAction.sourceFrame != navigationAction.targetFrame + + guard let url = navigationAction.request.url, + url.isFileURL == false, + isIframeLoad == false + else { + decisionHandler(.allow) + return + } + + decisionHandler(.cancel) + + if let action = schemeHandler.action(url: url) { + schemeHandler.process(action: action, url: url) + return + } + + let (clickAction, buttonId) = queryHandler.process(url: url) + process(clickAction: clickAction, buttonId: buttonId) + dismiss() + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + // Drag and drop interaction handler does not exist until the message is loaded. + webView.disableDragAndDrop() + // Disable selection by inserting css + webView.disableSelection() + + makeKey() + + UIView.animate( + withDuration: message.animateIn + ? attributes.animation.duration(legacy: message.legacy) + : 0, + animations: { + self.presented = true + self.superview?.layoutIfNeeded() + }, + completion: { _ in + self.logImpression() + self.presentationCompletion?() + self.presentationCompletion = nil + self.didPresent() + } + ) + } + + public func webView( + _ webView: WKWebView, + didFail navigation: WKNavigation!, + withError error: Error + ) { + logError(.webViewNavigation(.init(error))) + message.animateOut = false + dismiss() + } + +} + +// MARK: - alert / confirm / prompt + +extension BrazeInAppMessageUI.HtmlView: WKUIDelegate { + + private func presentAlert(message: String, configure: (UIAlertController) -> Void) { + let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert) + configure(alert) + controller?.present(alert, animated: true, completion: nil) + } + + public func webView( + _ webView: WKWebView, + runJavaScriptAlertPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping () -> Void + ) { + presentAlert(message: message) { + $0.addAction(.init(title: "Close", style: .default, handler: { _ in completionHandler() })) + } + } + + public func webView( + _ webView: WKWebView, + runJavaScriptConfirmPanelWithMessage message: String, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (Bool) -> Void + ) { + presentAlert(message: message) { + $0.addAction( + .init(title: "Cancel", style: .cancel, handler: { _ in completionHandler(false) })) + $0.addAction(.init(title: "OK", style: .default, handler: { _ in completionHandler(true) })) + } + } + + public func webView( + _ webView: WKWebView, + runJavaScriptTextInputPanelWithPrompt prompt: String, + defaultText: String?, + initiatedByFrame frame: WKFrameInfo, + completionHandler: @escaping (String?) -> Void + ) { + presentAlert(message: prompt) { alert in + alert.addTextField { $0.text = defaultText } + alert.addAction( + .init(title: "Cancel", style: .cancel, handler: { _ in completionHandler(nil) })) + alert.addAction( + .init( + title: "OK", style: .default, + handler: { _ in completionHandler(alert.textFields?.first?.text) })) + } + } + +} + +// MARK: - Misc. + +extension WKWebView { + + fileprivate func disableDragAndDrop() { + if #available(iOS 11.0, *) { + self + .bfsSubviews + .lazy + .first { $0.interactions.contains(where: { $0 is UIDragInteraction }) }? + .interactions + .filter { $0 is UIDragInteraction } + .forEach { $0.view?.removeInteraction($0) } + } + } + + fileprivate func disableSelection() { + evaluateJavaScript( + """ + const css = `* { + -webkit-touch-callout: none; + -webkit-user-select: none; + }` + const head = document.head || document.getElementsByTagName('head')[0] + var style = document.createElement('style') + style.type = 'text/css' + style.appendChild(document.createTextNode(css)) + head.appendChild(style) + """ + ) + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift new file mode 100644 index 0000000000..cdf2f844cf --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalImageView.swift @@ -0,0 +1,415 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for modal image in-app messages. + /// + /// The modal image view can be customized using the ``attributes-swift.property`` property. + open class ModalImageView: UIView, InAppMessageView { + + /// The modal image in-app message. + public var message: Braze.InAppMessage.ModalImage + + // MARK: - Attributes + + /// The attributes supported by the modal image in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The minimum spacing around the content view. + public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + + /// The spacing around the content's view content. + public var padding = UIEdgeInsets(top: 0, left: 25, bottom: 30, right: 25) + + /// The content view corner radius. + public var cornerRadius = 8.0 + + /// The content view shadow. + public var shadow: Shadow? = Shadow.default + + /// The minimum width. + public var minWidth = 320.0 + + /// The maximum width. + public var maxWidth = 450.0 + + /// The maximum height. + public var maxHeight = 720.0 + + /// Specify whether the modal image in-app message view displays the image in a scroll view + /// for large images. + public var scrollLargeImages = false + + /// Specify whether the modal image in-app message view can be dismissed from a tap with the + /// view's background. + public var dismissOnBackgroundTap = false + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((ModalImageView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((ModalImageView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((ModalImageView) -> Void)? + + /// The defaults modal image view attributes. + /// + /// Modify this value directly to apply the customizations to all modal in-app messages + /// presented by the SDK. + public static var defaults = Self() + + } + + public var attributes: Attributes { + didSet { applyAttributes() } + } + + open func applyAttributes() { + // Margin + layoutMargins = attributes.margin + + // Padding + buttonsContainer?.layoutMargins = attributes.padding + + // Corner radius + contentView.layer.cornerRadius = attributes.cornerRadius + + // Shadow + contentView.shadow = attributes.shadow + + // Dimensions + minWidthConstraint.constant = attributes.minWidth + maxWidthConstraint.constant = attributes.maxWidth + maxHeightConstraint.constant = attributes.maxHeight + + // Scroll large image + imageContainerView.isScrollEnabled = attributes.scrollLargeImages + imageCenterConstraints.forEach { + $0.isActive = !attributes.scrollLargeImages + } + + // User interactions + tapBackgroundGesture.isEnabled = attributes.dismissOnBackgroundTap + + setNeedsLayout() + layoutIfNeeded() + } + + // MARK: - Views + + let gifViewProvider: GIFViewProvider + + public lazy var imageView: UIView = { + let imageView = gifViewProvider.view(message.imageUri) + imageView.contentMode = .scaleAspectFill + return imageView + }() + + public lazy var imageContainerView: UIScrollView = { + let view = UIScrollView() + view.addSubview(imageView) + if #available(iOS 11, *) { + view.contentInsetAdjustmentBehavior = .never + } else { + // No need for iOS 10 support. + // No devices supporting iOS 10 has a need for safe-area handling. + } + return view + }() + + public lazy var buttonsContainer: StackView? = { + let container = StackView( + buttons: message.buttons, + onClick: { [weak self] button in + guard let self = self else { return } + self.logClick(buttonId: "\(button.id)") + self.process(clickAction: button.clickAction, buttonId: "\(button.id)") + self.dismiss() + } + ) + container?.stack.spacing = 10 + return container + }() + + public lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setTitle("✕", for: .normal) + button.accessibilityLabel = localize( + "braze.in-app-message.close-button.title", + for: .inAppMessage + ) + button.titleLabel?.font = .preferredFont(forTextStyle: .title2) + button.addAction { [weak self] in self?.dismiss() } + return button + }() + + public lazy var contentView: UIView = { + let view = UIView() + view.addSubview(imageContainerView) + if let buttonsContainer = buttonsContainer { + view.addSubview(buttonsContainer) + } + view.addSubview(closeButton) + view.clipsToBounds = true + return view + }() + + // MARK: - LifeCycle + + public init( + message: Braze.InAppMessage.ModalImage, + attributes: Attributes = .defaults, + gifViewProvider: GIFViewProvider = .default, + presented: Bool = false + ) { + self.message = message + self.attributes = attributes + self.gifViewProvider = gifViewProvider + self.presented = presented + + super.init(frame: .zero) + + addSubview(contentView) + installInternalConstraints() + + addGestureRecognizer(tapBackgroundGesture) + contentView.addGestureRecognizer(tapGesture) + + applyTheme() + applyAttributes() + + alpha = presented ? 1 : 0 + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Theme + + public var theme: Braze.InAppMessage.Theme { message.theme(for: traitCollection) } + + open func applyTheme() { + closeButton.setTitleColor(theme.closeButtonColor.uiColor, for: .normal) + contentView.backgroundColor = theme.backgroundColor.uiColor + backgroundColor = theme.frameColor.uiColor + + attributes.onTheme?(self) + } + + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + applyTheme() + } + + // MARK: - Layout + + open var presentationConstraintsInstalled = false + var minWidthConstraint: NSLayoutConstraint! + var maxWidthConstraint: NSLayoutConstraint! + var maxHeightConstraint: NSLayoutConstraint! + var imageAspectRatioConstraint: NSLayoutConstraint! + var imageCenterConstraints: [NSLayoutConstraint]! + var contentPositionConstraints: [NSLayoutConstraint]! + + open override func layoutSubviews() { + super.layoutSubviews() + installPresentationConstraintsIfNeeded() + contentView.updateShadow() + attributes.onLayout?(self) + } + + open func installInternalConstraints() { + // Dummy frame for first layout pass + frame = CGRect(x: 0, y: 0, width: 500, height: 500) + Constraints { + // ImageView + if let imageSize = imageSize(url: message.imageUri) { + let aspectRatio = imageSize.width / imageSize.height + imageAspectRatioConstraint = imageView.anchors.width.equal( + imageView.anchors.height.multiplied(by: aspectRatio) + ) + imageAspectRatioConstraint.priority = .defaultHigh + } + imageView.anchors.edges.pin() + imageView.anchors.width.equal(imageContainerView.anchors.width) + imageView.anchors.height.equal(imageContainerView.anchors.height).priority = .defaultHigh + imageCenterConstraints = imageView.anchors.center.align() + + // Container scrollview + imageContainerView.anchors.edges.pin() + // - dimensions + minWidthConstraint = imageContainerView.anchors.width.greaterThanOrEqual( + attributes.minWidth) + minWidthConstraint.priority = .defaultHigh + maxWidthConstraint = imageContainerView.anchors.width.lessThanOrEqual(attributes.maxWidth) + maxHeightConstraint = imageContainerView.anchors.height.lessThanOrEqual( + attributes.maxHeight) + + // Buttons + buttonsContainer?.anchors.edges.pin( + to: contentView.layoutMarginsGuide, + alignment: .bottom + ) + + // Close button + closeButton.anchors.height.equal(closeButton.anchors.width) + closeButton.anchors.edges.pin( + to: contentView.layoutMarginsGuide, + alignment: .topTrailing + ) + + // Content view + contentView.anchors.height.equal(imageContainerView.anchors.height) + contentPositionConstraints = + contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + + contentView.anchors.center.align() + } + } + + open func installPresentationConstraintsIfNeeded() { + guard let superview = superview, !presentationConstraintsInstalled else { return } + presentationConstraintsInstalled = true + anchors.edges.pin() + setNeedsLayout() + superview.layoutIfNeeded() + } + + // MARK: - Presentation / InAppMessageView conformance + + public var presented: Bool = false { + didSet { alpha = presented ? 1 : 0 } + } + + public func present(completion: (() -> Void)?) { + installPresentationConstraintsIfNeeded() + + willPresent() + attributes.onPresent?(self) + + UIView.performWithoutAnimation { + superview?.layoutIfNeeded() + } + + makeKey() + + UIView.animate( + withDuration: message.animateIn ? 0.25 : 0, + animations: { self.presented = true }, + completion: { _ in + self.logImpression() + completion?() + self.didPresent() + } + ) + } + + @objc + public func dismiss(completion: (() -> Void)? = nil) { + willDismiss() + UIView.animate( + withDuration: message.animateOut ? 0.25 : 0, + animations: { self.presented = false }, + completion: { _ in + completion?() + self.didDismiss() + } + ) + } + + // MARK: - User Interactions + + open lazy var tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(tap) + ) + + @objc + func tap(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended else { + return + } + logClick() + process(clickAction: message.clickAction) + dismiss() + } + + open lazy var tapBackgroundGesture = UITapGestureRecognizer( + target: self, + action: #selector(tapBackground) + ) + + @objc + func tapBackground(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended else { + return + } + dismiss() + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct ModalImageView_Previews: PreviewProvider { + typealias ModalImageView = BrazeInAppMessageUI.ModalImageView + + static var previews: some View { + Group { + dimensionsPreviews + customPreviews + } + } + + @ViewBuilder + static var dimensionsPreviews: some View { + ModalImageView(message: .mock, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Square") + + ModalImageView(message: .mockLargeImage, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Large") + + ModalImageView(message: .mockExtraLargeImage, presented: true) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Dimension | Extra Large") + } + + static var customPreviews: some View { + var attributes = ModalImageView.Attributes() + attributes.scrollLargeImages = true + + return ModalImageView( + message: .mockExtraLargeImage, + attributes: attributes, + presented: true + ) + .preview(center: .required) + .frame(maxHeight: 800) + .previewDisplayName("Custom | Scroll Large Images") + } + + } +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift new file mode 100644 index 0000000000..525cab225c --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUIModalView.swift @@ -0,0 +1,568 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for modal in-app messages. + /// + /// The modal view can be customized using the ``attributes-swift.property`` property. + open class ModalView: UIView, InAppMessageView { + + /// The modal in-app message. + public var message: Braze.InAppMessage.Modal + + // MARK: - Attributes + + /// The attributes supported by the modal in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The minimum spacing around the content view. + public var margin = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + + /// The spacing around the content's view content. + public var padding = UIEdgeInsets(top: 40, left: 25, bottom: 30, right: 25) + + /// The spacing between the header and message + public var labelsSpacing = 10.0 + + /// The spacing between the graphic, the labels scroll view and the buttons. + public var spacing = 20.0 + + /// The font for the header. + public var headerFont = UIFont.preferredFont(textStyle: .title3, weight: .bold) + + /// The font for the message. + public var messageFont = UIFont.preferredFont(forTextStyle: .subheadline) + + /// The content view corner radius. + public var cornerRadius = 8.0 + + /// The content view shadow. + public var shadow: Shadow? = Shadow.default + + /// The minimum width. + public var minWidth = 320.0 + + /// The maximum width. + public var maxWidth = 450.0 + + /// The maximum height. + public var maxHeight = 720.0 + + /// Specify whether the modal in-app message view can be dismissed from a tap with the + /// view's background. + public var dismissOnBackgroundTap = false + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((ModalView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((ModalView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((ModalView) -> Void)? + + /// The defaults modal view attributes. + /// + /// Modify this value directly to apply the customizations to all modal in-app messages + /// presented by the SDK. + public static var defaults = Self() + } + + public var attributes: Attributes { + didSet { applyAttributes() } + } + + open func applyAttributes() { + // Margin + layoutMargins = attributes.margin + + // Padding + let padding = attributes.padding + let hasImage: Bool + if case .image = message.graphic { + hasImage = true + } else { + hasImage = false + } + + contentView.stack.layoutMargins = .init( + top: hasImage ? 0 : padding.top, + left: 0, + bottom: padding.bottom, + right: 0 + ) + textStack.layoutMargins = .init( + top: 0, + left: padding.left, + bottom: 0, + right: padding.right + ) + buttonsContainer?.stack.layoutMargins = .init( + top: 0, + left: padding.left, + bottom: 0, + right: padding.right + ) + + // Spacings + textStack.spacing = attributes.labelsSpacing + contentView.stack.spacing = attributes.spacing + + // Fonts + headerLabel.font = attributes.headerFont + messageLabel.font = attributes.messageFont + + // Corner radius + contentView.layer.cornerRadius = attributes.cornerRadius + contentView.layer.masksToBounds = true + shadowView.layer.cornerRadius = attributes.cornerRadius + + // Shadow + shadowView.shadow = attributes.shadow + + // Dimensions + minWidthConstraint.constant = attributes.minWidth + maxWidthConstraint.constant = attributes.maxWidth + maxHeightConstraint.constant = attributes.maxHeight + + // User interactions + tapBackgroundGesture.isEnabled = attributes.dismissOnBackgroundTap + + setNeedsLayout() + layoutIfNeeded() + } + + // MARK: - Views + + public let gifViewProvider: GIFViewProvider + + public lazy var graphicView: UIView? = { + switch message.graphic { + + case .icon(let id): + return IconView(symbol: id, theme: theme) + .boundedByIntrinsicContentSize() + + case .image(let url): + let imageView = gifViewProvider.view(url) + imageView.contentMode = .scaleAspectFit + return imageView + + default: + return nil + } + }() + + public lazy var headerLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true + label.attributedText = message.header.attributed { + $0.lineSpacing = 2 + $0.alignment = message.headerTextAlignment.nsTextAlignment(forTraits: traitCollection) + } + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentHuggingPriority(.required, for: .vertical) + return label + }() + + public lazy var messageLabel: UILabel = { + let label = UILabel() + label.numberOfLines = 0 + label.adjustsFontForContentSizeCategory = true + label.attributedText = message.message.attributed { + $0.lineSpacing = 4 + $0.alignment = message.messageTextAlignment.nsTextAlignment(forTraits: traitCollection) + } + label.setContentCompressionResistancePriority(.required, for: .vertical) + label.setContentHuggingPriority(.required, for: .vertical) + return label + }() + + public lazy var textStack: UIStackView = { + let stack = UIStackView(arrangedSubviews: [headerLabel, messageLabel]) + stack.axis = .vertical + stack.isLayoutMarginsRelativeArrangement = true + return stack + }() + + public lazy var textContainer: UIScrollView = { + let container = UIScrollView() + container.addSubview(textStack) + return container + }() + + public lazy var buttonsContainer: StackView? = { + let container = StackView( + buttons: message.buttons, + onClick: { [weak self] button in + guard let self = self else { return } + self.logClick(buttonId: "\(button.id)") + self.process(clickAction: button.clickAction, buttonId: "\(button.id)") + self.dismiss() + } + ) + container?.isHidden = message.buttons.isEmpty + container?.stack.spacing = 10 + container?.stack.isLayoutMarginsRelativeArrangement = true + return container + }() + + public lazy var contentView: StackView = { + let view = StackView( + arrangedSubviews: [ + graphicView, + textContainer, + buttonsContainer, + ] + .compactMap { $0 } + ) + view.stack.axis = .vertical + view.stack.distribution = .fill + view.stack.isLayoutMarginsRelativeArrangement = true + view.addSubview(closeButton) + return view + }() + + public lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setTitle("✕", for: .normal) + button.accessibilityLabel = localize( + "braze.in-app-message.close-button.title", + for: .inAppMessage + ) + button.titleLabel?.font = .preferredFont(forTextStyle: .title2) + button.addAction { [weak self] in self?.dismiss() } + return button + }() + + public lazy var shadowView = UIView() + + // MARK: - LifeCycle + + public init( + message: Braze.InAppMessage.Modal, + attributes: Attributes = .defaults, + gifViewProvider: GIFViewProvider = .default, + presented: Bool = false + ) { + self.message = message + self.attributes = attributes + self.gifViewProvider = gifViewProvider + self.presented = presented + + super.init(frame: .zero) + + addSubview(shadowView) + addSubview(contentView) + installInternalConstraints() + + addGestureRecognizer(tapBackgroundGesture) + contentView.addGestureRecognizer(tapGesture) + + applyTheme() + applyAttributes() + + alpha = presented ? 1 : 0 + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Theme + + public var theme: Braze.InAppMessage.Theme { message.theme(for: traitCollection) } + + open func applyTheme() { + headerLabel.textColor = theme.headerTextColor.uiColor + messageLabel.textColor = theme.textColor.uiColor + closeButton.setTitleColor(theme.closeButtonColor.uiColor, for: .normal) + contentView.backgroundColor = theme.backgroundColor.uiColor + backgroundColor = theme.frameColor.uiColor + + attributes.onTheme?(self) + } + + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + applyTheme() + } + + // MARK: - Layout + + open var presentationConstraintsInstalled = false + var imageConstraint: NSLayoutConstraint? + var minWidthConstraint: NSLayoutConstraint! + var maxWidthConstraint: NSLayoutConstraint! + var maxHeightConstraint: NSLayoutConstraint! + var contentPositionConstraints: [NSLayoutConstraint]! + + open override func layoutSubviews() { + super.layoutSubviews() + installPresentationConstraintsIfNeeded() + shadowView.updateShadow() + attributes.onLayout?(self) + } + + open func installInternalConstraints() { + // Dummy frame for first layout pass + frame = CGRect(x: 0, y: 0, width: 500, height: 500) + Constraints { + + // Graphic + switch (message.graphic, graphicView) { + case (.image(let url), .some(let imageView)): + if let imageSize = imageSize(url: url) { + let aspectRatio = imageSize.width / imageSize.height + imageConstraint = imageView.anchors.width.equal( + imageView.anchors.height.multiplied(by: aspectRatio) + ) + } + case (.icon, .some(let iconView)): + iconView.anchors.height.equal(50) + default: + break + } + + // Text + textStack.anchors.edges.pin() + textStack.anchors.width.equal(textContainer.anchors.width) + // - not required priority to allow the text container scrollview to shrink + textStack.anchors.height.lessThanOrEqual(textContainer.anchors.height).priority = + .defaultHigh + let textStackHeightConstraint = textStack.anchors.height.equal(textContainer.anchors.height) + textStackHeightConstraint.priority = .defaultHigh - 1 + + // Close button + closeButton.anchors.height.equal(closeButton.anchors.width) + closeButton.anchors.edges.pin(to: contentView.layoutMarginsGuide, alignment: .topTrailing) + + // Content view + // - dimensions + minWidthConstraint = contentView.anchors.width.greaterThanOrEqual(attributes.minWidth) + minWidthConstraint.priority = .defaultHigh + maxWidthConstraint = contentView.anchors.width.lessThanOrEqual(attributes.maxWidth) + maxHeightConstraint = contentView.anchors.height.lessThanOrEqual(attributes.maxHeight) + // - position + let bottom = contentView.anchors.bottom.lessThanOrEqual(layoutMarginsGuide.anchors.bottom) + bottom.priority = .defaultHigh + let centerY = contentView.anchors.centerY.align() + centerY.priority = .defaultHigh + contentPositionConstraints = [ + contentView.anchors.top.greaterThanOrEqual(layoutMarginsGuide.anchors.top), + contentView.anchors.leading.greaterThanOrEqual(layoutMarginsGuide.anchors.leading), + contentView.anchors.trailing.lessThanOrEqual(layoutMarginsGuide.anchors.trailing), + bottom, + contentView.anchors.centerX.align(), + centerY, + ] + + // Shadow view + shadowView.anchors.edges.pin(to: contentView) + } + } + + open func installPresentationConstraintsIfNeeded() { + guard let superview = superview, !presentationConstraintsInstalled else { return } + presentationConstraintsInstalled = true + anchors.edges.pin() + setNeedsLayout() + superview.layoutIfNeeded() + } + + // MARK: - Presentation / InAppMessageView conformance + + public var presented: Bool = false { + didSet { alpha = presented ? 1 : 0 } + } + + public func present(completion: (() -> Void)?) { + installPresentationConstraintsIfNeeded() + + willPresent() + attributes.onPresent?(self) + + UIView.performWithoutAnimation { + superview?.layoutIfNeeded() + } + + makeKey() + + UIView.animate( + withDuration: message.animateIn ? 0.25 : 0, + animations: { self.presented = true }, + completion: { _ in + self.logImpression() + completion?() + self.didPresent() + } + ) + } + + @objc + public func dismiss(completion: (() -> Void)? = nil) { + isUserInteractionEnabled = false + willDismiss() + UIView.animate( + withDuration: message.animateOut ? 0.25 : 0, + animations: { self.presented = false }, + completion: { _ in + completion?() + self.didDismiss() + } + ) + } + + // MARK: - User Interactions + + open lazy var tapGesture = UITapGestureRecognizer( + target: self, + action: #selector(tap) + ) + + @objc + func tap(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended else { + return + } + logClick() + process(clickAction: message.clickAction) + dismiss() + } + + open lazy var tapBackgroundGesture = UITapGestureRecognizer( + target: self, + action: #selector(tapBackground) + ) + + @objc + func tapBackground(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended else { + return + } + dismiss() + } + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct ModalView_Previews: PreviewProvider { + typealias ModalView = BrazeInAppMessageUI.ModalView + + static var previews: some View { + Group { + variationPreviews + // dimensionPreviews + // rightToLeftPreviews + // themePreviews + } + } + + @ViewBuilder + static var variationPreviews: some View { + ModalView(message: .mock, presented: true) + .preview(center: .required) + .frame(maxHeight: 300) + .previewDisplayName("Var. | Default") + + ModalView(message: .mockOneButton, presented: true) + .preview(center: .required) + .frame(maxHeight: 300) + .previewDisplayName("Var. | 1 Button") + + ModalView(message: .mockTwoButtons, presented: true) + .preview(center: .required) + .frame(maxHeight: 300) + .previewDisplayName("Var. | 2 Buttons") + + ModalView(message: .mockIcon, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .previewDisplayName("Var. | Icon") + + ModalView(message: .mockImage, presented: true) + .preview(center: .required) + .frame(maxHeight: 400) + .previewDisplayName("Var. | Image") + + ModalView(message: .mockImageWrongAspectRatio, presented: true) + .preview(center: .required) + .frame(maxHeight: 400) + .previewDisplayName("Var. | Image (wrong aspect ratio)") + + ModalView(message: .mockLeadingAligned, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .previewDisplayName("Var. | Leading alignment") + + ModalView(message: .mockLong, presented: true) + .preview(center: .required) + .frame(maxHeight: 375) + .previewDisplayName("Var. | Long (constrained)") + } + + @ViewBuilder + static var dimensionPreviews: some View { + ModalView(message: .mockIcon, presented: true) + .preview(center: .required) + .frame(width: 540, height: 430) + .previewDisplayName("Dimension | Small") + + ModalView(message: .mockLong, presented: true) + .preview(center: .required) + .frame(width: 540, height: 800) + .previewDisplayName("Dimensions | Large") + } + + // OpenRadar: https://archive.md/zr3l4 + // swift-snapshot-testing issue: https://archive.md/dnUQM + @ViewBuilder + static var rightToLeftPreviews: some View { + ModalView(message: .mockIcon, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .environment(\.layoutDirection, .rightToLeft) + .previewDisplayName("RTL Support | Default") + } + + @ViewBuilder + static var themePreviews: some View { + ModalView(message: .mockIcon, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .preferredColorScheme(.light) + .previewDisplayName("Theme | Light") + + ModalView(message: .mockIcon, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .preferredColorScheme(.dark) + .previewDisplayName("Theme | Dark") + + ModalView(message: .mockThemed, presented: true) + .preview(center: .required) + .frame(maxHeight: 370) + .previewDisplayName("Theme | Custom") + } + + } + +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift new file mode 100644 index 0000000000..4d1f53f5ca --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageUISlideupView.swift @@ -0,0 +1,610 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// The view for slideup in-app messages. + /// + /// The slideup view can be customized using the ``attributes-swift.property`` property. + open class SlideupView: UIView, InAppMessageView { + + /// The slideup in-app message. + public let message: Braze.InAppMessage.Slideup + + // MARK: - Attributes + + /// The attributes supported by the slideup in-app message view. + /// + /// Attributes can be updated in multiple ways: + /// - Via modifying the ``defaults`` static property + /// - Via implementing the ``BrazeInAppMessageUIDelegate/inAppMessage(_:prepareWith:)-11fog`` + /// delegate. + /// - Via modifying the ``BrazeInAppMessageUI/messageView`` attributes on the + /// `BrazeInAppMessageUI` instance. + public struct Attributes { + + /// The minimum spacing around the content view. + public var margin = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + + /// The spacing around the content's view content. + public var padding = UIEdgeInsets(top: 15, left: 20, bottom: 15, right: 20) + + /// The leading padding when the view is displaying a graphic (icon or image) + public var graphicLeadingPadding = 15.0 + + /// The spacing between the graphic view, the body and the chevron. + public var spacing = 10.0 + + /// The font for the body. + public var font = UIFont.preferredFont(textStyle: .subheadline, weight: .bold) + + /// The image size. + public var imageSize = CGSize(width: 50, height: 50) + + /// The content view corner radius. + public var cornerRadius = 15.0 + + /// The content view shadow. + public var shadow: Shadow? = .default + + /// The minimum height. + public var minHeight = 60.0 + + /// The maximum width. + public var maxWidth = 450.0 + + /// Closure allowing further customization, executed when the view is about to be presented. + public var onPresent: ((SlideupView) -> Void)? + + /// Closure executed every time the view is laid out. + public var onLayout: ((SlideupView) -> Void)? + + /// Closure executed every time the view update its theme. + public var onTheme: ((SlideupView) -> Void)? + + /// The defaults slideup view attributes. + /// + /// Modify this value directly to apply the customizations to all slideup in-app messages + /// presented by the SDK. + public static var defaults = Self() + + } + + /// The view attributes. See ``Attributes-swift.struct``. + public var attributes: Attributes { + didSet { applyAttributes() } + } + + open func applyAttributes() { + // Margin + layoutMargins = attributes.margin + + // Padding + contentView.stack.layoutMargins = UIEdgeInsets( + top: attributes.padding.top, + left: message.graphic != nil + ? attributes.graphicLeadingPadding + : attributes.padding.left, + bottom: attributes.padding.bottom, + right: attributes.padding.right + ) + + // Spacing + contentView.stack.spacing = attributes.spacing + + // Fonts + messageLabel.font = attributes.font + + // Corner radius + contentView.layer.cornerRadius = attributes.cornerRadius + + // Shadow + contentView.shadow = attributes.shadow + + // Dimensions + maxWidthConstraints.forEach { $0.constant = attributes.maxWidth } + minHeightConstraint.constant = attributes.minHeight + + setNeedsLayout() + layoutIfNeeded() + } + + // MARK: - Views + + let gifViewProvider: GIFViewProvider + + var highlighted: Bool = false { + didSet { + let alpha = highlighted ? 0.7 : 1 + let duration = highlighted ? 0.15 : 0.3 + UIView.animate(withDuration: duration) { + self.messageLabel.alpha = alpha + self.chevronView.alpha = alpha + } + } + } + + open lazy var graphicView: UIView? = { + switch message.graphic { + case .icon(let id): + return IconView(symbol: id, theme: theme) + case .image(let url): + let imageView = self.gifViewProvider.view(url) + imageView.contentMode = .scaleAspectFit + return imageView + default: + return nil + } + }() + + open lazy var messageLabel: UILabel = { + let label = UILabel() + label.font = attributes.font + label.numberOfLines = 3 + label.adjustsFontForContentSizeCategory = true + label.attributedText = message.message.attributed { + $0.lineSpacing = 2 + $0.lineBreakMode = .byTruncatingTail + } + label.setContentCompressionResistancePriority(.required, for: .horizontal) + return label + }() + + open lazy var chevronView: UIImageView = { + let image = UIImage( + named: "InAppMessage/chevron", + in: Bundle.module, + compatibleWith: traitCollection + )? + .withRenderingMode(.alwaysTemplate) + .imageFlippedForRightToLeftLayoutDirection() + let view = UIImageView(image: image) + view.isHidden = message.clickAction == .none + return view + }() + + open lazy var contentView: StackView = { + let view = StackView( + arrangedSubviews: [ + graphicView, + messageLabel, + chevronView, + ] + .compactMap { $0 } + ) + view.stack.alignment = .center + view.stack.distribution = .fill + view.stack.isLayoutMarginsRelativeArrangement = true + return view + }() + + // MARK: - LifeCycle + + public init( + message: Braze.InAppMessage.Slideup, + attributes: Attributes = .defaults, + gifViewProvider: GIFViewProvider = .default, + presented: Bool = false + ) { + self.message = message + self.attributes = attributes + self.gifViewProvider = gifViewProvider + self.presented = presented + + super.init(frame: .zero) + + addSubview(contentView) + installInternalConstraints() + + contentView.addGestureRecognizer(panGesture) + contentView.addGestureRecognizer(pressGesture) + + applyTheme() + applyAttributes() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Theme + + public var theme: Braze.InAppMessage.Theme { message.theme(for: traitCollection) } + + open func applyTheme() { + messageLabel.textColor = theme.textColor.uiColor + chevronView.tintColor = theme.closeButtonColor.uiColor + contentView.backgroundColor = theme.backgroundColor.uiColor + + attributes.onTheme?(self) + } + + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + applyTheme() + } + + // MARK: - Layout + + open var presentationConstraintsInstalled = false + open var innerYConstraint: NSLayoutConstraint! + open var outerYConstraint: NSLayoutConstraint! + var maxWidthConstraints: [NSLayoutConstraint]! + var minHeightConstraint: NSLayoutConstraint! + var imageWidthConstraint: NSLayoutConstraint? + var imageHeightConstraint: NSLayoutConstraint? + + open override func layoutSubviews() { + super.layoutSubviews() + installPresentationConstraintsIfNeeded() + contentView.updateShadow() + attributes.onLayout?(self) + } + + open func installInternalConstraints() { + // Dummy frame for first layout pass + frame = CGRect(x: 0, y: 0, width: 500, height: 500) + Constraints { + // Graphic + switch graphicView { + case let imageView as UIImageView: + imageWidthConstraint = imageView.anchors.width.equal(attributes.imageSize.width) + imageHeightConstraint = imageView.anchors.height.equal(attributes.imageSize.height) + default: + break + } + + // Chevron + chevronView.anchors.size.equal(CGSize(width: 12, height: 20)) + + // Content view + // - dimensions + let lessWidthConstraint = contentView.anchors.width.lessThanOrEqual(attributes.maxWidth) + let equalWidthConstraint = contentView.anchors.width.equal(attributes.maxWidth) + equalWidthConstraint.priority = .required - 1 + maxWidthConstraints = [lessWidthConstraint, equalWidthConstraint] + minHeightConstraint = contentView.anchors.height.greaterThanOrEqual(attributes.minHeight) + // - position + contentView.anchors.centerX.align() + contentView.anchors.edges.lessThanOrEqual(layoutMarginsGuide) + } + } + + open func installPresentationConstraintsIfNeeded() { + guard let superview = superview, !presentationConstraintsInstalled else { return } + presentationConstraintsInstalled = true + + Constraints { + anchors.edges.pin(axis: .horizontal) + + switch message.slideFrom { + case .top: + innerYConstraint = anchors.top.pin() + outerYConstraint = contentView.anchors.top.pin(to: layoutMarginsGuide) + anchors.bottom.equal(superview.anchors.top).priority = .defaultLow + case .bottom: + innerYConstraint = anchors.bottom.pin() + outerYConstraint = contentView.anchors.bottom.pin(to: layoutMarginsGuide) + anchors.top.equal(superview.anchors.bottom).priority = .defaultLow + @unknown default: + // Same as .bottom + innerYConstraint = anchors.bottom.pin() + outerYConstraint = contentView.anchors.bottom.pin(to: layoutMarginsGuide) + anchors.top.equal(superview.anchors.bottom).priority = .defaultLow + } + } + + innerYConstraint.isActive = presented + outerYConstraint.isActive = presented + + setNeedsLayout() + superview.layoutIfNeeded() + } + + // MARK: - Presentation / InAppMessageView conformance + + open var presented: Bool = false { + didSet { + presented + ? NSLayoutConstraint.activate([innerYConstraint, outerYConstraint]) + : NSLayoutConstraint.deactivate([innerYConstraint, outerYConstraint]) + } + } + + open func present(completion: (() -> Void)? = nil) { + installPresentationConstraintsIfNeeded() + + willPresent() + attributes.onPresent?(self) + + UIView.performWithoutAnimation { + superview?.layoutIfNeeded() + } + + presented = true + UIView.animate( + withDuration: message.animateIn ? 0.3 : 0, + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 1, + options: .beginFromCurrentState, + animations: { self.superview?.layoutIfNeeded() }, + completion: { _ in + self.logImpression() + completion?() + self.didPresent() + } + ) + } + + open func dismiss(completion: (() -> Void)? = nil) { + willDismiss() + presented = false + UIView.animate( + withDuration: message.animateOut ? 0.3 : 0, + delay: 0, + usingSpringWithDamping: 1, + initialSpringVelocity: 1, + options: .beginFromCurrentState, + animations: { self.superview?.layoutIfNeeded() }, + completion: { _ in + completion?() + self.didDismiss() + } + ) + } + + // MARK: - User Interactions + + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + contentView.point(inside: convert(point, to: contentView), with: event) + } + + class PressGestureDelegate: NSObject, UIGestureRecognizerDelegate { + func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { true } + } + + private let pressGestureDelegate = PressGestureDelegate() + + open lazy var panGesture: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer(target: self, action: #selector(pan(_:))) + return pan + }() + + open lazy var pressGesture: UILongPressGestureRecognizer = { + let press = UILongPressGestureRecognizer(target: self, action: #selector(press(_:))) + press.minimumPressDuration = 0 + press.delegate = pressGestureDelegate + return press + }() + + @objc + func pan(_ gesture: UIPanGestureRecognizer) { + guard let superview = gesture.view?.superview else { return } + var dy = gesture.translation(in: superview).y + + switch gesture.state { + case .changed: + if abs(dy) >= 5 { pressGesture.isEnabled = false } + switch message.slideFrom { + case .top where dy > 0: + dy = dy * 0.095 + outerYConstraint.constant = dy + innerYConstraint.constant = 0 + case .top where dy <= 0: + outerYConstraint.constant = 0 + innerYConstraint.constant = dy + case .bottom where dy < 0: + dy = dy * 0.095 + outerYConstraint.constant = dy + innerYConstraint.constant = 0 + case .bottom where dy >= 0: + outerYConstraint.constant = 0 + innerYConstraint.constant = dy + default: + break + } + case .ended: + pressGesture.isEnabled = true + let vy = gesture.velocity(in: superview).y + + switch message.slideFrom { + case .top where vy <= -60 || dy < -25: + dismiss() + case .bottom where vy >= 60 || dy > 25: + dismiss() + default: + outerYConstraint.constant = 0 + innerYConstraint.constant = 0 + UIView.animate(withDuration: 0.3) { + self.superview?.layoutIfNeeded() + } + } + + default: + break + } + } + + @objc + func press(_ gesture: UILongPressGestureRecognizer) { + guard gesture.view != nil else { return } + + switch gesture.state { + case .began, .changed: + highlighted = true + case .ended: + highlighted = false + logClick() + process(clickAction: message.clickAction) + dismiss() + default: + highlighted = false + } + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct SlideupView_Previews: PreviewProvider { + typealias SlideupView = BrazeInAppMessageUI.SlideupView + + public static var previews: some View { + Group { + variationPreviews + // dimensionPreviews + // positionPreviews + // rightToLeftPreviews + // themePreviews + // customPreviews + } + } + + @ViewBuilder + static var variationPreviews: some View { + SlideupView(message: .mock) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Text") + + SlideupView(message: .mockChevron) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Chevron") + + SlideupView(message: .mockIcon) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Icon") + + SlideupView(message: .mockImage) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Image") + + SlideupView(message: .mockLong) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Var. | Long") + + } + + @ViewBuilder + static var dimensionPreviews: some View { + SlideupView(message: .mockIcon) + .preview(center: .required) + .frame(width: 375, height: 120) + .previewDisplayName("Dimension | Small") + + SlideupView(message: .mockLong) + .preview(center: .required) + .frame(width: 650, height: 120) + .previewDisplayName("Dimensions | Large") + } + + @ViewBuilder + static var positionPreviews: some View { + SlideupView(message: .mockTop, presented: true) + .preview() + .frame(maxHeight: 150) + .previewDisplayName("Position | Top") + + SlideupView(message: .mockIcon, presented: true) + .preview() + .frame(maxHeight: 150) + .previewDisplayName("Position | Bottom") + } + + // OpenRadar: https://archive.md/zr3l4 + // swift-snapshot-testing issue: https://archive.md/dnUQM + @ViewBuilder + static var rightToLeftPreviews: some View { + SlideupView(message: .mockIcon) + .preview(center: .required) + .frame(maxHeight: 120) + .environment(\.layoutDirection, .rightToLeft) + .previewDisplayName("RTL Support | Default") + } + + @ViewBuilder + static var themePreviews: some View { + SlideupView(message: .mockIcon) + .preview(center: .required) + .frame(maxHeight: 120) + .preferredColorScheme(.light) + .previewDisplayName("Theme | Light") + + SlideupView(message: .mockIcon) + .preview(center: .required) + .frame(maxHeight: 120) + .preferredColorScheme(.dark) + .previewDisplayName("Theme | Dark") + + SlideupView(message: .mockThemed) + .preview(center: .required) + .frame(maxHeight: 120) + .previewDisplayName("Theme | Custom") + } + + static let extendedEdgesAttributes: SlideupView.Attributes = { + var attributes = SlideupView.Attributes() + attributes.maxWidth = 10000 + attributes.padding = .zero + attributes.padding.left = 15 + attributes.padding.right = 15 + attributes.cornerRadius = 0 + attributes.onPresent = { + let backgroundView = UIView() + $0.addSubview(backgroundView) + switch $0.message.slideFrom { + case .top: + backgroundView.anchors.bottom.equal($0.anchors.top) + case .bottom: + backgroundView.anchors.top.equal($0.anchors.bottom) + } + backgroundView.anchors.edges.pin(axis: .horizontal) + backgroundView.anchors.height.equal(1000) + backgroundView.backgroundColor = $0.contentView.backgroundColor + $0.shadow = $0.contentView.shadow + $0.contentView.shadow = nil + } + attributes.onLayout = { + $0.updateShadow() + } + attributes.onTheme = { + $0.backgroundColor = $0.contentView.backgroundColor + $0.subviews.last?.backgroundColor = $0.contentView.backgroundColor + } + return attributes + }() + + @ViewBuilder + static var customPreviews: some View { + SlideupView( + message: .mockIcon, + attributes: extendedEdgesAttributes, + presented: true + ) + .preview { + ($0 as! SlideupView).attributes.onPresent?(($0 as! SlideupView)) + } + .frame(maxHeight: 120) + .previewDisplayName(#"Custom | Extended edges"#) + } + + } + +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift new file mode 100644 index 0000000000..6653a08cdf --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageView.swift @@ -0,0 +1,180 @@ +import BrazeKit +import UIKit + +/// The requirements for a view displayed by ``BrazeInAppMessageUI``. +/// +/// BrazeUI ships with the following in-app message views: +/// - ``BrazeInAppMessageUI/SlideupView`` +/// - ``BrazeInAppMessageUI/ModalView`` +/// - ``BrazeInAppMessageUI/ModalImageView`` +/// - ``BrazeInAppMessageUI/FullView`` +/// - ``BrazeInAppMessageUI/FullImageView`` +/// - ``BrazeInAppMessageUI/HtmlView`` +/// - ``BrazeInAppMessageUI/ControlView`` +/// +/// Custom in-app message views must conform to this protocol. +public protocol InAppMessageView: UIView { + + /// The current presented state — is the message view visible to the user. + var presented: Bool { get } + + /// Presents the message view to the user. + /// + /// When this method is called, the message view is already added to the view hierarchy. + /// + /// As part of its presentation, the in-app message view must report lifecycle events using: + /// - ``willPresent()``: before any presentation animation occurs. + /// - ``didPresent()``: after all presentation animations are completed and the message view is + /// fully visible to the user. + /// + /// Additionally, the in-app message must also report analytics and click actions using: + /// - ``logImpression()``: as soon as the message is fully visible to the user. + /// - ``logClick(buttonId:):``: as the result of a user click. + /// + /// - Parameters: + /// - completion: The completion block executed once the message view is fully visible to the + /// user. + func present(completion: (() -> Void)?) + + /// Dimisses the message view. + /// + /// As part of its dismissal, the in-app message view must reports lifecycle events using the + /// following methods: + /// - ``willDismiss()``: before any dismissal animation occurs. + /// - ``didDismiss()``: after all dismissal animations are completed and the message view is fully + /// hidden from the user. + /// + /// - Parameter completion: The completion block executed once the message view is fully hidden + /// from the user. + func dismiss(completion: (() -> Void)?) + +} + +extension InAppMessageView { + + /// The preferred status bar hidden state. + /// + /// Setting this value may have no effect depending of upstream customizations. + public var prefersStatusBarHidden: Bool? { + get { controller?.messageViewPrefersStatusBarHidden } + set { controller?.messageViewPrefersStatusBarHidden = newValue } + } + + /// Call this method to report the "will present" event before any presentation animation occurs. + public func willPresent() { + guard let controller = controller, let ui = controller.ui else { + return + } + + ui.delegate?.inAppMessage(ui, willPresent: controller.message, view: self) + } + + /// Call this method to report the "did present" event after all presentation animations are + /// completed and the message view is fully visible to the user. + public func didPresent() { + guard let controller = controller, let ui = controller.ui else { + return + } + + ui.delegate?.inAppMessage(ui, didPresent: controller.message, view: self) + } + + /// Call this method to report the "will dismiss" event before any dismissal animation occurs. + public func willDismiss() { + guard let controller = controller, let ui = controller.ui else { + return + } + + ui.delegate?.inAppMessage(ui, willDismiss: controller.message, view: self) + } + + /// Call this method to report the "did dismiss" event after all dismissal animations are + /// completed and the message view is fully hidden from the user. + public func didDismiss() { + guard let controller = controller, let ui = controller.ui else { + return + } + + ui.dismissTimer?.invalidate() + ui.dismissTimer = nil + + removeFromSuperview() + + if #available(iOS 13.0, *) { + ui.window?.windowScene = nil + } + ui.window = nil + + Braze.UIUtils.activeRootViewController?.setNeedsStatusBarAppearanceUpdate() + + ui.delegate?.inAppMessage(ui, didDismiss: controller.message, view: self) + } + + /// Call this method to report the in-app message impression. + public func logImpression() { + guard let context = controller?.message.context else { + logError(.noContextLogImpression) + return + } + context.logImpression() + } + + /// Call this method to report the in-app message click. + /// - Parameter buttonId: An optional button identifier. + public func logClick(buttonId: String? = nil) { + guard let context = controller?.message.context else { + logError(.noContextLogClick) + return + } + context.logClick(buttonId: buttonId) + } + + /// Call this method to process the in-app message click action. + /// - Parameters: + /// - clickAction: The click action to process. + /// - buttonId: An optional button identifier. + public func process(clickAction: Braze.InAppMessage.ClickAction, buttonId: String? = nil) { + guard let ui = controller?.ui, let message = controller?.message else { + return + } + + let process = + ui.delegate?.inAppMessage( + ui, + shouldProcess: clickAction, + buttonId: buttonId, + message: message, + view: self + ) ?? true + + guard process else { return } + guard let context = message.context else { + logError(.noContextProcessClickAction) + return + } + + context.processClickAction(clickAction) + } + + public func logError(_ error: BrazeInAppMessageUI.Error) { + controller?.message.context?.logError(flattened: error.logDescription) + ?? print(error.logDescription) + } + + /// The controller currently displaying the in-app message view. + public var controller: BrazeInAppMessageUI.ViewController? { + responders + .lazy + .compactMap { $0 as? BrazeInAppMessageUI.ViewController } + .first + } + + /// Makes the in-app message view window become the first responder. + /// + /// Use this methods to properly dismiss the keyboard if needed. When the message view is + /// dismissed, the keyboard original state is restored automatically by UIKit. + public func makeKey() { + window?.makeKey() + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageViewWebKitExt.swift b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageViewWebKitExt.swift new file mode 100644 index 0000000000..b151f106cc --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/InAppMessageViewWebKitExt.swift @@ -0,0 +1,42 @@ +import BrazeKit +import WebKit + +extension InAppMessageView { + + /// Creates and returns a script message handler implementing the Braze JavaScript bridge api. + public func webViewScriptMessageHandler() -> Braze.WebViewBridge.ScriptMessageHandler { + let closeMessage: () -> Void = { [weak self] in self?.dismiss(completion: nil) } + let braze = controller?.message.context?.braze as? Braze + + return .init( + logClick: { [weak self] in self?.logClick(buttonId: $0) }, + logError: { [weak self] in self?.logError(.webViewScript($0)) }, + showNewsFeed: { [weak self] in self?.process(clickAction: .newsFeed, buttonId: nil) }, + closeMessage: closeMessage, + braze: braze + ) + } + + /// Creates and returns a custom scheme handler implementing the logic for scheme-based actions. + public func webViewSchemeHandler() -> Braze.WebViewBridge.SchemeHandler { + let closeMessage: () -> Void = { [weak self] in self?.dismiss(completion: nil) } + let braze = controller?.message.context?.braze as? Braze + + return .init( + logError: { [weak self] in self?.logError(.webViewScheme($0)) }, + showNewsFeed: { [weak self] in self?.process(clickAction: .newsFeed, buttonId: nil) }, + closeMessage: closeMessage, + queryHandler: webViewQueryHandler(), + braze: braze + ) + } + + /// Creates and returns an url query handler implementing the logic for query-based actions. + public func webViewQueryHandler() -> Braze.WebViewBridge.QueryHandler { + .init( + logClick: { [weak self] in self?.logClick(buttonId: $0) }, + logError: { [weak self] in self?.logError(.webViewQuery($0)) } + ) + } + +} diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift new file mode 100644 index 0000000000..038246202d --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/ButtonView.swift @@ -0,0 +1,168 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// View displaying a Braze in-app message compatible button. + open class ButtonView: UIButton { + + /// The button definition. + public var button: Braze.InAppMessage.Button + + /// Creates and returns a Braze in-app message compatible button. + /// - Parameters: + /// - button: A button definition as provided by BrazeKit. + /// - attributes: A high-level customization struct. + public init( + button: Braze.InAppMessage.Button, + attributes: Attributes = .init() + ) { + self.button = button + self.attributes = attributes + + super.init(frame: .zero) + + setTitle(button.text, for: .normal) + titleLabel?.adjustsFontForContentSizeCategory = true + titleLabel?.adjustsFontSizeToFitWidth = true + layer.masksToBounds = true + + setContentCompressionResistancePriority(.required, for: .vertical) + + minWidthConstraint = anchors.width.greaterThanOrEqual(0) + maxHeightContraint = anchors.height.lessThanOrEqual(0) + + applyTheme() + applyAttributes() + } + + @available(*, unavailable) + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Attributes + + /// Attributes allow a high-level customization of Braze's UI component. + public struct Attributes { + + /// Space around the button label, inside the button border. + /// + /// Default: `10pt.` vertical, `12pt.` horizontal. + public var padding: UIEdgeInsets = .init(top: 10, left: 12, bottom: 10, right: 12) + + /// Font used for the button label. + /// + /// Default: `subheadline` dynamic type, `bold` weight. + public var font: UIFont = .preferredFont(textStyle: .subheadline, weight: .bold) + + /// Border width. + /// + /// Default: `1pt.` + /// Set it to `0` to remove the border. + public var borderWidth: Double = 1 + + /// Corner radius. + /// + /// Default: `5pt.` + public var cornerRadius: Double = 5 + + /// Minimum width. + /// + /// Default: `80pt.` + public var minWidth: Double = 80 + + /// Maximum height. + /// + /// Default: `44pt.` + public var maxHeight: Double = 44 + + /// Default initializer + public init() {} + } + + /// The high-level customization struct. + /// + /// See ``Attributes-swift.struct`` for customizable values. + public var attributes: Attributes { + didSet { applyAttributes() } + } + + /// Apply the current ``attributes-swift.property`` to the view. + /// + /// This is called automatically whenever ``attributes-swift.property`` is updated. + open func applyAttributes() { + contentEdgeInsets = attributes.padding + titleLabel?.font = attributes.font + layer.borderWidth = attributes.borderWidth + layer.cornerRadius = attributes.cornerRadius + + minWidthConstraint.constant = attributes.minWidth + maxHeightContraint.constant = attributes.maxHeight + + invalidateIntrinsicContentSize() + } + + // MARK: - Layout + + var minWidthConstraint: NSLayoutConstraint! + var maxHeightContraint: NSLayoutConstraint! + + open override var intrinsicContentSize: CGSize { + CGSize( + width: max(super.intrinsicContentSize.width, attributes.minWidth), + height: min(super.intrinsicContentSize.height, attributes.maxHeight) + ) + } + + // MARK: - Theme + + /// The current theme. + public var theme: Braze.InAppMessage.ButtonTheme { button.theme(for: traitCollection) } + + /// Apply the current ``theme`` to the view. + /// + /// This is called automatically whenever the trait collection is updated. + open func applyTheme() { + setTitleColor(theme.textColor.uiColor, for: .normal) + setBackgroundImage(theme.backgroundColor.image, for: .normal) + setBackgroundImage( + theme.backgroundColor.adjustingBrightness(by: -0.08).image, + for: .highlighted + ) + layer.borderColor = theme.borderColor.uiColor.cgColor + } + + open override func traitCollectionDidChange( + _ previousTraitCollection: UITraitCollection? + ) { + super.traitCollectionDidChange(previousTraitCollection) + applyTheme() + } + + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct Button_Previews: PreviewProvider { + typealias ButtonView = BrazeInAppMessageUI.ButtonView + + static var previews: some View { + ButtonView(button: .mockPrimary) + .preview() + .fixedSize() + .preferredColorScheme(.light) + ButtonView(button: .mockSecondary) + .preview() + .fixedSize() + .preferredColorScheme(.dark) + } + + } +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift new file mode 100644 index 0000000000..9347b5a962 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/IconView.swift @@ -0,0 +1,191 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI { + + /// View displaying a [FontAwesome v4.3] icon. + /// + /// [FontAwesome v4.3]: https://fontawesome.com/v4.7/cheatsheet/ + open class IconView: UIView { + + /// FontAwesome symbol to display. + /// + /// - Important: The value should be the actual unicode symbol instead of the FontAwesome symbol + /// identifier. For instance, use `` instead of `fa-arrow-right`. + /// + /// You can copy FontAwesome 4.3 compatible symbols directly from the FontAwesome [cheatsheet]. + /// + /// [cheatsheet]: https://fontawesome.com/v4.7/cheatsheet/ + public var symbol: String { + didSet { label.text = symbol } + } + + /// Label displaying the FontAwesome symbol. + public var label = UILabel() + + /// Creates and returns an icon view. + /// - Parameters: + /// - symbol: A FontAwesome unicode symbol, see ``symbol`` for more details. + /// - attributes: A high-level customization struct. + /// - theme: An in-app message theme. + public init( + symbol: String, + attributes: Attributes = .init(), + theme: Braze.InAppMessage.Theme = .defaultLight + ) { + self.symbol = symbol + self.attributes = attributes + self.theme = theme + + super.init(frame: .zero) + addSubview(label) + + label.text = symbol + label.textAlignment = .center + + applyTheme() + applyAttributes() + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Attributes + + /// Attributes allow high-level customization of Braze's UI component. + public struct Attributes { + + /// Intrinsic size of the icon, including the background. + /// + /// Default: `50x50pt.` + public var size: CGSize = CGSize(width: 50, height: 50) + + /// Size of the symbol, excluding the background. + /// + /// Default: `30pt.` + public var symbolSize = 30.0 + + /// Corner radius of the icon's background + /// + /// Default: `10pt.` + public var cornerRadius = 10.0 + + /// Default initializer. + public init() {} + } + + /// The high-level customization struct. + /// + /// See ``Attributes-swift.struct`` for customizable values. + public var attributes: Attributes { + didSet { applyAttributes() } + } + + /// Apply the current ``attributes-swift.property`` to the view. + /// + /// This is called automatically whenever ``attributes-swift.property`` is updated. + open func applyAttributes() { + NSLayoutConstraint.deactivate(constraints) + anchors.size.equal(attributes.size) + + label.font = Self.fontAwesome.withSize(attributes.symbolSize) + + layer.cornerRadius = attributes.cornerRadius + layer.masksToBounds = true + } + + // MARK: - Layout + + open override var intrinsicContentSize: CGSize { + attributes.size + } + + open override func layoutSubviews() { + super.layoutSubviews() + label.frame = bounds + } + + // MARK: - Theme + + /// The current theme. + public var theme: Braze.InAppMessage.Theme { + didSet { applyTheme() } + } + + /// Apply the current ``theme`` to the view. + /// + /// This is called automatically whenever ``theme`` is updated. + open func applyTheme() { + label.textColor = theme.iconColor.uiColor + label.backgroundColor = theme.iconBackgroundColor.uiColor + } + + // MARK: - FontAwesome + + static let fontAwesome: UIFont = { + _ = registerFontAwesomeIfNeeded() + return UIFont(name: "FontAwesome", size: 30) ?? .systemFont(ofSize: 30) + }() + + static func registerFontAwesomeIfNeeded() -> Bool { + guard UIFont(name: "FontAwesome", size: 30) == nil else { + return true + } + guard let url = Bundle.module.url(forResource: "FontAwesome", withExtension: "otf"), + let data = try? Data(contentsOf: url), + let dataProvider = CGDataProvider(data: data as CFData), + let font = CGFont(dataProvider) + else { + return false + } + var error: Unmanaged? = nil + guard CTFontManagerRegisterGraphicsFont(font, &error) else { + return false + } + return true + } + } + +} + +// MARK: - Previews + +#if UI_PREVIEWS + import SwiftUI + + @available(iOS 13.0, *) + struct IconView_Previews: PreviewProvider { + typealias IconView = BrazeInAppMessageUI.IconView + + static var previews: some View { + IconView( + symbol: "", + theme: .defaultLight + ).preview() + .frame(width: 50, height: 50) + .preferredColorScheme(.light) + .previewDisplayName("Light") + IconView( + symbol: "", + theme: .defaultDark + ).preview() + .frame(width: 50, height: 50) + .preferredColorScheme(.dark) + .previewDisplayName("Dark") + IconView( + symbol: "", + theme: .init( + iconColor: 0xFF4C_D137, + iconBackgroundColor: 0xFF2D_3436 + ) + ).preview() + .frame(width: 50, height: 50) + .previewDisplayName("Custom Theme") + IconView(symbol: "", theme: .defaultLight).preview() + .frame(width: 50, height: 50) + .previewDisplayName("Invalid") + } + + } +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift new file mode 100644 index 0000000000..7414e6e7ba --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView+ButtonViews.swift @@ -0,0 +1,69 @@ +import BrazeKit +import UIKit + +extension BrazeInAppMessageUI.StackView { + + /// Creates and returns an in-app message buttons container stack view. + /// + /// Buttons in the stack view are laid out as follows: + /// - One button: The intrinsic size of the button is respected. + /// - Two or more buttons: Buttons are laid out horizontally and occupy all the available width. + /// + /// - Parameters: + /// - buttons: An array of in-app message buttons. + /// - context: An optional context, used to log and process button clicks. + public convenience init?( + buttons: [Braze.InAppMessage.Button], + onClick: @escaping (Braze.InAppMessage.Button) -> Void + ) { + if buttons.isEmpty { return nil } + + let subviews: [UIView] = + buttons + .map { data in + let button = BrazeInAppMessageUI.ButtonView(button: data) + button.addAction { onClick(data) } + return button + } + + self.init( + arrangedSubviews: subviews.count == 1 + ? subviews.map { $0.boundedByIntrinsicContentSize(centerY: false) } + : subviews + ) + + stack.distribution = .fillEqually + stack.alignment = .center + stack.axis = .horizontal + stack.spacing = 10 + } + +} + +#if UI_PREVIEWS + import SwiftUI + + struct StackViewButtons_Previews: PreviewProvider { + typealias StackView = BrazeInAppMessageUI.StackView + + static var previews: some View { + StackView( + buttons: [.mockPrimary], + onClick: { _ in } + )! + .preview() + .frame(maxHeight: 70) + .previewDisplayName("Var. | 1 Button") + + StackView( + buttons: [.mockSecondary, .mockPrimary], + onClick: { _ in } + )! + .preview() + .frame(maxHeight: 70) + .previewDisplayName("Var. | 2 Button") + } + + } + +#endif diff --git a/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift new file mode 100644 index 0000000000..dc95253428 --- /dev/null +++ b/Sources/BrazeUI/InAppMessageUI/Views/Misc/StackView.swift @@ -0,0 +1,41 @@ +import UIKit + +extension BrazeInAppMessageUI { + + /// A `UIStackView` wrapper view. + /// + /// Before iOS 14, `UIStackView` uses a non-rendering `CATransformLayer` instead of a classic + /// `CALayer` (see [tweet](https://archive.md/t0AIh)). This wrapper view allow to set the layer's + /// properties on pre-iOS 14 devices + public class StackView: UIView { + + /// The inner stack view. + public let stack = UIStackView() + + /// The inner stack autolayout position constraints. + public var stackPositionConstraints: [NSLayoutConstraint]! + + public override var intrinsicContentSize: CGSize { + stack.intrinsicContentSize + } + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(stack) + layoutMargins = .zero + stackPositionConstraints = stack.anchors.edges.pin(to: self.layoutMarginsGuide) + } + + /// See `UIStackView/init(arrangedSubviews:)`. + convenience init(arrangedSubviews subviews: [UIView]) { + self.init(frame: .zero) + subviews.forEach(stack.addArrangedSubview) + } + + required init(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + } + +} diff --git a/Sources/BrazeUI/PreviewProviders.swift b/Sources/BrazeUI/PreviewProviders.swift new file mode 100644 index 0000000000..0ab24d2f6f --- /dev/null +++ b/Sources/BrazeUI/PreviewProviders.swift @@ -0,0 +1,71 @@ +#if UI_PREVIEWS + + import SwiftUI + + extension UIView { + + /// Wraps the UIView into a `UIViewRepresentable` class for compatibility with SwiftUI previews. + /// - Parameters: + /// - pin: The priority for pinning the view's edges to the container view (default: `nil`). + /// - center: The priority for centering the view in the container view (default: `nil`). + /// - flex: Allows the view to resize by setting the hugging and compression resistance + /// to low priorities (default: `false`). + /// - layout: A layout closure executed after the previous parameters have been applied. + /// - Returns: A SwiftUI previews compatible view. + @available(iOS 13.0, *) + func preview( + pin: UILayoutPriority? = nil, + center: UILayoutPriority? = nil, + flex: Bool = false, + layout: @escaping (UIView) -> Void = { _ in } + ) -> some View { + final class Wrapper: UIViewRepresentable { + let view: UIView + let pin: UILayoutPriority? + let center: UILayoutPriority? + let flex: Bool + let layout: (UIView) -> Void + var setup = false + + init( + view: UIView, + pin: UILayoutPriority?, + center: UILayoutPriority?, + flex: Bool, + layout: @escaping (UIView) -> Void + ) { + self.view = view + self.pin = pin + self.center = center + self.flex = flex + self.layout = layout + } + + func updateUIView(_ view: UIView, context: Context) { + guard !setup else { return } + setup = true + if let pin = pin { + view.anchors.edges.pin().forEach { $0.priority = pin } + } + if let center = center { + view.anchors.center.align().forEach { $0.priority = center } + } + let flexPriority: UILayoutPriority = flex ? .defaultLow : .required + view.setContentHuggingPriority(flexPriority, for: .horizontal) + view.setContentHuggingPriority(flexPriority, for: .vertical) + view.setContentCompressionResistancePriority(flexPriority, for: .vertical) + view.setContentCompressionResistancePriority(flexPriority, for: .horizontal) + layout(view) + } + func makeUIView(context: Context) -> UIView { + return view + } + } + + return Wrapper(view: self, pin: pin, center: center, flex: flex, layout: layout) + .previewLayout(.sizeThatFits) + } + + } + +#endif diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/Contents.json new file mode 100644 index 0000000000..73c00596a7 --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/Contents.json b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/Contents.json new file mode 100644 index 0000000000..4288080d23 --- /dev/null +++ b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "arrow.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "arrow@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "arrow@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow.png b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow.png new file mode 100644 index 0000000000..a59d07fb08 Binary files /dev/null and b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow.png differ diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@2x.png b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@2x.png new file mode 100644 index 0000000000..c38103e3f8 Binary files /dev/null and b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@2x.png differ diff --git a/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@3x.png b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@3x.png new file mode 100644 index 0000000000..9a1f8e78a5 Binary files /dev/null and b/Sources/BrazeUI/Resources/Assets.xcassets/InAppMessage/chevron.imageset/arrow@3x.png differ diff --git a/Sources/BrazeUI/Resources/FontAwesome.otf b/Sources/BrazeUI/Resources/FontAwesome.otf new file mode 100644 index 0000000000..f7936cc1e7 Binary files /dev/null and b/Sources/BrazeUI/Resources/FontAwesome.otf differ diff --git a/Sources/BrazeUI/Resources/Localization/Base.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/Base.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..e1befa25dd --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/Base.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "Close"; diff --git a/Sources/BrazeUI/Resources/Localization/ar.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ar.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..762f15eb37 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ar.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "لإغلاق"; diff --git a/Sources/BrazeUI/Resources/Localization/cs.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/cs.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..c4ce1a9579 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/cs.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "zavřít"; diff --git a/Sources/BrazeUI/Resources/Localization/da.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/da.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..7386c80798 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/da.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "at lukke"; diff --git a/Sources/BrazeUI/Resources/Localization/de.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/de.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..fb77a28cc8 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/de.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "schließen"; diff --git a/Sources/BrazeUI/Resources/Localization/en.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/en.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..e1befa25dd --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/en.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "Close"; diff --git a/Sources/BrazeUI/Resources/Localization/es-419.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es-419.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..95bfd1b9b6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es-419.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUI/Resources/Localization/es-MX.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es-MX.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..95bfd1b9b6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es-MX.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUI/Resources/Localization/es.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/es.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..95bfd1b9b6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/es.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "cerrar"; diff --git a/Sources/BrazeUI/Resources/Localization/et.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/et.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..cc18c12e3e --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/et.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "sulgema"; diff --git a/Sources/BrazeUI/Resources/Localization/fi.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fi.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..105b099cbc --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fi.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "sulkea"; diff --git a/Sources/BrazeUI/Resources/Localization/fil.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fil.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..4d5d2d1bf1 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fil.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "Isara"; diff --git a/Sources/BrazeUI/Resources/Localization/fr.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/fr.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..8be61ea297 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/fr.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "fermer"; diff --git a/Sources/BrazeUI/Resources/Localization/he.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/he.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..700e77c7a8 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/he.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "לִסְגוֹר"; diff --git a/Sources/BrazeUI/Resources/Localization/hi.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/hi.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..12167c958b --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/hi.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "बंद करना"; diff --git a/Sources/BrazeUI/Resources/Localization/id.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/id.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..cb39e6d613 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/id.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "untuk menutup"; diff --git a/Sources/BrazeUI/Resources/Localization/it.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/it.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..336f9bb4ea --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/it.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "chiudere"; diff --git a/Sources/BrazeUI/Resources/Localization/ja.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ja.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..ad63de3826 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ja.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "閉じる"; diff --git a/Sources/BrazeUI/Resources/Localization/km.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/km.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..0ff6a95646 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/km.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "បិទ"; diff --git a/Sources/BrazeUI/Resources/Localization/ko.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ko.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..16722f8d2b --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ko.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "닫다"; diff --git a/Sources/BrazeUI/Resources/Localization/lo.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/lo.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..f49e0d25b9 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/lo.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "ປິດ"; diff --git a/Sources/BrazeUI/Resources/Localization/ms.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ms.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..cb39e6d613 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ms.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "untuk menutup"; diff --git a/Sources/BrazeUI/Resources/Localization/my.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/my.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..96a158a275 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/my.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "ပိတ်ရန်"; diff --git a/Sources/BrazeUI/Resources/Localization/nb.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/nb.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..863b95eae6 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/nb.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "å lukke"; diff --git a/Sources/BrazeUI/Resources/Localization/nl.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/nl.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..c246fcb65a --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/nl.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "sluiten"; diff --git a/Sources/BrazeUI/Resources/Localization/pl.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pl.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..dc1ee6b2c9 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pl.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "zamknąć"; diff --git a/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..96fdaa3389 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pt-PT.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "fechar"; diff --git a/Sources/BrazeUI/Resources/Localization/pt.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/pt.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..96fdaa3389 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/pt.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "fechar"; diff --git a/Sources/BrazeUI/Resources/Localization/ru.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/ru.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..458288a71a --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/ru.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "закрывать"; diff --git a/Sources/BrazeUI/Resources/Localization/sv.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/sv.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..4a85989f78 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/sv.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "stänga"; diff --git a/Sources/BrazeUI/Resources/Localization/th.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/th.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..6f6b469a7b --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/th.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "ปิด"; diff --git a/Sources/BrazeUI/Resources/Localization/uk.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/uk.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..44eadb6479 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/uk.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "закрити"; diff --git a/Sources/BrazeUI/Resources/Localization/vi.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/vi.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..8136f04198 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/vi.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "đóng"; diff --git a/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..af68836046 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-HK.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..d5686e65cc --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-Hans.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "关闭"; diff --git a/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..af68836046 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-Hant.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..af68836046 --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh-TW.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "關閉"; diff --git a/Sources/BrazeUI/Resources/Localization/zh.lproj/InAppMessageLocalizable.strings b/Sources/BrazeUI/Resources/Localization/zh.lproj/InAppMessageLocalizable.strings new file mode 100644 index 0000000000..d5686e65cc --- /dev/null +++ b/Sources/BrazeUI/Resources/Localization/zh.lproj/InAppMessageLocalizable.strings @@ -0,0 +1 @@ +"braze.in-app-message.close-button.title" = "关闭"; diff --git a/Sources/BrazeUI/Resources/align.license b/Sources/BrazeUI/Resources/align.license new file mode 100644 index 0000000000..96ada17d21 --- /dev/null +++ b/Sources/BrazeUI/Resources/align.license @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017-2020 Alexander Grebenyuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.